wxWidgets/wxPython/wx/lib/rpcMixin.py

423 lines
12 KiB
Python
Raw Normal View History

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