]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/rpcMixin.py
updates from Andrea, plus some fixes
[wxWidgets.git] / wxPython / wx / lib / rpcMixin.py
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