]> git.saurik.com Git - wxWidgets.git/blame - wxPython/wx/lib/rpcMixin.py
Patches from Andrea
[wxWidgets.git] / wxPython / wx / lib / rpcMixin.py
CommitLineData
d14a1e28
RD
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#----------------------------------------------------------------------
b881fc78
RD
20# 12/11/2003 - Jeff Grimmett (grimmtooth@softhome.net)
21#
22# o 2.5 compatability update.
23# o xmlrpcserver not available.
24#
d14a1e28
RD
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
5423 May 2001: Version bumped to 0.2.0
55 Numerous code and design changes
56
5721 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
636 Mar. 2001: Version bumped to 0.1.3
64 Documentation changes to make this compatible with happydoc
65
6621 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.
7014 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
b881fc78
RD
76import new
77import SocketServer
78import sys
79import threading
80import xmlrpclib
81import xmlrpcserver
82
83import wx
d14a1e28
RD
84
85rpcPENDING = 0
86rpcDONE = 1
87rpcEXCEPT = 2
88
89class 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
b881fc78
RD
95wxEVT_EXTERNAL_EVENT = wx.NewEventType()
96EVT_EXTERNAL_EVENT = wx.PyEventBinder(wxEVT_EXTERNAL_EVENT, 0)
97
98class ExternalEvent(wx.PyEvent):
d14a1e28
RD
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):
b881fc78 104 wx.PyEvent.__init__(self)
d14a1e28
RD
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
d14a1e28
RD
119class 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
b881fc78 154 wx.PostEvent(self._app,evt)
d14a1e28
RD
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
177stopEvent = threading.Event()
178stopEvent.clear()
179
180class _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
199class 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
b881fc78 236 self.Bind(EVT_EXTERNAL_EVENT,self.OnExternal)
d14a1e28
RD
237 if hasattr(self,'OnClose'):
238 self._origOnClose = self.OnClose
b881fc78 239 self.Disconnect(-1,-1,wx.EVT_CLOSE_WINDOW)
d14a1e28
RD
240 else:
241 self._origOnClose = None
242 self.OnClose = self.RPCOnClose
b881fc78 243 self.Bind(wx.EVT_CLOSE,self.RPCOnClose)
d14a1e28
RD
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
344if __name__ == '__main__':
345 import time
346 if sys.platform == 'win32':
347 port = 800
348 else:
349 port = 8023
350
b881fc78 351 class rpcFrame(wx.Frame,rpcMixin):
d14a1e28
RD
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
b881fc78 369 apply(wx.Frame.__init__,(self,)+args,kwargs)
d14a1e28
RD
370 apply(rpcMixin.__init__,(self,),mixinArgs)
371
b881fc78 372 self.Bind(wx.EVT_CHAR,self.OnChar)
d14a1e28
RD
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
b881fc78 391 class MyApp(wx.App):
d14a1e28 392 def OnInit(self):
b881fc78
RD
393 self.frame = rpcFrame(None, -1, "wxPython RPCDemo", wx.DefaultPosition,
394 (300,300), rpcHost='localhost',rpcPort=port)
d14a1e28
RD
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)
1fded56b 421
1fded56b 422