root / pykota / trunk / pykota / accounters / pjl.py @ 3025

Revision 3025, 12.7 kB (checked in by jerome, 18 years ago)

Added the 'noprintingmaxdelay' directive to workaround printers
which don't conform to RFC3805.
Improved the reliability of SNMP and PJL hardware accounting.
PJL still needs some work though...

  • Property svn:eol-style set to native
  • Property svn:keywords set to Auth Date Rev Id
Line 
1# PyKota
2# -*- coding: ISO-8859-15 -*-
3#
4# PyKota - Print Quotas for CUPS and LPRng
5#
6# (c) 2003, 2004, 2005, 2006 Jerome Alet <alet@librelogiciel.com>
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20#
21# $Id$
22#
23#
24
25import sys
26import os
27import socket
28import time
29import signal
30
31# NB : in fact these variables don't do much, since the time
32# is in fact wasted in the sock.recv() blocking call, with the timeout
33ITERATIONDELAY = 1   # 1 Second
34STABILIZATIONDELAY = 3 # We must read three times the same value to consider it to be stable
35NOPRINTINGMAXDELAY = 60 # The printer must begin to print within 60 seconds.
36
37# Here's the real thing :
38TIMEOUT = 5
39
40# Old method : pjlMessage = "\033%-12345X@PJL USTATUSOFF\r\n@PJL INFO STATUS\r\n@PJL INFO PAGECOUNT\r\n\033%-12345X"
41# Here's a new method, which seems to work fine on my HP2300N, while the
42# previous one didn't.
43# TODO : We could also experiment with USTATUS JOB=ON and we would know for sure
44# when the job is finished, without having to poll the printer repeatedly.
45pjlMessage = "\033%-12345X@PJL USTATUS DEVICE=ON\r\n@PJL INFO STATUS\r\n@PJL INFO PAGECOUNT\r\n@PJL USTATUS DEVICE=OFF\033%-12345X"
46pjlStatusValues = {
47                    "10000" : "Powersave Mode",
48                    "10001" : "Ready Online",
49                    "10002" : "Ready Offline",
50                    "10003" : "Warming Up",
51                    "10004" : "Self Test",
52                    "10005" : "Reset",
53                    "10023" : "Printing",
54                    "35078" : "Powersave Mode",         # 10000 is ALSO powersave !!!
55                    "40000" : "Sleep Mode",             # Standby
56                  }
57                 
58class Handler :
59    """A class for PJL print accounting."""
60    def __init__(self, parent, printerhostname) :
61        self.parent = parent
62        self.printerHostname = printerhostname
63        try :
64            self.port = int(self.parent.arguments.split(":")[1].strip())
65        except (IndexError, ValueError) :
66            self.port = 9100
67        self.printerInternalPageCounter = self.printerStatus = None
68        self.timedout = 0
69       
70    def alarmHandler(self, signum, frame) :   
71        """Query has timedout, handle this."""
72        self.timedout = 1
73        raise IOError, "Waiting for PJL answer timed out. Please try again later."
74       
75    def retrievePJLValues(self) :   
76        """Retrieves a printer's internal page counter and status via PJL."""
77        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
78        try :
79            sock.connect((self.printerHostname, self.port))
80        except socket.error, msg :
81            self.parent.filter.printInfo(_("Problem during connection to %s:%s : %s") % (self.printerHostname, self.port, str(msg)), "warn")
82        else :
83            self.parent.filter.logdebug("Connected to printer %s" % self.printerHostname)
84            try :
85                sock.send(pjlMessage)
86            except socket.error, msg :
87                self.parent.filter.printInfo(_("Problem while sending PJL query to %s:%s : %s") % (self.printerHostname, self.port, str(msg)), "warn")
88            else :   
89                self.parent.filter.logdebug("Query sent to %s : %s" % (self.printerHostname, repr(pjlMessage)))
90                actualpagecount = self.printerStatus = None
91                self.timedout = 0
92                while (self.timedout == 0) or (actualpagecount is None) or (self.printerStatus is None) :
93                    signal.signal(signal.SIGALRM, self.alarmHandler)
94                    signal.alarm(TIMEOUT)
95                    try :
96                        answer = sock.recv(1024)
97                    except IOError, msg :   
98                        self.parent.filter.logdebug("I/O Error [%s] : alarm handler probably called" % msg)
99                        break   # our alarm handler was launched, probably
100                    except socket.error, msg :
101                        self.parent.filter.printInfo(_("Problem while receiving PJL answer from %s:%s : %s") % (self.printerHostname, self.port, str(msg)), "warn")
102                    else :   
103                        readnext = 0
104                        self.parent.filter.logdebug("PJL answer : %s" % repr(answer))
105                        for line in [l.strip() for l in answer.split()] : 
106                            if line.startswith("CODE=") :
107                                self.printerStatus = line.split("=")[1]
108                                self.parent.filter.logdebug("Found status : %s" % self.printerStatus)
109                            elif line.startswith("PAGECOUNT=") :   
110                                try :
111                                    actualpagecount = int(line.split('=')[1].strip())
112                                except ValueError :   
113                                    self.parent.filter.logdebug("Received incorrect datas : [%s]" % line.strip())
114                                else :
115                                    self.parent.filter.logdebug("Found pages counter : %s" % actualpagecount)
116                            elif line.startswith("PAGECOUNT") :   
117                                readnext = 1 # page counter is on next line
118                            elif readnext :   
119                                try :
120                                    actualpagecount = int(line.strip())
121                                except ValueError :   
122                                    self.parent.filter.logdebug("Received incorrect datas : [%s]" % line.strip())
123                                else :
124                                    self.parent.filter.logdebug("Found pages counter : %s" % actualpagecount)
125                                    readnext = 0
126                    signal.alarm(0)
127                self.printerInternalPageCounter = max(actualpagecount, self.printerInternalPageCounter)
128        sock.close()
129        self.parent.filter.logdebug("Connection to %s is now closed." % self.printerHostname)
130       
131    def waitPrinting(self) :
132        """Waits for printer status being 'printing'."""
133        try :
134            noprintingmaxdelay = int(self.parent.filter.config.getNoPrintingMaxDelay(self.parent.filter.PrinterName))
135        except (TypeError, AttributeError) : # NB : AttributeError in testing mode because I'm lazy !
136            noprintingmaxdelay = NOPRINTINGMAXDELAY
137            self.parent.filter.logdebug("No max delay defined for printer %s, using %i seconds." % (self.parent.filter.PrinterName, noprintingmaxdelay))
138        if not noprintingmaxdelay :
139            self.parent.filter.logdebug("Will wait indefinitely until printer %s is in 'printing' state." % self.parent.filter.PrinterName)
140        else :   
141            self.parent.filter.logdebug("Will wait until printer %s is in 'printing' state or %i seconds have elapsed." % (self.parent.filter.PrinterName, noprintingmaxdelay))
142        previousValue = self.parent.getLastPageCounter()
143        timebefore = time.time()
144        firstvalue = None
145        while 1:
146            self.retrievePJLValues()
147            if self.printerStatus in ('10023', '10003') :
148                break
149            if self.printerInternalPageCounter is not None :   
150                if firstvalue is None :
151                    # first time we retrieved a page counter, save it
152                    firstvalue = self.printerInternalPageCounter
153                else :     
154                    # second time (or later)
155                    if firstvalue < self.printerInternalPageCounter :
156                        # Here we have a printer which lies :
157                        # it says it is not printing or warming up
158                        # BUT the page counter increases !!!
159                        # So we can probably quit being sure it is printing.
160                        self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.PrinterName, "warn")
161                        break
162                    elif noprintingmaxdelay and ((time.time() - timebefore) > noprintingmaxdelay) :
163                        # More than X seconds without the printer being in 'printing' mode
164                        # We can safely assume this won't change if printer is now 'idle'
165                        if self.printerStatus in ('10000', '10001', '35078', '40000') :
166                            if self.printerInternalPageCounter == previousValue :
167                                # Here the job won't be printed, because probably
168                                # the printer rejected it for some reason.
169                                self.parent.filter.printInfo("Printer %s probably won't print this job !!!" % self.parent.filter.PrinterName, "warn")
170                            else :     
171                                # Here the job has already been entirely printed, and
172                                # the printer has already passed from 'idle' to 'printing' to 'idle' again.
173                                self.parent.filter.printInfo("Printer %s has probably already printed this job !!!" % self.parent.filter.PrinterName, "warn")
174                            break
175            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.PrinterName)
176            time.sleep(ITERATIONDELAY)
177       
178    def waitIdle(self) :
179        """Waits for printer status being 'idle'."""
180        idle_num = idle_flag = 0
181        while 1 :
182            self.retrievePJLValues()
183            idle_flag = 0
184            if self.printerStatus in ('10000', '10001', '35078', '40000') :
185                idle_flag = 1
186            if idle_flag :   
187                idle_num += 1
188                if idle_num >= STABILIZATIONDELAY :
189                    # printer status is stable, we can exit
190                    break
191            else :   
192                idle_num = 0
193            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.PrinterName)
194            time.sleep(ITERATIONDELAY)
195   
196    def retrieveInternalPageCounter(self) :
197        """Returns the page counter from the printer via internal PJL handling."""
198        try :
199            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
200               (os.environ.get("PYKOTAACTION") == "ALLOW") and \
201               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
202               self.parent.filter.JobSizeBytes :
203                self.waitPrinting()
204            self.waitIdle()   
205        except :   
206            self.parent.filter.printInfo(_("PJL querying stage interrupted. Using latest value seen for internal page counter (%s) on printer %s.") % (self.printerInternalPageCounter, self.parent.filter.PrinterName), "warn")
207            raise
208        return self.printerInternalPageCounter
209           
210def main(hostname) :
211    """Tries PJL accounting for a printer host."""
212    class fakeFilter :
213        """Fakes a filter for testing purposes."""
214        def __init__(self) :
215            """Initializes the fake filter."""
216            self.PrinterName = "FakePrintQueue"
217            self.JobSizeBytes = 1
218           
219        def printInfo(self, msg, level="info") :
220            """Prints informational message."""
221            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
222            sys.stderr.flush()
223           
224        def logdebug(self, msg) :   
225            """Prints debug message."""
226            self.printInfo(msg, "debug")
227           
228    class fakeAccounter :       
229        """Fakes an accounter for testing purposes."""
230        def __init__(self) :
231            """Initializes fake accounter."""
232            self.arguments = "pjl:9100"
233            self.filter = fakeFilter()
234            self.protocolHandler = Handler(self, sys.argv[1])
235           
236        def getLastPageCounter(self) :   
237            """Fakes the return of a page counter."""
238            return 0
239       
240    acc = fakeAccounter()           
241    return acc.protocolHandler.retrieveInternalPageCounter()
242   
243if __name__ == "__main__" :           
244    if len(sys.argv) != 2 :   
245        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
246    else :   
247        def _(msg) :
248            return msg
249           
250        pagecounter = main(sys.argv[1])
251        print "Internal page counter's value is : %s" % pagecounter
252       
Note: See TracBrowser for help on using the browser.