root / pykoticon / trunk / bin / pykoticon @ 112

Revision 112, 16.0 kB (checked in by jerome, 18 years ago)

Now receives and sends back UTF-8 encoded text (when the text comes
from user input)

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