Merge branch 'master' into website
[galtack.git] / galcon / net_client_base.py
blob7df6efaa4bf8f9106db8595f2c6fd71b068291b5
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 """
4 Galcon networking - basic client protocol classes.
5 """
6 # Copyright (C) 2007 Felix Rabe <public@felixrabe.textdriven.com>
7 # Copyright (C) 2007 Michael Carter
9 # Permission is hereby granted, free of charge, to any person obtaining a
10 # copy of this software and associated documentation files (the
11 # "Software"), to deal in the Software without restriction, including
12 # without limitation the rights to use, copy, modify, merge, publish,
13 # distribute, sublicense, and/or sell copies of the Software, and to permit
14 # persons to whom the Software is furnished to do so, subject to the
15 # following conditions:
17 # The above copyright notice and this permission notice shall be included
18 # in all copies or substantial portions of the Software.
20 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
21 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
23 # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
24 # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
25 # OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
26 # THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28 # Recommended line length or text width: 75 characters.
30 from twisted.internet.protocol import DatagramProtocol
31 from galcon.net_common import *
34 class GalconClientBase(DatagramProtocol, object):
35 """
36 Galcon network client base class.
37 """
39 def __init__(self, user_info, server_info, *a, **kw):
40 """
41 Initialize.
43 The arguments are a galcon.net_common.UserInfo and a
44 galcon.net_common.ServerInfo instance.
45 """
46 super(GalconClientBase, self).__init__(*a, **kw)
47 self._user_info = user_info
48 self._server_info = server_info
50 def startProtocol(self):
51 """
52 Called by twisted when a transport is connected to this protocol.
53 """
54 self.transport.connect(self._server_info.addr,
55 self._server_info.port)
57 def datagramReceived(self, datagram, host):
58 """
59 Called by twisted when a datagram is received.
60 """
61 return self._commands_received(self, datagram)
63 def _commands_received(self, commands):
64 """
65 Called with the commands received.
67 Fill this method with meaning in the mixin classes -- and only
68 there, please.
69 """
72 class GalconClientIgnoreResentCmdsMixin(GalconClientBase):
73 """
74 GalconClient that ignores resent commands from the server.
76 The server might resend a command since it didn't get our [ACK]
77 response (done manually or using GalconClientAckSendingMixin), in which
78 case only the first instance of that command is important to us.
80 As it gets increasingly expensive to keep track of really *all*
81 commands (or their sequence numbers) that got ever sent to us, this
82 class by default keeps a ring buffer of the sequence numbers of the
83 last 100 commands received.
85 If you want to be really strict and don't care about performance at
86 all, pass __init__(ignore_resent_cmds_ring_bufsize = None).
87 """
89 def __init__(self, *a, **kw):
90 self.__command_callbacks = {}
91 self.__seq_nr_buffer = []
92 bufsize = kw.pop("ignore_resent_cmds_ring_bufsize", 100)
93 self.__seq_nr_buffer_size = bufsize
94 super(GalconClientIgnoreResentCmdsMixin, self).__init__(*a, **kw)
96 def __seq_nr_buffer_truncate(self):
97 """
98 Truncate self.__seq_nr_buffer to self.__seq_nr_buffer_size.
99 """
100 bufsize = self.__seq_nr_buffer_size
101 if bufsize is None: return None
102 self.__seq_nr_buffer = self.__seq_nr_buffer[:-bufsize]
104 def datagramReceived(self, datagram, host):
105 commands = tuple( tuple(line.split('\t'))
106 for line in datagram.split('\n') )
107 commands = filter(lambda c: c, commands) # drop empty lines
108 new_commands = []
109 for command in commands:
110 try: seq_nr, want_ack = map(int, command[:2])
111 except: continue # in case the server would send [HEADER]
112 if seq_nr == 1: # numbers got reset, we reset too
113 self.__seq_nr_buffer = []
114 continue
115 if seq_nr in self.__seq_nr_buffer: # command already received
116 continue
117 new_commands.append(command)
118 self.__seq_nr_buffer.append(seq_nr)
119 self.__seq_nr_buffer_truncate()
120 return self._commands_received("\n".join( map("\t".join,
121 new_commands) ))
124 class GalconClientBackCallerMixin(GalconClientIgnoreResentCmdsMixin):
126 GalconClient that allows callbacks to be registered to respond to
127 certain Galcon commands received from a server.
130 def __init__(self, *a, **kw):
131 self.__command_callbacks = {}
132 super(GalconClientBackCallerMixin, self).__init__(*a, **kw)
134 def register_command_callback(self, cmd_name = None, callback = None):
136 Register a command with a callback.
138 Example:
139 galcon_client.register_command_callback("message", cb_message)
141 Callbacks registered to no command name (cmd_name = None) are
142 implicitly registered to all command names (individually).
143 Example:
144 galcon_client.register_command_callback(callback = cb_catchall)
146 Callbacks registered to cmd_name = False are registered to the
147 whole command *chunks* that arrive in a UDP packet.
149 The signature of a callback function (add self for a method) looks
150 like this:
151 def callback(command)
152 The 'command' argument is a tuple created by something like
153 tuple(cmd_line.split("\t")), or, in the case of a callback
154 registered to cmd_name = False, a tuple of those tuples.
156 # command argument is optional - callback is not:
157 if callback is None:
158 raise TypeError, ("register_command_callback() requires the " +
159 "callback argument")
160 callback_list = self.__command_callbacks.get(cmd_name, [])
161 callback_list.append(callback)
162 self.__command_callbacks[cmd_name] = callback_list
164 def unregister_command_callback(self, cmd_name = None,
165 callback = None):
167 Unregister a callback.
169 Works like register_command_callback(), just reversed.
171 callback_list = self.__command_callbacks[cmd_name]
172 # Do it this way to remove the _last_ (added) callback:
173 callback_list.reverse()
174 callback_list.remove(callback)
175 callback_list.reverse()
177 def call_command_callbacks(self, cmd_name = None, *a, **kw):
179 Run the callbacks registered to cmd_name with the given arguments.
181 Calling this method can be used to simulate commands received from
182 the server.
184 Alternatively, you can use it to "invent" commands.
186 cb_list = []
187 if kw.pop("run_common", False):
188 cb_list = self.__command_callbacks.get(None, [])
189 cb_list += self.__command_callbacks.get(cmd_name, [])
190 for callback in cb_list:
191 callback(*a, **kw)
193 def _commands_received(self, commands):
195 Call the registered callbacks for the received commands.
197 commands = tuple( tuple(line.split('\t'))
198 for line in commands.split('\n') )
199 commands = filter(lambda c: c, commands) # drop empty lines
201 self.call_command_callbacks(False, commands)
203 common_callbacks = self.__command_callbacks.get(None, [])
204 for command in commands:
205 try: map(int, command[:2])
206 except: continue # in case the server would send [HEADER]
207 name = command[2]
208 self.call_command_callbacks(name, command, run_common = True)
210 # Call parent class' _commands_received() method:
211 Cls = GalconClientBackCallerMixin
212 return super(Cls, self)._commands_received(commands)
215 class GalconClientSendCmdBaseMixin(GalconClientBase):
217 GalconClient with a send_commands() method.
219 This only provides the general infrastructure for sending Galcon
220 commands. Sequence number adding and [ACK] handling is done by other
221 mixin classes.
224 _HEADER = ("[HEADER]\tpasswd\t%s\n" +
225 "[HEADER]\tsecret\t%s\n" +
226 "[HEADER]\tname\t%s\n")
228 def __init__(self, *a, **kw):
229 super(GalconClientSendCmdBaseMixin, self).__init__(*a, **kw)
230 self.__can_send_commands = False
232 def startProtocol(self):
233 super(GalconClientSendCmdBaseMixin, self).startProtocol()
234 self.__can_send_commands = True
236 def stopProtocol(self):
237 super(GalconClientSendCmdBaseMixin, self).stopProtocol()
238 self.__can_send_commands = False
240 def _pre_send_command_hook(self, command, **kw):
242 Process a command before sending, e.g. by prepending the sequence
243 number.
245 The 'command' argument is a tuple created by something like
246 tuple(cmd_line.split("\t")).
248 Remember to call this method using super() from subclasses
249 overriding this hook, and to return its (possibly modified) return
250 value. Even though this method is empty.
252 return command
254 def _pre_send_commands_hook(self, *commands, **kw):
256 Process the specified commands before sending.
258 Remember to call this method using super() from subclasses
259 overriding this hook, and to return its (possibly modified) return
260 value.
262 try:
263 f = lambda cmd: self._pre_send_command_hook(cmd, **kw)
264 return map(f, map(tuple, commands))
265 except:
266 import traceback
267 traceback.print_exc()
268 return commands
270 def send_commands(self, *commands, **kw):
272 Send the specified commands.
274 Any keyword arguments given are passed on to the hooks.
276 if not self.__can_send_commands:
277 raise Warning, ("unable to send commands since " +
278 "the protocol has not been started yet")
279 if not commands: # no cmds to send, e.g. when they were generated
280 return
282 header = self._HEADER % (self._server_info.passwd,
283 self._server_info.secret,
284 self._user_info.name)
286 cmds = self._pre_send_commands_hook(*commands, **kw)
287 cmds_str = "\n".join("\t".join(map(str, c)) for c in cmds)
289 packet = header + cmds_str + '\n'
290 self.transport.write(packet)
292 return cmds