# # This was modified from rpcMixin.py distributed with wxPython # #---------------------------------------------------------------------- # Name: rpcMixin # Version: 0.2.0 # Purpose: provides xmlrpc server functionality for wxPython # applications via a mixin class # # Requires: (1) Python with threading enabled. # (2) xmlrpclib from PythonWare # (http://www.pythonware.com/products/xmlrpc/) # the code was developed and tested using version 0.9.8 # # Author: greg Landrum (Landrum@RationalDiscovery.com) # # Copyright: (c) 2000, 2001 by Greg Landrum and Rational Discovery LLC # Licence: wxWindows license #---------------------------------------------------------------------- # 12/11/2003 - Jeff Grimmett (grimmtooth@softhome.net) # # o 2.5 compatability update. # o xmlrpcserver not available. # """provides xmlrpc server functionality for wxPython applications via a mixin class **Some Notes:** 1) The xmlrpc server runs in a separate thread from the main GUI application, communication between the two threads using a custom event (see the Threads demo in the wxPython docs for more info). 2) Neither the server nor the client are particularly smart about checking method names. So it's easy to shoot yourself in the foot by calling improper methods. It would be pretty easy to add either a list of allowed methods or a list of forbidden methods. 3) Authentication of xmlrpc clients is *not* performed. I think it would be pretty easy to do this in a hacky way, but I haven't done it yet. 4) See the bottom of this file for an example of using the class. **Obligatory disclaimer:** This is my first crack at both using xmlrpc and multi-threaded programming, so there could be huge horrible bugs or design flaws. If you see one, I'd love to hear about them. """ """ ChangeLog 23 May 2001: Version bumped to 0.2.0 Numerous code and design changes 21 Mar. 2001: Version bumped to 0.1.4 Updated rpcMixin.OnExternal to support methods with further references (i.e. now you can do rpcClient.foo.bar() and have it work) This probably ain't super legal in xmlrpc land, but it works just fine here and we need it. 6 Mar. 2001: Version bumped to 0.1.3 Documentation changes to make this compatible with happydoc 21 Jan. 2001: Version bumped to 0.1.2 OnExternal() method in the mixin class now uses getattr() to check if a desired method is present. It should have been done this way in the first place. 14 Dec. 2000: Version bumped to 0.1.1 rearranged locking code and made other changes so that multiple servers in one application are possible. """ import new import SocketServer import sys import threading import xmlrpclib import xmlrpcserver import wx rpcPENDING = 0 rpcDONE = 1 rpcEXCEPT = 2 class RPCRequest: """A wrapper to use for handling requests and their responses""" status = rpcPENDING result = None # here's the ID for external events wxEVT_EXTERNAL_EVENT = wx.NewEventType() EVT_EXTERNAL_EVENT = wx.PyEventBinder(wxEVT_EXTERNAL_EVENT, 0) class ExternalEvent(wx.PyEvent): """The custom event class used to pass xmlrpc calls from the server thread into the GUI thread """ def __init__(self,method,args): wx.PyEvent.__init__(self) self.SetEventType(wxEVT_EXTERNAL_EVENT) self.method = method self.args = args self.rpcStatus = RPCRequest() self.rpcStatusLock = threading.Lock() self.rpcCondVar = threading.Condition() def Destroy(self): self.method=None self.args=None self.rpcStatus = None self.rpcStatusLock = None self.rpcondVar = None class Handler(xmlrpcserver.RequestHandler): """The handler class that the xmlrpcserver actually calls when a request comes in. """ def log_message(self,*args): """ causes the server to stop spewing messages every time a request comes in """ pass def call(self,method,params): """When an xmlrpc request comes in, this is the method that gets called. **Arguments** - method: name of the method to be called - params: arguments to that method """ if method == '_rpcPing': # we just acknowledge these without processing them return 'ack' # construct the event evt = ExternalEvent(method,params) # update the status variable evt.rpcStatusLock.acquire() evt.rpcStatus.status = rpcPENDING evt.rpcStatusLock.release() evt.rpcCondVar.acquire() # dispatch the event to the GUI wx.PostEvent(self._app,evt) # wait for the GUI to finish while evt.rpcStatus.status == rpcPENDING: evt.rpcCondVar.wait() evt.rpcCondVar.release() evt.rpcStatusLock.acquire() if evt.rpcStatus.status == rpcEXCEPT: # The GUI threw an exception, release the status lock # and re-raise the exception evt.rpcStatusLock.release() raise evt.rpcStatus.result[0],evt.rpcStatus.result[1] else: # everything went through without problems s = evt.rpcStatus.result evt.rpcStatusLock.release() evt.Destroy() self._app = None return s # this global Event is used to let the server thread # know when it should quit stopEvent = threading.Event() stopEvent.clear() class _ServerThread(threading.Thread): """ this is the Thread class which actually runs the server """ def __init__(self,server,verbose=0): self._xmlServ = server threading.Thread.__init__(self,verbose=verbose) def stop(self): stopEvent.set() def shouldStop(self): return stopEvent.isSet() def run(self): while not self.shouldStop(): self._xmlServ.handle_request() self._xmlServ = None class rpcMixin: """A mixin class to provide xmlrpc server functionality to wxPython frames/windows If you want to customize this, probably the best idea is to override the OnExternal method, which is what's invoked when an RPC is handled. """ # we'll try a range of ports for the server, this is the size of the # range to be scanned nPortsToTry=20 if sys.platform == 'win32': defPort = 800 else: defPort = 8023 def __init__(self,host='',port=-1,verbose=0,portScan=1): """Constructor **Arguments** - host: (optional) the hostname for the server - port: (optional) the port the server will use - verbose: (optional) if set, the server thread will be launched in verbose mode - portScan: (optional) if set, we'll scan across a number of ports to find one which is avaiable """ if port == -1: port = self.defPort self.verbose=verbose self.Bind(EVT_EXTERNAL_EVENT,self.OnExternal) if hasattr(self,'OnClose'): self._origOnClose = self.OnClose self.Disconnect(-1,-1,wx.EVT_CLOSE_WINDOW) else: self._origOnClose = None self.OnClose = self.RPCOnClose self.Bind(wx.EVT_CLOSE,self.RPCOnClose) tClass = new.classobj('Handler%d'%(port),(Handler,),{}) tClass._app = self if portScan: self.rpcPort = -1 for i in xrange(self.nPortsToTry): try: xmlServ = SocketServer.TCPServer((host,port+i),tClass) except: pass else: self.rpcPort = port+i else: self.rpcPort = port try: xmlServ = SocketServer.TCPServer((host,port),tClass) except: self.rpcPort = -1 if self.rpcPort == -1: raise 'RPCMixinError','Cannot initialize server' self.servThread = _ServerThread(xmlServ,verbose=self.verbose) self.servThread.setName('XML-RPC Server') self.servThread.start() def RPCOnClose(self,event): """ callback for when the application is closed be sure to shutdown the server and the server thread before leaving """ # by setting the global stopEvent we inform the server thread # that it's time to shut down. stopEvent.set() if event is not None: # if we came in here from a user event (as opposed to an RPC event), # then we'll need to kick the server one last time in order # to get that thread to terminate. do so now s1 = xmlrpclib.Server('http://localhost:%d'%(self.rpcPort)) try: s1._rpcPing() except: pass if self._origOnClose is not None: self._origOnClose(event) def RPCQuit(self): """ shuts down everything, including the rpc server """ self.RPCOnClose(None) def OnExternal(self,event): """ this is the callback used to handle RPCs **Arguments** - event: an _ExternalEvent_ sent by the rpc server Exceptions are caught and returned in the global _rpcStatus structure. This allows the xmlrpc server to report the exception to the client without mucking up any of the delicate thread stuff. """ event.rpcStatusLock.acquire() doQuit = 0 try: methsplit = event.method.split('.') meth = self for piece in methsplit: meth = getattr(meth,piece) except AttributeError,msg: event.rpcStatus.result = 'No Such Method',msg event.rpcStatus.status = rpcEXCEPT else: try: res = apply(meth,event.args) except: import traceback if self.verbose: traceback.print_exc() event.rpcStatus.result = sys.exc_info()[:2] event.rpcStatus.status = rpcEXCEPT else: if res is None: # returning None across the xmlrpc interface is problematic event.rpcStatus.result = [] else: event.rpcStatus.result = res event.rpcStatus.status = rpcDONE event.rpcStatusLock.release() # broadcast (using the condition var) that we're done with the event event.rpcCondVar.acquire() event.rpcCondVar.notify() event.rpcCondVar.release() if __name__ == '__main__': import time if sys.platform == 'win32': port = 800 else: port = 8023 class rpcFrame(wx.Frame,rpcMixin): """A simple wxFrame with the rpcMixin functionality added """ def __init__(self,*args,**kwargs): """ rpcHost or rpcPort keyword arguments will be passed along to the xmlrpc server. """ mixinArgs = {} if kwargs.has_key('rpcHost'): mixinArgs['host'] = kwargs['rpcHost'] del kwargs['rpcHost'] if kwargs.has_key('rpcPort'): mixinArgs['port'] = kwargs['rpcPort'] del kwargs['rpcPort'] if kwargs.has_key('rpcPortScan'): mixinArgs['portScan'] = kwargs['rpcPortScan'] del kwargs['rpcPortScan'] apply(wx.Frame.__init__,(self,)+args,kwargs) apply(rpcMixin.__init__,(self,),mixinArgs) self.Bind(wx.EVT_CHAR,self.OnChar) def TestFunc(self,args): """a demo method""" return args def OnChar(self,event): key = event.GetKeyCode() if key == ord('q'): self.OnQuit(event) def OnQuit(self,event): self.OnClose(event) def OnClose(self,event): self.Destroy() class MyApp(wx.App): def OnInit(self): self.frame = rpcFrame(None, -1, "wxPython RPCDemo", wx.DefaultPosition, (300,300), rpcHost='localhost',rpcPort=port) self.frame.Show(True) return True def testcon(port): s1 = xmlrpclib.Server('http://localhost:%d'%(port)) s1.SetTitle('Munged') s1._rpcPing() if doQuit: s1.RPCQuit() doQuit = 1 if len(sys.argv)>1 and sys.argv[1] == '-q': doQuit = 0 nT = threading.activeCount() app = MyApp(0) activePort = app.frame.rpcPort t = threading.Thread(target=lambda x=activePort:testcon(x),verbose=0) t.start() app.MainLoop() # give the threads time to shut down if threading.activeCount() > nT: print 'waiting for all threads to terminate' while threading.activeCount() > nT: time.sleep(0.5)