b881fc787d
Jeff Grimmett with some tweaks and changes from Robin git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@24889 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
423 lines
12 KiB
Python
423 lines
12 KiB
Python
#
|
|
# 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)
|
|
|
|
|