root / pykoticon / trunk / bin / pykoticon @ 178

Revision 177, 20.2 kB (checked in by jerome, 17 years ago)

Now accepts several concurrent requests.

  • Property svn:keywords set to Id
Line 
1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
4"""PyKotIcon is a generic, networked, cross-platform dialog box manager."""
5
6# PyKotIcon - Client side helper for PyKota and other applications
7#
8# (c) 2003, 2004, 2005, 2006, 2007 Jerome Alet <alet@librelogiciel.com>
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
22#
23#
24
25__version__ = "1.03"
26__author__ = "Jerome Alet"
27__author_email__ = "alet@librelogiciel.com"
28__license__ = "GNU GPL"
29__url__ = "http://www.pykota.com/software/pykoticon"
30__revision__ = "$Id$"
31
32import sys
33import os
34import time
35import urllib
36import urllib2
37import locale
38import gettext
39import socket
40import threading
41import xmlrpclib
42import SimpleXMLRPCServer
43import SocketServer
44
45try :
46    import optparse
47except ImportError :   
48    sys.stderr.write("You need Python v2.3 or higher for PyKotIcon to work.\nAborted.\n")
49    sys.exit(-1)
50
51if sys.platform == "win32" :
52    isWindows = True
53    try :
54        import win32api
55    except ImportError :   
56        raise ImportError, "Mark Hammond's Win32 Extensions are missing. Please install them."
57    else :   
58        iconsdir = os.path.split(sys.argv[0])[0]
59else :       
60    isWindows = False
61    iconsdir = "/usr/share/pykoticon"   # TODO : change this
62    import pwd
63   
64try :   
65    import wx
66    hasWxPython = True
67except ImportError :   
68    hasWxPython = False
69    raise ImportError, "wxPython is missing. Please install it."
70   
71aboutbox = """PyKotIcon v%(__version__)s (c) 2003-2006 %(__author__)s - %(__author_email__)s
72
73PyKotIcon is generic, networked, cross-platform dialog box manager.
74
75It is often used as a client side companion for PyKota, but it
76can be used from other applications if you want.
77
78This program is free software; you can redistribute it and/or modify
79it under the terms of the GNU General Public License as published by
80the Free Software Foundation; either version 2 of the License, or
81(at your option) any later version.
82
83This program is distributed in the hope that it will be useful,
84but WITHOUT ANY WARRANTY; without even the implied warranty of
85MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
86GNU General Public License for more details.
87
88You should have received a copy of the GNU General Public License
89along with this program; if not, write to the Free Software
90Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA."""
91
92
93class ThreadedXMLRPCServer(SocketServer.ThreadingTCPServer, SimpleXMLRPCServer.SimpleXMLRPCDispatcher) :
94    """Base class to have a threaded XMLRPC Server."""
95    def __init__(self, addr, requestHandler=SimpleXMLRPCServer.SimpleXMLRPCRequestHandler, logRequests=False) :
96        """Imitate what is in SimpleXMLRPCServer.py but with a threaded TCP server instead."""
97        self.logRequests = logRequests
98        SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self)
99        SocketServer.ThreadingTCPServer.__init__(self, addr, requestHandler)
100       
101class MyXMLRPCServer(ThreadedXMLRPCServer) :
102    """My own server class."""
103    allow_reuse_address = True
104    def __init__(self, frame, options, arguments) :
105        ThreadedXMLRPCServer.__init__(self, \
106                                      ('0.0.0.0', options.port), \
107                                      SimpleXMLRPCServer.SimpleXMLRPCRequestHandler, \
108                                      options.debug)
109        self.frame = frame
110        self.debug = options.debug
111        self.cacheduration = options.cache
112        self.cache = {}
113        self.printServers = [ socket.gethostbyname(arg) for arg in arguments ]
114        if "127.0.0.1" not in self.printServers :
115            self.printServers.append("127.0.0.1") # to allow clean shutdown
116        loop = threading.Thread(target=self.mainloop)
117        loop.start()
118       
119    def logDebug(self, message) :   
120        """Logs a debug message if debug mode is active."""
121        if self.debug :
122            sys.stderr.write("%s\n" % message)
123           
124    def getAnswerFromCache(self, key) :
125        """Tries to extract a value from the cache and returns it if still valid."""
126        cacheentry = self.cache.get(key)
127        if cacheentry is not None :
128            (birth, value) = cacheentry 
129            if (time.time() - birth) < self.cacheduration :
130                self.logDebug("Cache hit for %s" % str(key))
131                return value # NB : we don't extend the life of this entry
132            else :   
133                self.logDebug("Cache expired for %s" % str(key))
134        else :       
135            self.logDebug("Cache miss for %s" % str(key))
136        return None
137       
138    def storeAnswerInCache(self, key, value) :   
139        """Stores an entry in the cache."""
140        self.cache[key] = (time.time(), value)
141        self.logDebug("Cache store for %s" % str(key))
142       
143    def export_askDatas(self, labels, varnames, varvalues) :
144        """Asks some textual datas defined by a list of labels, a list of variables' names and a list of variables values in a mapping."""
145        values = {}
146        for (key, value) in varvalues.items() :
147            values[key] = self.frame.UTF8ToUserCharset(value.data)
148        cachekey = tuple(values.items()) 
149        retcode = self.getAnswerFromCache(cachekey)
150        if (retcode is None) or (not retcode["isValid"]) :
151            wx.CallAfter(self.frame.askDatas, [ self.frame.UTF8ToUserCharset(label.data) for label in labels ], \
152                                              varnames, \
153                                              values)
154            # ugly, isn't it ?
155            while self.frame.dialogAnswer is None :
156                time.sleep(0.1)
157            retcode = self.frame.dialogAnswer   
158            for (key, value) in retcode.items() :
159                if key != "isValid" :
160                    retcode[key] = xmlrpclib.Binary(self.frame.userCharsetToUTF8(value))
161            self.frame.dialogAnswer = None # prepare for next call, just in case
162            self.storeAnswerInCache(cachekey, retcode)
163        return retcode
164       
165    def export_quitApplication(self) :   
166        """Makes the application quit."""
167        self.frame.quitEvent.set()
168        wx.CallAfter(self.frame.OnClose, None)
169        return True
170       
171    def export_showDialog(self, message, yesno) :
172        """Opens a notification or confirmation dialog."""
173        wx.CallAfter(self.frame.showDialog, self.frame.UTF8ToUserCharset(message.data), yesno)
174        # ugly, isn't it ?
175        while self.frame.dialogAnswer is None :
176            time.sleep(0.1)
177        retcode = self.frame.dialogAnswer   
178        self.frame.dialogAnswer = None # prepare for next call, just in case
179        return retcode
180       
181    def export_nop(self) :   
182        """Does nothing, but allows a clean shutdown from the frame itself."""
183        return True
184       
185    def _dispatch(self, method, params) :   
186        """Ensure that only export_* methods are available."""
187        return getattr(self, "export_%s" % method)(*params)
188       
189    def handle_error(self, request, client_address) :   
190        """Doesn't display an ugly traceback in case an error occurs."""
191        self.logDebug("An exception occured while handling an incoming request from %s:%s" % (client_address[0], client_address[1]))
192       
193    def verify_request(self, request, client_address) :
194        """Ensures that requests which don't come from the print server are rejected."""
195        (client, port) = client_address
196        if client in self.printServers :
197            self.logDebug("%s accepted." % client)
198            return True
199        else :
200            # Unauthorized access !
201            self.logDebug("%s rejected." % client)
202            return False
203       
204    def mainloop(self) :
205        """XML-RPC Server's main loop."""
206        self.register_function(self.export_askDatas)
207        self.register_function(self.export_showDialog)
208        self.register_function(self.export_quitApplication)
209        self.register_function(self.export_nop)
210        while not self.frame.quitEvent.isSet() :
211            self.handle_request()
212        self.server_close()   
213        sys.exit(0)
214   
215   
216class GenericInputDialog(wx.Dialog) :
217    """Generic input dialog box."""
218    def __init__(self, parent, id, labels, varnames, varvalues):
219        wx.Dialog.__init__(self, parent, id, \
220               _("PyKotIcon data input"), \
221               style = wx.CAPTION \
222                     | wx.THICK_FRAME \
223                     | wx.STAY_ON_TOP \
224                     | wx.DIALOG_MODAL)
225
226        self.variables = []
227        vsizer = wx.BoxSizer(wx.VERTICAL)
228        for i in range(len(varnames)) :
229            varname = varnames[i]
230            try :
231                label = labels[i]
232            except IndexError :   
233                label = ""
234            labelid = wx.NewId()   
235            varid = wx.NewId()
236            labelst = wx.StaticText(self, labelid, label)
237            if varname.lower().find("password") != -1 :
238                variable = wx.TextCtrl(self, varid, varvalues.get(varname, ""), style=wx.TE_PASSWORD)
239            else :
240                variable = wx.TextCtrl(self, varid, varvalues.get(varname, ""))
241            self.variables.append(variable)   
242            hsizer = wx.BoxSizer(wx.HORIZONTAL)
243            hsizer.Add(labelst, 0, wx.ALIGN_CENTER | wx.ALIGN_RIGHT | wx.ALL, 5)
244            hsizer.Add(variable, 0, wx.ALIGN_CENTER | wx.ALIGN_LEFT | wx.ALL, 5)
245            vsizer.Add(hsizer, 0, wx.ALIGN_CENTER | wx.ALL, 5)
246           
247        okbutton = wx.Button(self, wx.ID_OK, "OK")   
248        vsizer.Add(okbutton, 0, wx.ALIGN_CENTER | wx.ALL, 5)
249        if self.variables :
250            self.variables[0].SetFocus()
251        self.SetAutoLayout(True)
252        self.SetSizerAndFit(vsizer)
253        self.Layout()
254       
255       
256class PyKotIcon(wx.Frame):
257    """Main class."""
258    def __init__(self, parent, id):
259        self.dialogAnswer = None
260        wx.Frame.__init__(self, parent, id, \
261               _("PyKotIcon info for %s") % self.getCurrentUserName(), \
262               size = (0, 0), \
263               style = wx.FRAME_NO_TASKBAR | wx.NO_FULL_REPAINT_ON_RESIZE)
264                     
265    def getCurrentUserName(self) :
266        """Retrieves the current user's name."""
267        if isWindows :
268            return win32api.GetUserName()
269        else :   
270            try :
271                return pwd.getpwuid(os.geteuid())[0]
272            except :
273                return "** Unknown **"
274           
275    def OnIconify(self, event) :
276        """Iconify/De-iconify the application."""
277        if not self.IsIconized() :
278            self.Iconize(True)
279        self.Hide()
280
281    def OnTaskBarActivate(self, event) :
282        """Show the application if it is minimized."""
283        if self.IsIconized() :
284            self.Iconize(False)
285        if not self.IsShown() :
286            self.Show(True)
287        self.Raise()
288
289    def OnClose(self, event) :
290        """Cleanly quit the application."""
291        if (event is None) \
292           or self.options.allowquit :
293            self.closeServer()
294            self.menu.Destroy()
295            self.tbicon.Destroy()
296            self.Destroy()
297            return True
298        else :   
299            # self.quitIsForbidden()
300            return False
301
302    def OnTaskBarMenu(self, event) :
303        """Open the taskbar menu."""
304        self.tbicon.PopupMenu(self.menu)
305
306    def OnTaskBarClose(self, event) :
307        """React to close from the taskbar."""
308        self.Close()
309           
310    def quitIsForbidden(self) :       
311        """Displays a message indicating that quitting the application is not allowed."""
312        message = _("Sorry, this was forbidden by your system administrator.")
313        caption = _("Information")
314        style = wx.OK | wx.ICON_INFORMATION | wx.STAY_ON_TOP
315        dialog = wx.MessageDialog(self, message, caption, style)
316        dialog.ShowModal()
317        dialog.Destroy()
318       
319    def OnAbout(self, event) :   
320        """Displays the about box."""
321        dialog = wx.MessageDialog(self, aboutbox % globals(), \
322                                        _("About"), \
323                                        wx.OK | wx.ICON_INFORMATION)
324        dialog.Raise()                                       
325        dialog.ShowModal()
326        dialog.Destroy()
327       
328    def showDialog(self, message, yesno) :
329        """Opens a notification dialog."""
330        self.dialogAnswer = None
331        if yesno :
332            caption = _("Confirmation")
333            style = wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION
334        else :
335            caption = _("Information")
336            style = wx.OK | wx.ICON_INFORMATION
337        style |= wx.STAY_ON_TOP   
338        dialog = wx.MessageDialog(self, message, caption, style)
339        dialog.Raise()
340        self.dialogAnswer = ((dialog.ShowModal() == wx.ID_NO) and "CANCEL") or "OK"
341        dialog.Destroy()
342       
343    def askDatas(self, labels, varnames, varvalues) :
344        """Opens a dialog box asking for data entry."""
345        self.dialogAnswer = None
346        dialog = GenericInputDialog(self, wx.ID_ANY, labels, varnames, varvalues)
347        dialog.Raise()
348        retvalues = {}
349        if dialog.ShowModal() == wx.ID_OK :
350            retvalues["isValid"] = True
351            for i in range(len(varnames)) :
352                retvalues[varnames[i]] = dialog.variables[i].GetValue()
353        else :       
354            retvalues["isValid"] = False
355            for k in varvalues.keys() :
356                retvalues[k] = ""
357        self.dialogAnswer = retvalues
358        dialog.Destroy()
359       
360    def closeServer(self) :   
361        """Tells the xml-rpc server to exit."""
362        if not self.quitEvent.isSet() :
363            self.quitEvent.set()
364        server = xmlrpclib.ServerProxy("http://localhost:%s" % self.options.port)   
365        try :
366            # wake the server with an empty request
367            # for it to see the event object
368            # which has just been set
369            server.nop()
370        except :   
371            # Probably already stopped
372            pass
373       
374    def postInit(self, charset, options, arguments) :   
375        """Starts the XML-RPC server."""
376        self.charset = charset
377        self.options = options
378       
379        self.tbicon = wx.TaskBarIcon()
380        self.greenicon = wx.Icon(os.path.join(iconsdir, "pykoticon-green.ico"), \
381                                  wx.BITMAP_TYPE_ICO)
382        self.redicon = wx.Icon(os.path.join(iconsdir, "pykoticon-red.ico"), \
383                                  wx.BITMAP_TYPE_ICO)
384        self.tbicon.SetIcon(self.greenicon, "PyKotIcon")
385       
386        wx.EVT_TASKBAR_LEFT_DCLICK(self.tbicon, self.OnTaskBarActivate)
387        wx.EVT_TASKBAR_RIGHT_UP(self.tbicon, self.OnTaskBarMenu)
388       
389        self.TBMENU_ABOUT = wx.NewId()
390        self.TBMENU_RESTORE = wx.NewId()
391        self.TBMENU_CLOSE = wx.NewId()
392        wx.EVT_MENU(self.tbicon, self.TBMENU_ABOUT, \
393                                 self.OnAbout)
394        wx.EVT_MENU(self.tbicon, self.TBMENU_RESTORE, \
395                                 self.OnTaskBarActivate)
396        wx.EVT_MENU(self.tbicon, self.TBMENU_CLOSE, \
397                                 self.OnTaskBarClose)
398        self.menu = wx.Menu()
399        self.menu.Append(self.TBMENU_ABOUT, _("About"), _("About this software"))
400        if options.allowquit :
401            self.menu.Append(self.TBMENU_CLOSE, _("Quit"), \
402                                                _("Exit the application"))
403        wx.EVT_ICONIZE(self, self.OnIconify)
404        wx.EVT_CLOSE(self, self.OnClose)
405        self.Show(True)
406        self.Hide()
407       
408        self.quitEvent = threading.Event()
409        self.server = MyXMLRPCServer(self, options, arguments)
410       
411    def UTF8ToUserCharset(self, text) :
412        """Converts from UTF-8 to user's charset."""
413        if text is not None :
414            try :
415                return text.decode("UTF-8").encode(self.charset, "replace") 
416            except (UnicodeError, AttributeError) :   
417                try :
418                    # Maybe already in Unicode
419                    return text.encode(self.charset, "replace") 
420                except (UnicodeError, AttributeError) :
421                    pass # Don't know what to do
422        return text
423       
424    def userCharsetToUTF8(self, text) :
425        """Converts from user's charset to UTF-8."""
426        if text is not None :
427            try :
428                return text.decode(self.charset).encode("UTF-8")
429            except (UnicodeError, AttributeError) :
430                try :
431                    return text.decode(self.charset, "replace").encode("UTF-8") 
432                except (UnicodeError, AttributeError) :   
433                    try :
434                        # Maybe already in Unicode
435                        return text.encode("UTF-8", "replace") 
436                    except (UnicodeError, AttributeError) :
437                        pass # Don't know what to do
438        return text
439       
440
441class PyKotIconApp(wx.App):
442    def OnInit(self) :
443        self.frame = PyKotIcon(None, wx.ID_ANY)
444        self.frame.Show(False)
445        self.SetTopWindow(self.frame)
446        return True
447       
448    def postInit(self, charset, options, arguments) :   
449        """Continues processing."""
450        self.frame.postInit(charset, options, arguments)
451       
452       
453def main() :
454    """Program's entry point."""
455    # locale stuff
456    try :
457        locale.setlocale(locale.LC_ALL, ("", None))
458    except (locale.Error, IOError) :
459        locale.setlocale(locale.LC_ALL, None)
460    (language, charset) = locale.getlocale()
461    language = language or "C"
462    charset = ((sys.platform != "win32") and charset) or locale.getpreferredencoding()
463   
464    # translation stuff
465    try :
466        try :
467            trans = gettext.translation("pykoticon", languages=["%s.%s" % (language, charset)], codeset=charset)
468        except TypeError : # Python <2.4
469            trans = gettext.translation("pykoticon", languages=["%s.%s" % (language, charset)])
470        trans.install()
471    except :
472        gettext.NullTranslations().install()
473   
474   
475    parser = optparse.OptionParser(usage="pykoticon [options] server1 [server2 ...]")
476    parser.add_option("-v", "--version", 
477                            action="store_true", 
478                            dest="version",
479                            help=_("show PyKotIcon's version number and exit."))
480    parser.add_option("-c", "--cache", 
481                            type="int", 
482                            default=0, 
483                            dest="cache",
484                            help=_("the duration of the cache in seconds to keep input forms' datas in memory. Defaults to 0 second, meaning no cache."))
485    parser.add_option("-d", "--debug", 
486                            action="store_true", 
487                            dest="debug",
488                            help=_("activate debug mode."))
489    parser.add_option("-p", "--port", 
490                            type="int", 
491                            default=7654, 
492                            dest="port",
493                            help=_("the TCP port PyKotIcon will listen to, default is 7654."))
494    parser.add_option("-q", "--allowquit", 
495                            action="store_true", 
496                            dest="allowquit",
497                            help=_("allow the end user to close the application."))
498    (options, arguments) = parser.parse_args()
499    if options.version :
500        print "PyKotIcon v%(__version__)s" % globals()
501    else :
502        if not (1024 <= options.port <= 65535) :
503            sys.stderr.write(_("The TCP port number specified for --port must be between 1024 and 65535.\n"))
504        elif not (0 <= options.cache <= 86400) :   
505            sys.stderr.write(_("The duration specified for --cache must be between 0 and 86400 seconds.\n"))
506        else :   
507            app = PyKotIconApp()
508            app.postInit(charset, options, arguments)
509            app.MainLoop()
510   
511   
512if __name__ == '__main__':
513    main()
514   
Note: See TracBrowser for help on using the browser.