]>
Commit | Line | Data |
---|---|---|
1 | # | |
2 | # This was modified from rpcMixin.py distributed with wxPython | |
3 | # | |
4 | #---------------------------------------------------------------------- | |
5 | # Name: rpcMixin | |
6 | # Version: 0.2.0 | |
7 | # Purpose: provides xmlrpc server functionality for wxPython | |
8 | # applications via a mixin class | |
9 | # | |
10 | # Requires: (1) Python with threading enabled. | |
11 | # (2) xmlrpclib from PythonWare | |
12 | # (http://www.pythonware.com/products/xmlrpc/) | |
13 | # the code was developed and tested using version 0.9.8 | |
14 | # | |
15 | # Author: greg Landrum (Landrum@RationalDiscovery.com) | |
16 | # | |
17 | # Copyright: (c) 2000, 2001 by Greg Landrum and Rational Discovery LLC | |
18 | # Licence: wxWindows license | |
19 | #---------------------------------------------------------------------- | |
20 | # 12/11/2003 - Jeff Grimmett (grimmtooth@softhome.net) | |
21 | # | |
22 | # o 2.5 compatability update. | |
23 | # o xmlrpcserver not available. | |
24 | # | |
25 | ||
26 | """provides xmlrpc server functionality for wxPython applications via a mixin class | |
27 | ||
28 | **Some Notes:** | |
29 | ||
30 | 1) The xmlrpc server runs in a separate thread from the main GUI | |
31 | application, communication between the two threads using a custom | |
32 | event (see the Threads demo in the wxPython docs for more info). | |
33 | ||
34 | 2) Neither the server nor the client are particularly smart about | |
35 | checking method names. So it's easy to shoot yourself in the foot | |
36 | by calling improper methods. It would be pretty easy to add | |
37 | either a list of allowed methods or a list of forbidden methods. | |
38 | ||
39 | 3) Authentication of xmlrpc clients is *not* performed. I think it | |
40 | would be pretty easy to do this in a hacky way, but I haven't done | |
41 | it yet. | |
42 | ||
43 | 4) See the bottom of this file for an example of using the class. | |
44 | ||
45 | **Obligatory disclaimer:** | |
46 | This is my first crack at both using xmlrpc and multi-threaded | |
47 | programming, so there could be huge horrible bugs or design | |
48 | flaws. If you see one, I'd love to hear about them. | |
49 | ||
50 | """ | |
51 | ||
52 | ||
53 | """ ChangeLog | |
54 | 23 May 2001: Version bumped to 0.2.0 | |
55 | Numerous code and design changes | |
56 | ||
57 | 21 Mar. 2001: Version bumped to 0.1.4 | |
58 | Updated rpcMixin.OnExternal to support methods with further references | |
59 | (i.e. now you can do rpcClient.foo.bar() and have it work) | |
60 | This probably ain't super legal in xmlrpc land, but it works just fine here | |
61 | and we need it. | |
62 | ||
63 | 6 Mar. 2001: Version bumped to 0.1.3 | |
64 | Documentation changes to make this compatible with happydoc | |
65 | ||
66 | 21 Jan. 2001: Version bumped to 0.1.2 | |
67 | OnExternal() method in the mixin class now uses getattr() to check if | |
68 | a desired method is present. It should have been done this way in | |
69 | the first place. | |
70 | 14 Dec. 2000: Version bumped to 0.1.1 | |
71 | rearranged locking code and made other changes so that multiple | |
72 | servers in one application are possible. | |
73 | ||
74 | """ | |
75 | ||
76 | import new | |
77 | import SocketServer | |
78 | import sys | |
79 | import threading | |
80 | import xmlrpclib | |
81 | import xmlrpcserver | |
82 | ||
83 | import wx | |
84 | ||
85 | rpcPENDING = 0 | |
86 | rpcDONE = 1 | |
87 | rpcEXCEPT = 2 | |
88 | ||
89 | class RPCRequest: | |
90 | """A wrapper to use for handling requests and their responses""" | |
91 | status = rpcPENDING | |
92 | result = None | |
93 | ||
94 | # here's the ID for external events | |
95 | wxEVT_EXTERNAL_EVENT = wx.NewEventType() | |
96 | EVT_EXTERNAL_EVENT = wx.PyEventBinder(wxEVT_EXTERNAL_EVENT, 0) | |
97 | ||
98 | class ExternalEvent(wx.PyEvent): | |
99 | """The custom event class used to pass xmlrpc calls from | |
100 | the server thread into the GUI thread | |
101 | ||
102 | """ | |
103 | def __init__(self,method,args): | |
104 | wx.PyEvent.__init__(self) | |
105 | self.SetEventType(wxEVT_EXTERNAL_EVENT) | |
106 | self.method = method | |
107 | self.args = args | |
108 | self.rpcStatus = RPCRequest() | |
109 | self.rpcStatusLock = threading.Lock() | |
110 | self.rpcCondVar = threading.Condition() | |
111 | ||
112 | def Destroy(self): | |
113 | self.method=None | |
114 | self.args=None | |
115 | self.rpcStatus = None | |
116 | self.rpcStatusLock = None | |
117 | self.rpcondVar = None | |
118 | ||
119 | class Handler(xmlrpcserver.RequestHandler): | |
120 | """The handler class that the xmlrpcserver actually calls | |
121 | when a request comes in. | |
122 | ||
123 | """ | |
124 | def log_message(self,*args): | |
125 | """ causes the server to stop spewing messages every time a request comes in | |
126 | ||
127 | """ | |
128 | pass | |
129 | def call(self,method,params): | |
130 | """When an xmlrpc request comes in, this is the method that | |
131 | gets called. | |
132 | ||
133 | **Arguments** | |
134 | ||
135 | - method: name of the method to be called | |
136 | ||
137 | - params: arguments to that method | |
138 | ||
139 | """ | |
140 | if method == '_rpcPing': | |
141 | # we just acknowledge these without processing them | |
142 | return 'ack' | |
143 | ||
144 | # construct the event | |
145 | evt = ExternalEvent(method,params) | |
146 | ||
147 | # update the status variable | |
148 | evt.rpcStatusLock.acquire() | |
149 | evt.rpcStatus.status = rpcPENDING | |
150 | evt.rpcStatusLock.release() | |
151 | ||
152 | evt.rpcCondVar.acquire() | |
153 | # dispatch the event to the GUI | |
154 | wx.PostEvent(self._app,evt) | |
155 | ||
156 | # wait for the GUI to finish | |
157 | while evt.rpcStatus.status == rpcPENDING: | |
158 | evt.rpcCondVar.wait() | |
159 | evt.rpcCondVar.release() | |
160 | evt.rpcStatusLock.acquire() | |
161 | if evt.rpcStatus.status == rpcEXCEPT: | |
162 | # The GUI threw an exception, release the status lock | |
163 | # and re-raise the exception | |
164 | evt.rpcStatusLock.release() | |
165 | raise evt.rpcStatus.result[0],evt.rpcStatus.result[1] | |
166 | else: | |
167 | # everything went through without problems | |
168 | s = evt.rpcStatus.result | |
169 | ||
170 | evt.rpcStatusLock.release() | |
171 | evt.Destroy() | |
172 | self._app = None | |
173 | return s | |
174 | ||
175 | # this global Event is used to let the server thread | |
176 | # know when it should quit | |
177 | stopEvent = threading.Event() | |
178 | stopEvent.clear() | |
179 | ||
180 | class _ServerThread(threading.Thread): | |
181 | """ this is the Thread class which actually runs the server | |
182 | ||
183 | """ | |
184 | def __init__(self,server,verbose=0): | |
185 | self._xmlServ = server | |
186 | threading.Thread.__init__(self,verbose=verbose) | |
187 | ||
188 | def stop(self): | |
189 | stopEvent.set() | |
190 | ||
191 | def shouldStop(self): | |
192 | return stopEvent.isSet() | |
193 | ||
194 | def run(self): | |
195 | while not self.shouldStop(): | |
196 | self._xmlServ.handle_request() | |
197 | self._xmlServ = None | |
198 | ||
199 | class rpcMixin: | |
200 | """A mixin class to provide xmlrpc server functionality to wxPython | |
201 | frames/windows | |
202 | ||
203 | If you want to customize this, probably the best idea is to | |
204 | override the OnExternal method, which is what's invoked when an | |
205 | RPC is handled. | |
206 | ||
207 | """ | |
208 | ||
209 | # we'll try a range of ports for the server, this is the size of the | |
210 | # range to be scanned | |
211 | nPortsToTry=20 | |
212 | if sys.platform == 'win32': | |
213 | defPort = 800 | |
214 | else: | |
215 | defPort = 8023 | |
216 | ||
217 | def __init__(self,host='',port=-1,verbose=0,portScan=1): | |
218 | """Constructor | |
219 | ||
220 | **Arguments** | |
221 | ||
222 | - host: (optional) the hostname for the server | |
223 | ||
224 | - port: (optional) the port the server will use | |
225 | ||
226 | - verbose: (optional) if set, the server thread will be launched | |
227 | in verbose mode | |
228 | ||
229 | - portScan: (optional) if set, we'll scan across a number of ports | |
230 | to find one which is avaiable | |
231 | ||
232 | """ | |
233 | if port == -1: | |
234 | port = self.defPort | |
235 | self.verbose=verbose | |
236 | self.Bind(EVT_EXTERNAL_EVENT,self.OnExternal) | |
237 | if hasattr(self,'OnClose'): | |
238 | self._origOnClose = self.OnClose | |
239 | self.Disconnect(-1,-1,wx.EVT_CLOSE_WINDOW) | |
240 | else: | |
241 | self._origOnClose = None | |
242 | self.OnClose = self.RPCOnClose | |
243 | self.Bind(wx.EVT_CLOSE,self.RPCOnClose) | |
244 | ||
245 | tClass = new.classobj('Handler%d'%(port),(Handler,),{}) | |
246 | tClass._app = self | |
247 | if portScan: | |
248 | self.rpcPort = -1 | |
249 | for i in xrange(self.nPortsToTry): | |
250 | try: | |
251 | xmlServ = SocketServer.TCPServer((host,port+i),tClass) | |
252 | except: | |
253 | pass | |
254 | else: | |
255 | self.rpcPort = port+i | |
256 | else: | |
257 | self.rpcPort = port | |
258 | try: | |
259 | xmlServ = SocketServer.TCPServer((host,port),tClass) | |
260 | except: | |
261 | self.rpcPort = -1 | |
262 | ||
263 | if self.rpcPort == -1: | |
264 | raise 'RPCMixinError','Cannot initialize server' | |
265 | self.servThread = _ServerThread(xmlServ,verbose=self.verbose) | |
266 | self.servThread.setName('XML-RPC Server') | |
267 | self.servThread.start() | |
268 | ||
269 | def RPCOnClose(self,event): | |
270 | """ callback for when the application is closed | |
271 | ||
272 | be sure to shutdown the server and the server thread before | |
273 | leaving | |
274 | ||
275 | """ | |
276 | # by setting the global stopEvent we inform the server thread | |
277 | # that it's time to shut down. | |
278 | stopEvent.set() | |
279 | if event is not None: | |
280 | # if we came in here from a user event (as opposed to an RPC event), | |
281 | # then we'll need to kick the server one last time in order | |
282 | # to get that thread to terminate. do so now | |
283 | s1 = xmlrpclib.Server('http://localhost:%d'%(self.rpcPort)) | |
284 | try: | |
285 | s1._rpcPing() | |
286 | except: | |
287 | pass | |
288 | ||
289 | if self._origOnClose is not None: | |
290 | self._origOnClose(event) | |
291 | ||
292 | def RPCQuit(self): | |
293 | """ shuts down everything, including the rpc server | |
294 | ||
295 | """ | |
296 | self.RPCOnClose(None) | |
297 | def OnExternal(self,event): | |
298 | """ this is the callback used to handle RPCs | |
299 | ||
300 | **Arguments** | |
301 | ||
302 | - event: an _ExternalEvent_ sent by the rpc server | |
303 | ||
304 | Exceptions are caught and returned in the global _rpcStatus | |
305 | structure. This allows the xmlrpc server to report the | |
306 | exception to the client without mucking up any of the delicate | |
307 | thread stuff. | |
308 | ||
309 | """ | |
310 | event.rpcStatusLock.acquire() | |
311 | doQuit = 0 | |
312 | try: | |
313 | methsplit = event.method.split('.') | |
314 | meth = self | |
315 | for piece in methsplit: | |
316 | meth = getattr(meth,piece) | |
317 | except AttributeError,msg: | |
318 | event.rpcStatus.result = 'No Such Method',msg | |
319 | event.rpcStatus.status = rpcEXCEPT | |
320 | else: | |
321 | try: | |
322 | res = apply(meth,event.args) | |
323 | except: | |
324 | import traceback | |
325 | if self.verbose: traceback.print_exc() | |
326 | event.rpcStatus.result = sys.exc_info()[:2] | |
327 | event.rpcStatus.status = rpcEXCEPT | |
328 | else: | |
329 | if res is None: | |
330 | # returning None across the xmlrpc interface is problematic | |
331 | event.rpcStatus.result = [] | |
332 | else: | |
333 | event.rpcStatus.result = res | |
334 | event.rpcStatus.status = rpcDONE | |
335 | ||
336 | event.rpcStatusLock.release() | |
337 | ||
338 | # broadcast (using the condition var) that we're done with the event | |
339 | event.rpcCondVar.acquire() | |
340 | event.rpcCondVar.notify() | |
341 | event.rpcCondVar.release() | |
342 | ||
343 | ||
344 | if __name__ == '__main__': | |
345 | import time | |
346 | if sys.platform == 'win32': | |
347 | port = 800 | |
348 | else: | |
349 | port = 8023 | |
350 | ||
351 | class rpcFrame(wx.Frame,rpcMixin): | |
352 | """A simple wxFrame with the rpcMixin functionality added | |
353 | """ | |
354 | def __init__(self,*args,**kwargs): | |
355 | """ rpcHost or rpcPort keyword arguments will be passed along to | |
356 | the xmlrpc server. | |
357 | """ | |
358 | mixinArgs = {} | |
359 | if kwargs.has_key('rpcHost'): | |
360 | mixinArgs['host'] = kwargs['rpcHost'] | |
361 | del kwargs['rpcHost'] | |
362 | if kwargs.has_key('rpcPort'): | |
363 | mixinArgs['port'] = kwargs['rpcPort'] | |
364 | del kwargs['rpcPort'] | |
365 | if kwargs.has_key('rpcPortScan'): | |
366 | mixinArgs['portScan'] = kwargs['rpcPortScan'] | |
367 | del kwargs['rpcPortScan'] | |
368 | ||
369 | apply(wx.Frame.__init__,(self,)+args,kwargs) | |
370 | apply(rpcMixin.__init__,(self,),mixinArgs) | |
371 | ||
372 | self.Bind(wx.EVT_CHAR,self.OnChar) | |
373 | ||
374 | def TestFunc(self,args): | |
375 | """a demo method""" | |
376 | return args | |
377 | ||
378 | def OnChar(self,event): | |
379 | key = event.GetKeyCode() | |
380 | if key == ord('q'): | |
381 | self.OnQuit(event) | |
382 | ||
383 | def OnQuit(self,event): | |
384 | self.OnClose(event) | |
385 | ||
386 | def OnClose(self,event): | |
387 | self.Destroy() | |
388 | ||
389 | ||
390 | ||
391 | class MyApp(wx.App): | |
392 | def OnInit(self): | |
393 | self.frame = rpcFrame(None, -1, "wxPython RPCDemo", wx.DefaultPosition, | |
394 | (300,300), rpcHost='localhost',rpcPort=port) | |
395 | self.frame.Show(True) | |
396 | return True | |
397 | ||
398 | ||
399 | def testcon(port): | |
400 | s1 = xmlrpclib.Server('http://localhost:%d'%(port)) | |
401 | s1.SetTitle('Munged') | |
402 | s1._rpcPing() | |
403 | if doQuit: | |
404 | s1.RPCQuit() | |
405 | ||
406 | doQuit = 1 | |
407 | if len(sys.argv)>1 and sys.argv[1] == '-q': | |
408 | doQuit = 0 | |
409 | nT = threading.activeCount() | |
410 | app = MyApp(0) | |
411 | activePort = app.frame.rpcPort | |
412 | t = threading.Thread(target=lambda x=activePort:testcon(x),verbose=0) | |
413 | t.start() | |
414 | ||
415 | app.MainLoop() | |
416 | # give the threads time to shut down | |
417 | if threading.activeCount() > nT: | |
418 | print 'waiting for all threads to terminate' | |
419 | while threading.activeCount() > nT: | |
420 | time.sleep(0.5) | |
421 | ||
422 |