Fixed GaltackClientIgnoreResentCmdsMixin
[galtack.git] / galtack / net_client_base.py
blob405d27ae6b18b8d2a6c225240d1878f3a02eaa0b
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 """
4 GalTacK 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 galtack.net_common import *
34 class GaltackClientBase(DatagramProtocol, object):
35 """
36 GalTacK network client base class (Galcon-compatible).
37 """
39 def __init__(self, options, user_info, server_info, *a, **kw):
40 """
41 Initialize.
43 The arguments are a galtack.net_common.UserInfo and a
44 galtack.net_common.ServerInfo instance.
45 """
46 super(GaltackClientBase, self).__init__(*a, **kw)
47 self._options = options
48 self._user_info = user_info
49 self._server_info = server_info
51 def startProtocol(self):
52 """
53 Called by twisted when a transport is connected to this protocol.
54 """
55 self.transport.connect(self._server_info.addr,
56 self._server_info.port)
58 def datagramReceived(self, datagram, host):
59 """
60 Called by twisted when a datagram is received.
61 """
62 return self._commands_received(self, datagram)
64 def _commands_received(self, commands):
65 """
66 Called with the commands received.
68 Fill this method with meaning in the mixin classes -- and only
69 there, please.
70 """
72 @classmethod
73 def _modify_option_parser(cls, option_parser):
74 """
75 Modify the specified option_parser (an optparse.OptionParser).
77 This can be used e.g. to add mixin-specific options.
78 """
79 return option_parser
81 @classmethod
82 def _mkopt(cls, option_name):
83 """
84 Helper method for _modify_option_parser().
85 """
86 return "_%s__%s" % (cls.__name__, option_name)
88 def _getopt(self, option_name, kw, kw_default = -1):
89 """
90 Helper method for __init__() to set an option value.
91 """
92 value = kw.pop(option_name, kw_default)
93 if value is kw_default:
94 name = "_%s__%s" % (self.__class__.__name__, option_name)
95 value = getattr(self._options, name)
96 return value
99 class GaltackClientIgnoreResentCmdsMixin(GaltackClientBase):
101 GaltackClient that ignores resent commands from the server.
103 The server might resend a command since it didn't get our [ACK]
104 response (done manually or using GaltackClientAckSendingMixin), in
105 which case only the first instance of that command is important to us.
107 As it gets increasingly expensive to keep track of really *all*
108 commands (or their sequence numbers) that got ever sent to us, this
109 class by default keeps a ring buffer of the sequence numbers of the
110 last 100 commands received.
112 If you want to be really strict and don't care about performance at
113 all, pass __init__(ignore_resent_cmds_ring_bufsize = None).
116 def __init__(self, *a, **kw):
117 self.__command_callbacks = {}
118 self.__seq_nr_buffer = []
119 bufsize = kw.pop("ignore_resent_cmds_ring_bufsize", 100)
120 self.__seq_nr_buffer_size = bufsize
121 super(GaltackClientIgnoreResentCmdsMixin, self).__init__(*a, **kw)
123 def __seq_nr_buffer_truncate(self):
125 Truncate self.__seq_nr_buffer to self.__seq_nr_buffer_size.
127 bufsize = self.__seq_nr_buffer_size
128 if bufsize is None: return None
129 self.__seq_nr_buffer = self.__seq_nr_buffer[-bufsize:]
131 def datagramReceived(self, datagram, host):
132 commands = tuple( tuple(line.split("\t"))
133 for line in datagram.split("\n") )
134 # drop empty lines
135 commands = filter(lambda c: c != ("",), commands)
136 new_commands = []
137 for command in commands:
138 try: seq_nr, want_ack = map(int, command[:2])
139 except: continue # in case the server would send [HEADER]
140 if seq_nr == 1: # numbers got reset, we reset too
141 self.__seq_nr_buffer = []
142 if seq_nr in self.__seq_nr_buffer: # command already received
143 continue
144 new_commands.append(command)
145 self.__seq_nr_buffer.append(seq_nr)
146 self.__seq_nr_buffer_truncate()
147 return self._commands_received("\n".join( map("\t".join,
148 new_commands) ))
151 class GaltackClientBackCallerMixin(GaltackClientIgnoreResentCmdsMixin):
153 GaltackClient that allows callbacks to be registered to respond to
154 certain GalTacK commands received from a server.
157 def __init__(self, *a, **kw):
158 self.__command_callbacks = {}
159 super(GaltackClientBackCallerMixin, self).__init__(*a, **kw)
161 def register_command_callback(self, cmd_name = None, callback = None):
163 Register a command with a callback.
165 Example:
166 galtack_client.register_command_callback("message",
167 cmd_message)
169 Callbacks registered to no command name (cmd_name = None) are
170 implicitly registered to all command names (individually).
171 Example:
172 galtack_client.register_command_callback(callback =
173 cmd_catchall)
175 Callbacks registered to cmd_name = False are registered to the
176 whole command *chunks* that arrive in a UDP packet.
178 The signature of a callback function (add self for a method) looks
179 like this:
180 def callback(command)
181 The 'command' argument is a tuple created by something like
182 tuple(cmd_line.split("\t")), or, in the case of a callback
183 registered to cmd_name = False, a tuple of those tuples.
185 # command argument is optional - callback is not:
186 if callback is None:
187 raise TypeError, ("register_command_callback() requires the " +
188 "callback argument")
189 callback_list = self.__command_callbacks.get(cmd_name, [])
190 callback_list.append(callback)
191 self.__command_callbacks[cmd_name] = callback_list
193 def unregister_command_callback(self, cmd_name = None,
194 callback = None):
196 Unregister a callback.
198 Works like register_command_callback(), just reversed.
200 callback_list = self.__command_callbacks[cmd_name]
201 # Do it this way to remove the _last_ (added) callback:
202 callback_list.reverse()
203 callback_list.remove(callback)
204 callback_list.reverse()
206 def call_command_callbacks(self, cmd_name = None, *a, **kw):
208 Run the callbacks registered to cmd_name with the given arguments.
210 Calling this method can be used to simulate commands received from
211 the server.
213 Alternatively, you can use it to "invent" commands.
215 cb_list = []
216 if kw.pop("run_common", False):
217 cb_list = self.__command_callbacks.get(None, [])
218 cb_list += self.__command_callbacks.get(cmd_name, [])
219 for callback in cb_list:
220 callback(*a, **kw)
222 def _commands_received(self, commands):
224 Call the registered callbacks for the received commands.
226 commands = tuple( tuple(line.split("\t"))
227 for line in commands.split("\n") )
228 commands = filter(lambda c: c, commands) # drop empty lines
230 self.call_command_callbacks(False, commands)
232 common_callbacks = self.__command_callbacks.get(None, [])
233 for command in commands:
234 try: map(int, command[:2])
235 except: continue # in case the server would send [HEADER]
236 name = command[2]
237 self.call_command_callbacks(name, command, run_common = True)
239 # Call parent class' _commands_received() method:
240 Cls = GaltackClientBackCallerMixin
241 return super(Cls, self)._commands_received(commands)
244 class GaltackClientSendCmdBaseMixin(GaltackClientBase):
246 GaltackClient with a send_commands() method.
248 This only provides the general infrastructure for sending GalTacK
249 commands. Sequence number adding and [ACK] handling is done by other
250 mixin classes.
253 _HEADER = ("[HEADER]\tpasswd\t%s\n" +
254 "[HEADER]\tsecret\t%s\n" +
255 "[HEADER]\tname\t%s\n")
257 def __init__(self, *a, **kw):
258 super(GaltackClientSendCmdBaseMixin, self).__init__(*a, **kw)
259 self.__can_send_commands = False
261 def startProtocol(self):
262 super(GaltackClientSendCmdBaseMixin, self).startProtocol()
263 self.__can_send_commands = True
265 def stopProtocol(self):
266 super(GaltackClientSendCmdBaseMixin, self).stopProtocol()
267 self.__can_send_commands = False
269 def _pre_send_command_hook(self, command, **kw):
271 Process a command before sending, e.g. by prepending the sequence
272 number.
274 The 'command' argument is a tuple created by something like
275 tuple(cmd_line.split("\t")).
277 Remember to call this method using super() from subclasses
278 overriding this hook, and to return its (possibly modified) return
279 value. Even though this method is empty.
281 return command
283 def _pre_send_commands_hook(self, *commands, **kw):
285 Process the specified commands before sending.
287 Remember to call this method using super() from subclasses
288 overriding this hook, and to return its (possibly modified) return
289 value.
291 try:
292 f = lambda cmd: self._pre_send_command_hook(cmd, **kw)
293 return map(f, map(tuple, commands))
294 except:
295 import traceback
296 traceback.print_exc()
297 return commands
299 def send_commands(self, *commands, **kw):
301 Send the specified commands.
303 Any keyword arguments given are passed on to the hooks.
305 if not self.__can_send_commands:
306 raise Warning, ("unable to send commands since " +
307 "the protocol has not been started yet")
308 if not commands: # no cmds to send, e.g. when they were generated
309 return
311 header = self._HEADER % (self._server_info.passwd,
312 self._server_info.secret,
313 self._user_info.name)
315 cmds = self._pre_send_commands_hook(*commands, **kw)
316 cmds_str = "\n".join("\t".join(map(str, c)) for c in cmds)
318 packet = header + cmds_str + '\n'
319 self.transport.write(packet)
321 return cmds