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

Revision 3180, 14.2 kB (checked in by jerome, 17 years ago)

Now the various delays are configurable when using hardware accounting,
through the newly introduced 'statusstabilizationdelay' and 'statusstabilizationloops'
directives in pykota.conf

  • 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, 2007 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 errno
29import time
30import threading
31import Queue
32
33from pykota import constants
34
35FORMFEEDCHAR = chr(0x0c)     # Form Feed character, ends PJL answers.
36
37# Old method : pjlMessage = "\033%-12345X@PJL USTATUSOFF\r\n@PJL INFO STATUS\r\n@PJL INFO PAGECOUNT\r\n\033%-12345X"
38# Here's a new method, which seems to work fine on my HP2300N, while the
39# previous one didn't.
40# TODO : We could also experiment with USTATUS JOB=ON and we would know for sure
41# when the job is finished, without having to poll the printer repeatedly.
42pjlMessage = "\033%-12345X@PJL USTATUS DEVICE=ON\r\n@PJL INFO STATUS\r\n@PJL INFO PAGECOUNT\r\n@PJL USTATUS DEVICE=OFF\033%-12345X"
43pjlStatusValues = {
44                    "10000" : "Powersave Mode",
45                    "10001" : "Ready Online",
46                    "10002" : "Ready Offline",
47                    "10003" : "Warming Up",
48                    "10004" : "Self Test",
49                    "10005" : "Reset",
50                    "10023" : "Printing",
51                    "35078" : "Powersave Mode",         # 10000 is ALSO powersave !!!
52                    "40000" : "Sleep Mode",             # Standby
53                  }
54                 
55class Handler :
56    """A class for PJL print accounting."""
57    def __init__(self, parent, printerhostname, skipinitialwait=False) :
58        self.parent = parent
59        self.printerHostname = printerhostname
60        self.skipinitialwait = skipinitialwait
61        try :
62            self.port = int(self.parent.arguments.split(":")[1].strip())
63        except (IndexError, ValueError) :
64            self.port = 9100
65        self.printerInternalPageCounter = self.printerStatus = None
66        self.closed = False
67        self.sock = None
68        self.queue = None
69        self.readthread = None
70        self.quitEvent = threading.Event()
71       
72    def __del__(self) :   
73        """Ensures the network connection is closed at object deletion time."""
74        self.close()
75       
76    def open(self) :   
77        """Opens the network connection."""
78        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
79        try :
80            sock.connect((self.printerHostname, self.port))
81        except socket.error, msg :
82            self.parent.filter.printInfo(_("Problem during connection to %s:%s : %s") % (self.printerHostname, self.port, str(msg)), "warn")
83            return False
84        else :
85            sock.setblocking(False)
86            self.sock = sock
87            self.closed = False
88            self.quitEvent.clear()
89            self.queue = Queue.Queue(0)
90            self.readthread = threading.Thread(target=self.readloop)
91            self.readthread.start()
92            self.parent.filter.logdebug("Connected to printer %s:%s" % (self.printerHostname, self.port))
93            return True
94       
95    def close(self) :   
96        """Closes the network connection."""
97        if not self.closed :
98            self.quitEvent.set()
99            if self.readthread is not None :
100                self.readthread.join()
101                self.readthread = None
102            if self.sock is not None :
103                self.sock.close()
104                self.sock = None
105            self.parent.filter.logdebug("Connection to %s:%s is now closed." % (self.printerHostname, self.port))
106            self.queue = None
107            self.closed = True
108           
109    def readloop(self) :       
110        """Reading loop thread."""
111        self.parent.filter.logdebug("Reading thread started.")
112        buffer = []
113        while not self.quitEvent.isSet() :
114            try :
115                answer = self.sock.recv(4096)
116            except socket.error, (err, msg) :
117                time.sleep(0.1) # We will try again later in all cases
118                if err != errno.EAGAIN :
119                    self.parent.filter.printInfo(_("Problem while receiving PJL answer from %s:%s : %s") % (self.printerHostname, self.port, str(msg)), "warn")
120            else :   
121                buffer.append(answer)
122                if answer.endswith(FORMFEEDCHAR) :
123                    self.queue.put("".join(buffer))
124                    buffer = []
125        if buffer :             
126            self.queue.put("".join(buffer))           
127        self.parent.filter.logdebug("Reading thread ended.")
128           
129    def retrievePJLValues(self) :   
130        """Retrieves a printer's internal page counter and status via PJL."""
131        try :
132            self.sock.send(pjlMessage)
133        except socket.error, msg :
134            self.parent.filter.printInfo(_("Problem while sending PJL query to %s:%s : %s") % (self.printerHostname, self.port, str(msg)), "warn")
135        else :   
136            self.parent.filter.logdebug("Query sent to %s : %s" % (self.printerHostname, repr(pjlMessage)))
137            actualpagecount = self.printerStatus = None
138            while (actualpagecount is None) or (self.printerStatus is None) :
139                try :
140                    answer = self.queue.get(True, 5)
141                except Queue.Empty :   
142                    self.parent.filter.logdebug("Timeout when reading printer's answer from %s:%s" % (self.printerHostname, self.port))
143                else :   
144                    readnext = False
145                    self.parent.filter.logdebug("PJL answer : %s" % repr(answer))
146                    for line in [l.strip() for l in answer.split()] : 
147                        if line.startswith("CODE=") :
148                            self.printerStatus = line.split("=")[1]
149                            self.parent.filter.logdebug("Found status : %s" % self.printerStatus)
150                        elif line.startswith("PAGECOUNT=") :   
151                            try :
152                                actualpagecount = int(line.split('=')[1].strip())
153                            except ValueError :   
154                                self.parent.filter.logdebug("Received incorrect datas : [%s]" % line.strip())
155                            else :
156                                self.parent.filter.logdebug("Found pages counter : %s" % actualpagecount)
157                        elif line.startswith("PAGECOUNT") :   
158                            readnext = True # page counter is on next line
159                        elif readnext :   
160                            try :
161                                actualpagecount = int(line.strip())
162                            except ValueError :   
163                                self.parent.filter.logdebug("Received incorrect datas : [%s]" % line.strip())
164                            else :
165                                self.parent.filter.logdebug("Found pages counter : %s" % actualpagecount)
166                                readnext = False
167            self.printerInternalPageCounter = max(actualpagecount, self.printerInternalPageCounter)
168       
169    def waitPrinting(self) :
170        """Waits for printer status being 'printing'."""
171        statusstabilizationdelay = constants.get(self.parent.filter, "StatusStabilizationDelay")
172        noprintingmaxdelay = constants.get(self.parent.filter, "NoPrintingMaxDelay")
173        if not noprintingmaxdelay :
174            self.parent.filter.logdebug("Will wait indefinitely until printer %s is in 'printing' state." % self.parent.filter.PrinterName)
175        else :   
176            self.parent.filter.logdebug("Will wait until printer %s is in 'printing' state or %i seconds have elapsed." % (self.parent.filter.PrinterName, noprintingmaxdelay))
177        previousValue = self.parent.getLastPageCounter()
178        timebefore = time.time()
179        firstvalue = None
180        while True :
181            self.retrievePJLValues()
182            if self.printerStatus in ('10023', '10003') :
183                break
184            if self.printerInternalPageCounter is not None :   
185                if firstvalue is None :
186                    # first time we retrieved a page counter, save it
187                    firstvalue = self.printerInternalPageCounter
188                else :     
189                    # second time (or later)
190                    if firstvalue < self.printerInternalPageCounter :
191                        # Here we have a printer which lies :
192                        # it says it is not printing or warming up
193                        # BUT the page counter increases !!!
194                        # So we can probably quit being sure it is printing.
195                        self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.PrinterName, "warn")
196                        break
197                    elif noprintingmaxdelay and ((time.time() - timebefore) > noprintingmaxdelay) :
198                        # More than X seconds without the printer being in 'printing' mode
199                        # We can safely assume this won't change if printer is now 'idle'
200                        if self.printerStatus in ('10000', '10001', '35078', '40000') :
201                            if self.printerInternalPageCounter == previousValue :
202                                # Here the job won't be printed, because probably
203                                # the printer rejected it for some reason.
204                                self.parent.filter.printInfo("Printer %s probably won't print this job !!!" % self.parent.filter.PrinterName, "warn")
205                            else :     
206                                # Here the job has already been entirely printed, and
207                                # the printer has already passed from 'idle' to 'printing' to 'idle' again.
208                                self.parent.filter.printInfo("Printer %s has probably already printed this job !!!" % self.parent.filter.PrinterName, "warn")
209                            break
210            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.PrinterName)
211            time.sleep(statusstabilizationdelay)
212       
213    def waitIdle(self) :
214        """Waits for printer status being 'idle'."""
215        statusstabilizationdelay = constants.get(self.parent.filter, "StatusStabilizationDelay")
216        statusstabilizationloops = constants.get(self.parent.filter, "StatusStabilizationLoops")
217        idle_num = 0
218        while True :
219            self.retrievePJLValues()
220            if self.printerStatus in ('10000', '10001', '35078', '40000') :
221                if (self.printerInternalPageCounter is not None) \
222                   and self.skipinitialwait \
223                   and (os.environ.get("PYKOTAPHASE") == "BEFORE") :
224                    self.parent.filter.logdebug("No need to wait for the printer to be idle, it is the case already.")
225                    return 
226                idle_num += 1
227                if idle_num >= statusstabilizationloops :
228                    # printer status is stable, we can exit
229                    break
230            else :   
231                idle_num = 0
232            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.PrinterName)
233            time.sleep(statusstabilizationdelay)
234   
235    def retrieveInternalPageCounter(self) :
236        """Returns the page counter from the printer via internal PJL handling."""
237        while not self.open() :
238            self.parent.filter.logdebug("Will retry in 1 second.")
239            time.sleep(1)
240        try :
241            try :
242                if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
243                   (os.environ.get("PYKOTAACTION") == "ALLOW") and \
244                   (os.environ.get("PYKOTAPHASE") == "AFTER") and \
245                   self.parent.filter.JobSizeBytes :
246                    self.waitPrinting()
247                self.waitIdle()   
248            except :   
249                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")
250                raise
251            else :   
252                return self.printerInternalPageCounter
253        finally :       
254            self.close()
255           
256def main(hostname) :
257    """Tries PJL accounting for a printer host."""
258    class fakeFilter :
259        """Fakes a filter for testing purposes."""
260        def __init__(self) :
261            """Initializes the fake filter."""
262            self.PrinterName = "FakePrintQueue"
263            self.JobSizeBytes = 1
264           
265        def printInfo(self, msg, level="info") :
266            """Prints informational message."""
267            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
268            sys.stderr.flush()
269           
270        def logdebug(self, msg) :   
271            """Prints debug message."""
272            self.printInfo(msg, "debug")
273           
274    class fakeAccounter :       
275        """Fakes an accounter for testing purposes."""
276        def __init__(self) :
277            """Initializes fake accounter."""
278            self.arguments = "pjl:9100"
279            self.filter = fakeFilter()
280            self.protocolHandler = Handler(self, sys.argv[1])
281           
282        def getLastPageCounter(self) :   
283            """Fakes the return of a page counter."""
284            return 0
285       
286    acc = fakeAccounter()           
287    return acc.protocolHandler.retrieveInternalPageCounter()
288   
289if __name__ == "__main__" :           
290    if len(sys.argv) != 2 :   
291        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
292    else :   
293        def _(msg) :
294            return msg
295           
296        pagecounter = main(sys.argv[1])
297        print "Internal page counter's value is : %s" % pagecounter
298       
Note: See TracBrowser for help on using the browser.