root / pykoticon / trunk / bin / pykoticon @ 122

Revision 122, 16.9 kB (checked in by jerome, 18 years ago)

Doesn't log incoming requests anymore unless in debug mode.

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