]>
Commit | Line | Data |
---|---|---|
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 | #---------------------------------------------------------------------- | |
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 new | |
76 | import sys | |
77 | ||
78 | rpcPENDING = 0 | |
79 | rpcDONE = 1 | |
80 | rpcEXCEPT = 2 | |
81 | ||
82 | class RPCRequest: | |
83 | """A wrapper to use for handling requests and their responses""" | |
84 | status = rpcPENDING | |
85 | result = None | |
86 | ||
87 | # here's the ID for external events | |
88 | wxEVT_EXTERNAL_EVENT = 25015 | |
89 | class ExternalEvent(wxPyEvent): | |
90 | """The custom event class used to pass xmlrpc calls from | |
91 | the server thread into the GUI thread | |
92 | ||
93 | """ | |
94 | def __init__(self,method,args): | |
95 | wxPyEvent.__init__(self) | |
96 | self.SetEventType(wxEVT_EXTERNAL_EVENT) | |
97 | self.method = method | |
98 | self.args = args | |
99 | self.rpcStatus = RPCRequest() | |
100 | self.rpcStatusLock = threading.Lock() | |
101 | self.rpcCondVar = threading.Condition() | |
102 | ||
103 | def Destroy(self): | |
104 | self.method=None | |
105 | self.args=None | |
106 | self.rpcStatus = None | |
107 | self.rpcStatusLock = None | |
108 | self.rpcondVar = None | |
109 | ||
110 | def EVT_EXTERNAL_EVENT(win,func): | |
111 | win.Connect(-1,-1,wxEVT_EXTERNAL_EVENT,func) | |
112 | ||
113 | class Handler(xmlrpcserver.RequestHandler): | |
114 | """The handler class that the xmlrpcserver actually calls | |
115 | when a request comes in. | |
116 | ||
117 | """ | |
118 | def log_message(self,*args): | |
119 | """ causes the server to stop spewing messages every time a request comes in | |
120 | ||
121 | """ | |
122 | pass | |
123 | def call(self,method,params): | |
124 | """When an xmlrpc request comes in, this is the method that | |
125 | gets called. | |
126 | ||
127 | **Arguments** | |
128 | ||
129 | - method: name of the method to be called | |
130 | ||
131 | - params: arguments to that method | |
132 | ||
133 | """ | |
134 | if method == '_rpcPing': | |
135 | # we just acknowledge these without processing them | |
136 | return 'ack' | |
137 | ||
138 | # construct the event | |
139 | evt = ExternalEvent(method,params) | |
140 | ||
141 | # update the status variable | |
142 | evt.rpcStatusLock.acquire() | |
143 | evt.rpcStatus.status = rpcPENDING | |
144 | evt.rpcStatusLock.release() | |
145 | ||
146 | evt.rpcCondVar.acquire() | |
147 | # dispatch the event to the GUI | |
148 | wxPostEvent(self._app,evt) | |
149 | ||
150 | # wait for the GUI to finish | |
151 | while evt.rpcStatus.status == rpcPENDING: | |
152 | evt.rpcCondVar.wait() | |
153 | evt.rpcCondVar.release() | |
154 | evt.rpcStatusLock.acquire() | |
155 | if evt.rpcStatus.status == rpcEXCEPT: | |
156 | # The GUI threw an exception, release the status lock | |
157 | # and re-raise the exception | |
158 | evt.rpcStatusLock.release() | |
159 | raise evt.rpcStatus.result[0],evt.rpcStatus.result[1] | |
160 | else: | |
161 | # everything went through without problems | |
162 | s = evt.rpcStatus.result | |
163 | ||
164 | evt.rpcStatusLock.release() | |
165 | evt.Destroy() | |
166 | self._app = None | |
167 | return s | |
168 | ||
169 | # this global Event is used to let the server thread | |
170 | # know when it should quit | |
171 | stopEvent = threading.Event() | |
172 | stopEvent.clear() | |
173 | ||
174 | class _ServerThread(threading.Thread): | |
175 | """ this is the Thread class which actually runs the server | |
176 | ||
177 | """ | |
178 | def __init__(self,server,verbose=0): | |
179 | self._xmlServ = server | |
180 | threading.Thread.__init__(self,verbose=verbose) | |
181 | ||
182 | def stop(self): | |
183 | stopEvent.set() | |
184 | ||
185 | def shouldStop(self): | |
186 | return stopEvent.isSet() | |
187 | ||
188 | def run(self): | |
189 | while not self.shouldStop(): | |
190 | self._xmlServ.handle_request() | |
191 | self._xmlServ = None | |
192 | ||
193 | class rpcMixin: | |
194 | """A mixin class to provide xmlrpc server functionality to wxPython | |
195 | frames/windows | |
196 | ||
197 | If you want to customize this, probably the best idea is to | |
198 | override the OnExternal method, which is what's invoked when an | |
199 | RPC is handled. | |
200 | ||
201 | """ | |
202 | ||
203 | # we'll try a range of ports for the server, this is the size of the | |
204 | # range to be scanned | |
205 | nPortsToTry=20 | |
206 | if sys.platform == 'win32': | |
207 | defPort = 800 | |
208 | else: | |
209 | defPort = 8023 | |
210 | ||
211 | def __init__(self,host='',port=-1,verbose=0,portScan=1): | |
212 | """Constructor | |
213 | ||
214 | **Arguments** | |
215 | ||
216 | - host: (optional) the hostname for the server | |
217 | ||
218 | - port: (optional) the port the server will use | |
219 | ||
220 | - verbose: (optional) if set, the server thread will be launched | |
221 | in verbose mode | |
222 | ||
223 | - portScan: (optional) if set, we'll scan across a number of ports | |
224 | to find one which is avaiable | |
225 | ||
226 | """ | |
227 | if port == -1: | |
228 | port = self.defPort | |
229 | self.verbose=verbose | |
230 | EVT_EXTERNAL_EVENT(self,self.OnExternal) | |
231 | if hasattr(self,'OnClose'): | |
232 | self._origOnClose = self.OnClose | |
233 | self.Disconnect(-1,-1,wxEVT_CLOSE_WINDOW) | |
234 | else: | |
235 | self._origOnClose = None | |
236 | self.OnClose = self.RPCOnClose | |
237 | EVT_CLOSE(self,self.RPCOnClose) | |
238 | ||
239 | tClass = new.classobj('Handler%d'%(port),(Handler,),{}) | |
240 | tClass._app = self | |
241 | if portScan: | |
242 | self.rpcPort = -1 | |
243 | for i in xrange(self.nPortsToTry): | |
244 | try: | |
245 | xmlServ = SocketServer.TCPServer((host,port+i),tClass) | |
246 | except: | |
247 | pass | |
248 | else: | |
249 | self.rpcPort = port+i | |
250 | else: | |
251 | self.rpcPort = port | |
252 | try: | |
253 | xmlServ = SocketServer.TCPServer((host,port),tClass) | |
254 | except: | |
255 | self.rpcPort = -1 | |
256 | ||
257 | if self.rpcPort == -1: | |
258 | raise 'RPCMixinError','Cannot initialize server' | |
259 | self.servThread = _ServerThread(xmlServ,verbose=self.verbose) | |
260 | self.servThread.setName('XML-RPC Server') | |
261 | self.servThread.start() | |
262 | ||
263 | def RPCOnClose(self,event): | |
264 | """ callback for when the application is closed | |
265 | ||
266 | be sure to shutdown the server and the server thread before | |
267 | leaving | |
268 | ||
269 | """ | |
270 | # by setting the global stopEvent we inform the server thread | |
271 | # that it's time to shut down. | |
272 | stopEvent.set() | |
273 | if event is not None: | |
274 | # if we came in here from a user event (as opposed to an RPC event), | |
275 | # then we'll need to kick the server one last time in order | |
276 | # to get that thread to terminate. do so now | |
277 | s1 = xmlrpclib.Server('http://localhost:%d'%(self.rpcPort)) | |
278 | try: | |
279 | s1._rpcPing() | |
280 | except: | |
281 | pass | |
282 | ||
283 | if self._origOnClose is not None: | |
284 | self._origOnClose(event) | |
285 | ||
286 | def RPCQuit(self): | |
287 | """ shuts down everything, including the rpc server | |
288 | ||
289 | """ | |
290 | self.RPCOnClose(None) | |
291 | def OnExternal(self,event): | |
292 | """ this is the callback used to handle RPCs | |
293 | ||
294 | **Arguments** | |
295 | ||
296 | - event: an _ExternalEvent_ sent by the rpc server | |
297 | ||
298 | Exceptions are caught and returned in the global _rpcStatus | |
299 | structure. This allows the xmlrpc server to report the | |
300 | exception to the client without mucking up any of the delicate | |
301 | thread stuff. | |
302 | ||
303 | """ | |
304 | event.rpcStatusLock.acquire() | |
305 | doQuit = 0 | |
306 | try: | |
307 | methsplit = event.method.split('.') | |
308 | meth = self | |
309 | for piece in methsplit: | |
310 | meth = getattr(meth,piece) | |
311 | except AttributeError,msg: | |
312 | event.rpcStatus.result = 'No Such Method',msg | |
313 | event.rpcStatus.status = rpcEXCEPT | |
314 | else: | |
315 | try: | |
316 | res = apply(meth,event.args) | |
317 | except: | |
318 | import traceback | |
319 | if self.verbose: traceback.print_exc() | |
320 | event.rpcStatus.result = sys.exc_info()[:2] | |
321 | event.rpcStatus.status = rpcEXCEPT | |
322 | else: | |
323 | if res is None: | |
324 | # returning None across the xmlrpc interface is problematic | |
325 | event.rpcStatus.result = [] | |
326 | else: | |
327 | event.rpcStatus.result = res | |
328 | event.rpcStatus.status = rpcDONE | |
329 | ||
330 | event.rpcStatusLock.release() | |
331 | ||
332 | # broadcast (using the condition var) that we're done with the event | |
333 | event.rpcCondVar.acquire() | |
334 | event.rpcCondVar.notify() | |
335 | event.rpcCondVar.release() | |
336 | ||
337 | ||
338 | if __name__ == '__main__': | |
339 | import time | |
340 | if sys.platform == 'win32': | |
341 | port = 800 | |
342 | else: | |
343 | port = 8023 | |
344 | ||
345 | class rpcFrame(wxFrame,rpcMixin): | |
346 | """A simple wxFrame with the rpcMixin functionality added | |
347 | """ | |
348 | def __init__(self,*args,**kwargs): | |
349 | """ rpcHost or rpcPort keyword arguments will be passed along to | |
350 | the xmlrpc server. | |
351 | """ | |
352 | mixinArgs = {} | |
353 | if kwargs.has_key('rpcHost'): | |
354 | mixinArgs['host'] = kwargs['rpcHost'] | |
355 | del kwargs['rpcHost'] | |
356 | if kwargs.has_key('rpcPort'): | |
357 | mixinArgs['port'] = kwargs['rpcPort'] | |
358 | del kwargs['rpcPort'] | |
359 | if kwargs.has_key('rpcPortScan'): | |
360 | mixinArgs['portScan'] = kwargs['rpcPortScan'] | |
361 | del kwargs['rpcPortScan'] | |
362 | ||
363 | apply(wxFrame.__init__,(self,)+args,kwargs) | |
364 | apply(rpcMixin.__init__,(self,),mixinArgs) | |
365 | ||
366 | EVT_CHAR(self,self.OnChar) | |
367 | ||
368 | def TestFunc(self,args): | |
369 | """a demo method""" | |
370 | return args | |
371 | ||
372 | def OnChar(self,event): | |
373 | key = event.GetKeyCode() | |
374 | if key == ord('q'): | |
375 | self.OnQuit(event) | |
376 | ||
377 | def OnQuit(self,event): | |
378 | self.OnClose(event) | |
379 | ||
380 | def OnClose(self,event): | |
381 | self.Destroy() | |
382 | ||
383 | ||
384 | ||
385 | class MyApp(wxApp): | |
386 | def OnInit(self): | |
387 | self.frame = rpcFrame(NULL, -1, "wxPython RPCDemo", wxDefaultPosition, | |
388 | wxSize(300,300), | |
389 | rpcHost='localhost',rpcPort=port) | |
390 | self.frame.Show(True) | |
391 | return True | |
392 | ||
393 | ||
394 | def testcon(port): | |
395 | s1 = xmlrpclib.Server('http://localhost:%d'%(port)) | |
396 | s1.SetTitle('Munged') | |
397 | s1._rpcPing() | |
398 | if doQuit: | |
399 | s1.RPCQuit() | |
400 | ||
401 | doQuit = 1 | |
402 | if len(sys.argv)>1 and sys.argv[1] == '-q': | |
403 | doQuit = 0 | |
404 | nT = threading.activeCount() | |
405 | app = MyApp(0) | |
406 | activePort = app.frame.rpcPort | |
407 | t = threading.Thread(target=lambda x=activePort:testcon(x),verbose=0) | |
408 | t.start() | |
409 | ||
410 | app.MainLoop() | |
411 | # give the threads time to shut down | |
412 | if threading.activeCount() > nT: | |
413 | print 'waiting for all threads to terminate' | |
414 | while threading.activeCount() > nT: | |
415 | time.sleep(0.5) | |
1fded56b | 416 | |
1fded56b | 417 |