| 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 | |