]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/rpcMixin.py
added wxART_BUTTON
[wxWidgets.git] / wxPython / wx / lib / rpcMixin.py
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