2 # -*- coding: utf-8 -*-
4 Galcon 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 galcon
.net_common
import *
34 class GalconClientBase(DatagramProtocol
, object):
36 Galcon network client base class.
39 def __init__(self
, user_info
, server_info
, *a
, **kw
):
43 The arguments are a galcon.net_common.UserInfo and a
44 galcon.net_common.ServerInfo instance.
46 super(GalconClientBase
, self
).__init
__(*a
, **kw
)
47 self
._user
_info
= user_info
48 self
._server
_info
= server_info
50 def startProtocol(self
):
52 Called by twisted when a transport is connected to this protocol.
54 self
.transport
.connect(self
._server
_info
.addr
,
55 self
._server
_info
.port
)
57 def datagramReceived(self
, datagram
, host
):
59 Called by twisted when a datagram is received.
61 return self
._commands
_received
(self
, datagram
)
63 def _commands_received(self
, commands
):
65 Called with the commands received.
67 Fill this method with meaning in the mixin classes -- and only
72 class GalconClientIgnoreResentCmdsMixin(GalconClientBase
):
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).
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
):
98 Truncate self.__seq_nr_buffer to self.__seq_nr_buffer_size.
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
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
= []
115 if seq_nr
in self
.__seq
_nr
_buffer
: # command already received
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
,
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.
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).
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
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:
158 raise TypeError, ("register_command_callback() requires the " +
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,
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
184 Alternatively, you can use it to "invent" commands.
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
:
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]
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
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
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.
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
263 f
= lambda cmd
: self
._pre
_send
_command
_hook
(cmd
, **kw
)
264 return map(f
, map(tuple, commands
))
267 traceback
.print_exc()
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
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
)