root / pykoticon / trunk / bin / pykoticon @ 138

Revision 138, 18.6 kB (checked in by jerome, 18 years ago)

Removed the traceback when client disconnects while server
is waiting for a dialog box to close.
Displays an error message when the user wants to quit in spite
of the administrator forbidding this.

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