root / pykoticon / trunk / bin / pykoticon @ 3439

Revision 3439, 19.8 kB (checked in by jerome, 14 years ago)

Removed trailing spaces.

  • 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        try :
99            SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self)
100        except TypeError :
101            SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self, False, None)
102        SocketServer.ThreadingTCPServer.__init__(self, addr, requestHandler)
103
104class MyXMLRPCServer(ThreadedXMLRPCServer) :
105    """My own server class."""
106    allow_reuse_address = True
107    def __init__(self, frame, options, arguments) :
108        ThreadedXMLRPCServer.__init__(self, \
109                                      ('0.0.0.0', options.port), \
110                                      SimpleXMLRPCServer.SimpleXMLRPCRequestHandler, \
111                                      options.debug)
112        self.frame = frame
113        self.debug = options.debug
114        self.cacheduration = options.cache
115        self.cache = {}
116        self.printServers = [ socket.gethostbyname(arg) for arg in arguments ]
117        if "127.0.0.1" not in self.printServers :
118            self.printServers.append("127.0.0.1") # to allow clean shutdown
119        loop = threading.Thread(target=self.mainloop)
120        loop.start()
121
122    def logDebug(self, message) :
123        """Logs a debug message if debug mode is active."""
124        if self.debug :
125            sys.stderr.write("%s\n" % message)
126
127    def getAnswerFromCache(self, key) :
128        """Tries to extract a value from the cache and returns it if still valid."""
129        cacheentry = self.cache.get(key)
130        if cacheentry is not None :
131            (birth, value) = cacheentry
132            if (time.time() - birth) < self.cacheduration :
133                self.logDebug("Cache hit for %s" % str(key))
134                return value # NB : we don't extend the life of this entry
135            else :
136                self.logDebug("Cache expired for %s" % str(key))
137        else :
138            self.logDebug("Cache miss for %s" % str(key))
139        return None
140
141    def storeAnswerInCache(self, key, value) :
142        """Stores an entry in the cache."""
143        self.cache[key] = (time.time(), value)
144        self.logDebug("Cache store for %s" % str(key))
145
146    def export_askDatas(self, labels, varnames, varvalues) :
147        """Asks some textual datas defined by a list of labels, a list of variables' names and a list of variables values in a mapping."""
148        values = {}
149        for (key, value) in varvalues.items() :
150            values[key] = self.frame.UTF8ToUserCharset(value.data)
151        cachekey = tuple(values.items())
152        retcode = self.getAnswerFromCache(cachekey)
153        if (retcode is None) or (not retcode["isValid"]) :
154            wx.CallAfter(self.frame.askDatas, [ self.frame.UTF8ToUserCharset(label.data) for label in labels ], \
155                                              varnames, \
156                                              values)
157            # ugly, isn't it ?
158            while self.frame.dialogAnswer is None :
159                time.sleep(0.1)
160            retcode = self.frame.dialogAnswer
161            for (key, value) in retcode.items() :
162                if key != "isValid" :
163                    retcode[key] = xmlrpclib.Binary(self.frame.userCharsetToUTF8(value))
164            self.frame.dialogAnswer = None # prepare for next call, just in case
165            self.storeAnswerInCache(cachekey, retcode)
166        return retcode
167
168    def export_quitApplication(self) :
169        """Makes the application quit."""
170        self.frame.quitEvent.set()
171        wx.CallAfter(self.frame.OnClose, None)
172        return True
173
174    def export_showDialog(self, message, yesno) :
175        """Opens a notification or confirmation dialog."""
176        wx.CallAfter(self.frame.showDialog, self.frame.UTF8ToUserCharset(message.data), yesno)
177        # ugly, isn't it ?
178        while self.frame.dialogAnswer is None :
179            time.sleep(0.1)
180        retcode = self.frame.dialogAnswer
181        self.frame.dialogAnswer = None # prepare for next call, just in case
182        return retcode
183
184    def export_nop(self) :
185        """Does nothing, but allows a clean shutdown from the frame itself."""
186        return True
187
188    def _dispatch(self, method, params) :
189        """Ensure that only export_* methods are available."""
190        return getattr(self, "export_%s" % method)(*params)
191
192    def handle_error(self, request, client_address) :
193        """Doesn't display an ugly traceback in case an error occurs."""
194        self.logDebug("An exception occured while handling an incoming request from %s:%s" % (client_address[0], client_address[1]))
195
196    def verify_request(self, request, client_address) :
197        """Ensures that requests which don't come from the print server are rejected."""
198        (client, port) = client_address
199        if client in self.printServers :
200            self.logDebug("%s accepted." % client)
201            return True
202        else :
203            # Unauthorized access !
204            self.logDebug("%s rejected." % client)
205            return False
206
207    def mainloop(self) :
208        """XML-RPC Server's main loop."""
209        self.register_function(self.export_askDatas)
210        self.register_function(self.export_showDialog)
211        self.register_function(self.export_quitApplication)
212        self.register_function(self.export_nop)
213        while not self.frame.quitEvent.isSet() :
214            self.handle_request()
215        self.server_close()
216        sys.exit(0)
217
218
219class GenericInputDialog(wx.Dialog) :
220    """Generic input dialog box."""
221    def __init__(self, parent, id, labels, varnames, varvalues):
222        wx.Dialog.__init__(self, parent, id, \
223               _("PyKotIcon data input"), \
224               style = wx.CAPTION \
225                     | wx.THICK_FRAME \
226                     | wx.STAY_ON_TOP \
227                     | wx.DIALOG_MODAL)
228
229        self.variables = []
230        vsizer = wx.BoxSizer(wx.VERTICAL)
231        for i in range(len(varnames)) :
232            varname = varnames[i]
233            try :
234                label = labels[i]
235            except IndexError :
236                label = ""
237            labelid = wx.NewId()
238            varid = wx.NewId()
239            labelst = wx.StaticText(self, labelid, label)
240            if varname.lower().find("password") != -1 :
241                variable = wx.TextCtrl(self, varid, varvalues.get(varname, ""), style=wx.TE_PASSWORD)
242            else :
243                variable = wx.TextCtrl(self, varid, varvalues.get(varname, ""))
244            self.variables.append(variable)
245            hsizer = wx.BoxSizer(wx.HORIZONTAL)
246            hsizer.Add(labelst, 0, wx.ALIGN_CENTER | wx.ALIGN_RIGHT | wx.ALL, 5)
247            hsizer.Add(variable, 0, wx.ALIGN_CENTER | wx.ALIGN_LEFT | wx.ALL, 5)
248            vsizer.Add(hsizer, 0, wx.ALIGN_CENTER | wx.ALL, 5)
249
250        okbutton = wx.Button(self, wx.ID_OK, "OK")
251        vsizer.Add(okbutton, 0, wx.ALIGN_CENTER | wx.ALL, 5)
252        if self.variables :
253            self.variables[0].SetFocus()
254        self.SetAutoLayout(True)
255        self.SetSizerAndFit(vsizer)
256        self.Layout()
257
258
259class PyKotIcon(wx.Frame):
260    """Main class."""
261    def __init__(self, parent, id):
262        self.dialogAnswer = None
263        wx.Frame.__init__(self, parent, id, \
264               _("PyKotIcon info for %s") % self.getCurrentUserName(), \
265               size = (0, 0), \
266               style = wx.FRAME_NO_TASKBAR | wx.NO_FULL_REPAINT_ON_RESIZE)
267
268    def getCurrentUserName(self) :
269        """Retrieves the current user's name."""
270        try :
271            if isWindows :
272                return win32api.GetUserName()
273            else :
274                return pwd.getpwuid(os.geteuid())[0]
275        except :
276            return "** Unknown **"
277
278    def OnIconify(self, event) :
279        """Iconify/De-iconify the application."""
280        if not self.IsIconized() :
281            self.Iconize(True)
282        self.Hide()
283
284    def OnTaskBarActivate(self, event) :
285        """Show the application if it is minimized."""
286        if self.IsIconized() :
287            self.Iconize(False)
288        if not self.IsShown() :
289            self.Show(True)
290        self.Raise()
291
292    def OnClose(self, event) :
293        """Cleanly quit the application."""
294        if (event is None) \
295           or self.options.allowquit :
296            self.closeServer()
297            self.menu.Destroy()
298            self.tbicon.Destroy()
299            self.Destroy()
300            return True
301        else :
302            # self.quitIsForbidden()
303            return False
304
305    def OnTaskBarMenu(self, event) :
306        """Open the taskbar menu."""
307        self.tbicon.PopupMenu(self.menu)
308
309    def OnTaskBarClose(self, event) :
310        """React to close from the taskbar."""
311        self.Close()
312
313    def quitIsForbidden(self) :
314        """Displays a message indicating that quitting the application is not allowed."""
315        message = _("Sorry, this was forbidden by your system administrator.")
316        caption = _("Information")
317        style = wx.OK | wx.ICON_INFORMATION | wx.STAY_ON_TOP
318        dialog = wx.MessageDialog(self, message, caption, style)
319        dialog.ShowModal()
320        dialog.Destroy()
321
322    def OnAbout(self, event) :
323        """Displays the about box."""
324        dialog = wx.MessageDialog(self, aboutbox % globals(), \
325                                        _("About"), \
326                                        wx.OK | wx.ICON_INFORMATION)
327        dialog.Raise()
328        dialog.ShowModal()
329        dialog.Destroy()
330
331    def showDialog(self, message, yesno) :
332        """Opens a notification dialog."""
333        self.dialogAnswer = None
334        if yesno :
335            caption = _("Confirmation")
336            style = wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION
337        else :
338            caption = _("Information")
339            style = wx.OK | wx.ICON_INFORMATION
340        style |= wx.STAY_ON_TOP
341        dialog = wx.MessageDialog(self, message, caption, style)
342        dialog.Raise()
343        self.dialogAnswer = ((dialog.ShowModal() == wx.ID_NO) and "CANCEL") or "OK"
344        dialog.Destroy()
345
346    def askDatas(self, labels, varnames, varvalues) :
347        """Opens a dialog box asking for data entry."""
348        self.dialogAnswer = None
349        dialog = GenericInputDialog(self, wx.ID_ANY, labels, varnames, varvalues)
350        dialog.Raise()
351        retvalues = {}
352        if dialog.ShowModal() == wx.ID_OK :
353            retvalues["isValid"] = True
354            for i in range(len(varnames)) :
355                retvalues[varnames[i]] = dialog.variables[i].GetValue()
356        else :
357            retvalues["isValid"] = False
358            for k in varvalues.keys() :
359                retvalues[k] = ""
360        self.dialogAnswer = retvalues
361        dialog.Destroy()
362
363    def closeServer(self) :
364        """Tells the xml-rpc server to exit."""
365        if not self.quitEvent.isSet() :
366            self.quitEvent.set()
367        server = xmlrpclib.ServerProxy("http://localhost:%s" % self.options.port)
368        try :
369            # wake the server with an empty request
370            # for it to see the event object
371            # which has just been set
372            server.nop()
373        except :
374            # Probably already stopped
375            pass
376
377    def postInit(self, charset, options, arguments) :
378        """Starts the XML-RPC server."""
379        self.charset = charset
380        self.options = options
381
382        self.tbicon = wx.TaskBarIcon()
383        self.greenicon = wx.Icon(os.path.join(iconsdir, "pykoticon-green.ico"), \
384                                  wx.BITMAP_TYPE_ICO)
385        self.redicon = wx.Icon(os.path.join(iconsdir, "pykoticon-red.ico"), \
386                                  wx.BITMAP_TYPE_ICO)
387        self.tbicon.SetIcon(self.greenicon, "PyKotIcon")
388
389        wx.EVT_TASKBAR_LEFT_DCLICK(self.tbicon, self.OnTaskBarActivate)
390        wx.EVT_TASKBAR_RIGHT_UP(self.tbicon, self.OnTaskBarMenu)
391
392        self.TBMENU_ABOUT = wx.NewId()
393        self.TBMENU_RESTORE = wx.NewId()
394        self.TBMENU_CLOSE = wx.NewId()
395        wx.EVT_MENU(self.tbicon, self.TBMENU_ABOUT, \
396                                 self.OnAbout)
397        wx.EVT_MENU(self.tbicon, self.TBMENU_RESTORE, \
398                                 self.OnTaskBarActivate)
399        wx.EVT_MENU(self.tbicon, self.TBMENU_CLOSE, \
400                                 self.OnTaskBarClose)
401        self.menu = wx.Menu()
402        self.menu.Append(self.TBMENU_ABOUT, _("About"), _("About this software"))
403        if options.allowquit :
404            self.menu.Append(self.TBMENU_CLOSE, _("Quit"), \
405                                                _("Exit the application"))
406        wx.EVT_ICONIZE(self, self.OnIconify)
407        wx.EVT_CLOSE(self, self.OnClose)
408        self.Show(True)
409        self.Hide()
410
411        self.quitEvent = threading.Event()
412        self.server = MyXMLRPCServer(self, options, arguments)
413
414    def UTF8ToUserCharset(self, text) :
415        """Converts from UTF-8 to user's charset."""
416        if text is not None :
417            try :
418                return text.decode("UTF-8").encode(self.charset, "replace")
419            except (UnicodeError, AttributeError) :
420                try :
421                    # Maybe already in Unicode
422                    return text.encode(self.charset, "replace")
423                except (UnicodeError, AttributeError) :
424                    pass # Don't know what to do
425        return text
426
427    def userCharsetToUTF8(self, text) :
428        """Converts from user's charset to UTF-8."""
429        if text is not None :
430            try :
431                return text.decode(self.charset).encode("UTF-8")
432            except (UnicodeError, AttributeError) :
433                try :
434                    return text.decode(self.charset, "replace").encode("UTF-8")
435                except (UnicodeError, AttributeError) :
436                    try :
437                        # Maybe already in Unicode
438                        return text.encode("UTF-8", "replace")
439                    except (UnicodeError, AttributeError) :
440                        pass # Don't know what to do
441        return text
442
443
444class PyKotIconApp(wx.App):
445    def OnInit(self) :
446        self.frame = PyKotIcon(None, wx.ID_ANY)
447        self.frame.Show(False)
448        self.SetTopWindow(self.frame)
449        return True
450
451    def postInit(self, charset, options, arguments) :
452        """Continues processing."""
453        self.frame.postInit(charset, options, arguments)
454
455
456def main() :
457    """Program's entry point."""
458    # locale stuff
459    try :
460        locale.setlocale(locale.LC_ALL, ("", None))
461    except (locale.Error, IOError) :
462        locale.setlocale(locale.LC_ALL, None)
463    (language, charset) = locale.getlocale()
464    language = language or "C"
465    charset = ((sys.platform != "win32") and charset) or locale.getpreferredencoding()
466
467    # translation stuff
468    try :
469        try :
470            trans = gettext.translation("pykoticon", languages=["%s.%s" % (language, charset)], codeset=charset)
471        except TypeError : # Python <2.4
472            trans = gettext.translation("pykoticon", languages=["%s.%s" % (language, charset)])
473        trans.install()
474    except :
475        gettext.NullTranslations().install()
476
477
478    parser = optparse.OptionParser(usage="pykoticon [options] server1 [server2 ...]")
479    parser.add_option("-v", "--version",
480                            action="store_true",
481                            dest="version",
482                            help=_("show PyKotIcon's version number and exit."))
483    parser.add_option("-c", "--cache",
484                            type="int",
485                            default=0,
486                            dest="cache",
487                            help=_("the duration of the cache in seconds to keep input forms' datas in memory. Defaults to 0 second, meaning no cache."))
488    parser.add_option("-d", "--debug",
489                            action="store_true",
490                            dest="debug",
491                            help=_("activate debug mode."))
492    parser.add_option("-p", "--port",
493                            type="int",
494                            default=7654,
495                            dest="port",
496                            help=_("the TCP port PyKotIcon will listen to, default is 7654."))
497    parser.add_option("-q", "--allowquit",
498                            action="store_true",
499                            dest="allowquit",
500                            help=_("allow the end user to close the application."))
501    (options, arguments) = parser.parse_args()
502    if options.version :
503        print "PyKotIcon v%(__version__)s" % globals()
504    else :
505        if not (1024 <= options.port <= 65535) :
506            sys.stderr.write(_("The TCP port number specified for --port must be between 1024 and 65535.\n"))
507        elif not (0 <= options.cache <= 86400) :
508            sys.stderr.write(_("The duration specified for --cache must be between 0 and 86400 seconds.\n"))
509        else :
510            app = PyKotIconApp()
511            app.postInit(charset, options, arguments)
512            app.MainLoop()
513
514
515if __name__ == '__main__':
516    main()
Note: See TracBrowser for help on using the browser.