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

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

Improved the detection of a stable idle status in snmp accounting
stuff, because "lp *.pdf" would make only the first job to be
accounted correctly. PJL stuff needs some work.

  • 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 byb default.
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.