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