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 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") )
135 commands
= filter(lambda c
: c
!= ("",), 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
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
,
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.
166 galtack_client.register_command_callback("message",
169 Callbacks registered to no command name (cmd_name = None) are
170 implicitly registered to all command names (individually).
172 galtack_client.register_command_callback(callback =
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
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:
187 raise TypeError, ("register_command_callback() requires the " +
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,
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
213 Alternatively, you can use it to "invent" commands.
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
:
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]
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
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
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.
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
292 f
= lambda cmd
: self
._pre
_send
_command
_hook
(cmd
, **kw
)
293 return map(f
, map(tuple, commands
))
296 traceback
.print_exc()
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
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
)