2 # -*- coding: utf-8 -*-
4 GalTacK networking - basic client protocol classes.
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):
36 GalTacK network client base class (Galcon-compatible).
39 def __init__(self
, options
, user_info
, server_info
, *a
, **kw
):
43 The arguments are a galtack.net_common.UserInfo and a
44 galtack.net_common.ServerInfo instance.
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
):
53 Called by twisted when a transport is connected to this protocol.
55 self
.transport
.connect(self
._server
_info
.addr
,
56 self
._server
_info
.port
)
58 def datagramReceived(self
, datagram
, host
):
60 Called by twisted when a datagram is received.
62 return self
._commands
_received
(self
, datagram
)
64 def _commands_received(self
, commands
):
66 Called with the commands received.
68 Fill this method with meaning in the mixin classes -- and only
73 def _modify_option_parser(cls
, option_parser
):
75 Modify the specified option_parser (an optparse.OptionParser).
77 This can be used e.g. to add mixin-specific options.
82 def _mkopt(cls
, option_name
):
84 Helper method for _modify_option_parser().
86 return "_%s__%s" % (cls
.__name
__, option_name
)
88 def _getopt(self
, option_name
, kw
, kw_default
= -1):
90 Helper method for __init__() to set an option value.
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
)
99 class GaltackClientSendCmdBaseMixin(GaltackClientBase
):
101 GaltackClient with a send_commands() method.
103 This only provides the general infrastructure for sending GalTacK
104 commands. Sequence number adding and [ACK] handling is done by other
108 _HEADER
= ("[HEADER]\tpasswd\t%s\n" +
109 "[HEADER]\tsecret\t%s\n" +
110 "[HEADER]\tname\t%s\n")
112 def __init__(self
, *a
, **kw
):
113 super(GaltackClientSendCmdBaseMixin
, self
).__init
__(*a
, **kw
)
114 self
.__can
_send
_commands
= False
116 def startProtocol(self
):
117 super(GaltackClientSendCmdBaseMixin
, self
).startProtocol()
118 self
.__can
_send
_commands
= True
120 def stopProtocol(self
):
121 super(GaltackClientSendCmdBaseMixin
, self
).stopProtocol()
122 self
.__can
_send
_commands
= False
124 def _pre_send_command_hook(self
, command
, **kw
):
126 Process a command before sending, e.g. by prepending the sequence
129 The 'command' argument is a tuple created by something like
130 tuple(cmd_line.split("\t")).
132 Remember to call this method using super() from subclasses
133 overriding this hook, and to return its (possibly modified) return
134 value. Even though this method is empty.
138 def _pre_send_commands_hook(self
, *commands
, **kw
):
140 Process the specified commands before sending.
142 Remember to call this method using super() from subclasses
143 overriding this hook, and to return its (possibly modified) return
147 f
= lambda cmd
: self
._pre
_send
_command
_hook
(cmd
, **kw
)
148 return map(f
, map(tuple, commands
))
151 traceback
.print_exc()
154 def _post_send_datagram_hook(self
, datagram
, **kw
):
156 Post-process the sent datagram.
160 def send_commands(self
, *commands
, **kw
):
162 Send the specified commands.
164 Any keyword arguments given are passed on to the hooks.
166 if not self
.__can
_send
_commands
:
167 raise Warning, ("unable to send commands since " +
168 "the protocol has not been started yet")
169 if not commands
: # no cmds to send, e.g. when they were generated
172 header
= self
._HEADER
% (self
._server
_info
.passwd
,
173 self
._server
_info
.secret
,
174 self
._user
_info
.name
)
176 cmds
= self
._pre
_send
_commands
_hook
(*commands
, **kw
)
177 cmds_str
= "\n".join("\t".join(map(str, c
)) for c
in cmds
)
179 datagram
= header
+ cmds_str
+ '\n'
180 self
.transport
.write(datagram
)
182 self
._post
_send
_datagram
_hook
(datagram
, **kw
)
187 class GaltackClientIgnoreResentCmdsMixin(GaltackClientBase
):
189 GaltackClient that ignores resent commands from the server.
191 The server might resend a command since it didn't get our [ACK]
192 response (done manually or using GaltackClientAckSenderMixin), in
193 which case only the first instance of that command is important to us
194 for post-[ACK]-sending-processing.
196 As it gets increasingly expensive to keep track of really *all*
197 commands (or their sequence numbers) that got ever sent to us, this
198 class by default keeps a ring buffer of the sequence numbers of the
199 last 100 commands received.
201 If you want to be really strict and don't care about performance at
202 all, pass __init__(ignore_resent_cmds_ring_bufsize = None).
205 def __init__(self
, *a
, **kw
):
206 self
.__command
_callbacks
= {}
207 self
.__seq
_nr
_buffer
= []
208 bufsize
= kw
.pop("ignore_resent_cmds_ring_bufsize", 100)
209 self
.__seq
_nr
_buffer
_size
= bufsize
210 super(GaltackClientIgnoreResentCmdsMixin
, self
).__init
__(*a
, **kw
)
212 def __seq_nr_buffer_truncate(self
):
214 Truncate self.__seq_nr_buffer to self.__seq_nr_buffer_size.
216 bufsize
= self
.__seq
_nr
_buffer
_size
217 if bufsize
is None: return None
218 self
.__seq
_nr
_buffer
= self
.__seq
_nr
_buffer
[-bufsize
:]
220 def datagramReceived(self
, datagram
, host
):
221 commands
= tuple( tuple(line
.split("\t"))
222 for line
in datagram
.split("\n") )
224 commands
= filter(lambda c
: c
!= ("",), commands
)
226 for command
in commands
:
227 try: seq_nr
, want_ack
= map(int, command
[:2])
228 except: continue # in case the server would send [HEADER]
229 if seq_nr
== 1: # numbers got reset, we reset too
230 self
.__seq
_nr
_buffer
= []
231 if seq_nr
in self
.__seq
_nr
_buffer
: # command already received
233 new_commands
.append(command
)
234 self
.__seq
_nr
_buffer
.append(seq_nr
)
235 self
.__seq
_nr
_buffer
_truncate
()
236 return self
._commands
_received
("\n".join( map("\t".join
,
240 class GaltackClientBackCallerMixin(GaltackClientIgnoreResentCmdsMixin
):
242 GaltackClient that allows callbacks to be registered to respond to
243 certain GalTacK commands received from a server.
246 def __init__(self
, *a
, **kw
):
247 self
.__command
_callbacks
= {}
248 super(GaltackClientBackCallerMixin
, self
).__init
__(*a
, **kw
)
250 def register_command_callback(self
, cmd_name
= None, callback
= None):
252 Register a command with a callback.
255 galtack_client.register_command_callback("message",
258 Callbacks registered to no command name (cmd_name = None) are
259 implicitly registered to all command names (individually).
261 galtack_client.register_command_callback(callback =
264 Callbacks registered to cmd_name = False are registered to the
265 whole command *chunks* that arrive in a UDP packet.
267 The signature of a callback function (add self for a method) looks
269 def callback(command)
270 The 'command' argument is a tuple created by something like
271 tuple(cmd_line.split("\t")), or, in the case of a callback
272 registered to cmd_name = False, a tuple of those tuples.
274 # command argument is optional - callback is not:
276 raise TypeError, ("register_command_callback() requires the " +
278 callback_list
= self
.__command
_callbacks
.get(cmd_name
, [])
279 callback_list
.append(callback
)
280 self
.__command
_callbacks
[cmd_name
] = callback_list
282 def unregister_command_callback(self
, cmd_name
= None,
285 Unregister a callback.
287 Works like register_command_callback(), just reversed.
289 callback_list
= self
.__command
_callbacks
[cmd_name
]
290 # Do it this way to remove the _last_ (added) callback:
291 callback_list
.reverse()
292 callback_list
.remove(callback
)
293 callback_list
.reverse()
295 def call_command_callbacks(self
, cmd_name
= None, *a
, **kw
):
297 Run the callbacks registered to cmd_name with the given arguments.
299 Calling this method can be used to simulate commands received from
302 Alternatively, you can use it to "invent" commands.
305 if kw
.pop("run_common", False):
306 cb_list
= self
.__command
_callbacks
.get(None, [])
307 cb_list
+= self
.__command
_callbacks
.get(cmd_name
, [])
308 for callback
in cb_list
:
311 def _commands_received(self
, commands
):
313 Call the registered callbacks for the received commands.
315 commands
= tuple( tuple(line
.split("\t"))
316 for line
in commands
.split("\n") )
317 commands
= filter(lambda c
: c
, commands
) # drop empty lines
319 self
.call_command_callbacks(False, commands
)
321 common_callbacks
= self
.__command
_callbacks
.get(None, [])
322 for command
in commands
:
323 try: map(int, command
[:2])
324 except: continue # in case the server would send [HEADER]
326 self
.call_command_callbacks(name
, command
, run_common
= True)
328 # Call parent class' _commands_received() method:
329 Cls
= GaltackClientBackCallerMixin
330 return super(Cls
, self
)._commands
_received
(commands
)
333 class GaltackClientPrependSeqNrMixin(GaltackClientBackCallerMixin
,
334 GaltackClientSendCmdBaseMixin
):
336 GaltackClient that adds the sequence number to each command before
340 def __init__(self
, *a
, **kw
):
341 super(GaltackClientPrependSeqNrMixin
, self
).__init
__(*a
, **kw
)
343 self
.register_command_callback("[RESET]", self
.__cmd
_reset
)
345 def _pre_send_command_hook(self
, command
, **kw
):
347 To disable this hook, call:
348 send_command(..., prepend_seq_nr = False)
350 prepend_seq_nr
= kw
.pop("prepend_seq_nr", True)
351 Cls
= GaltackClientPrependSeqNrMixin
352 command
= super(Cls
, self
)._pre
_send
_command
_hook
(command
, **kw
)
355 command
= (self
.__seq
_nr
,) + command
358 def __cmd_reset(self
, command
):
359 if int(command
[0]) == 1:
363 class GaltackClientAckSenderMixin(GaltackClientPrependSeqNrMixin
,
364 GaltackClientIgnoreResentCmdsMixin
):
366 GaltackClient that sends out [ACK] commands when needed.
368 This intercepts datagrams *before* GaltackClientIgnoreResentCmdsMixin
369 gets a chance to process them.
372 def __init__(self
, *a
, **kw
):
373 super(GaltackClientAckSenderMixin
, self
).__init
__(*a
, **kw
)
374 self
.__debug
_log
= file("/tmp/acksend.log", "w")
376 def datagramReceived(self
, datagram
, host
):
377 self
.__debug
_log
.write("\n* datagramReceived:\n\n%s\n\n" % datagram
)
378 Cls
= GaltackClientAckSenderMixin
379 super(Cls
, self
).datagramReceived(datagram
, host
)
380 self
.__debug
_log
.write("\n* Received\n")
381 commands
= tuple( tuple(line
.split("\t"))
382 for line
in datagram
.split("\n") )
384 for command
in commands
:
386 seq_nr
, want_ack
= map(int, command
[0:2])
387 except: continue # in case the server would send [HEADER]
388 if not want_ack
: continue
389 seq_nr_list
.append(seq_nr
)
390 self
.send_commands(*((0, "[ACK]", sn
) for sn
in seq_nr_list
))