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

Revision 3193, 14.3 kB (checked in by jerome, 17 years ago)

Ensures the whole PJL message is sent to the printer.

  • 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                if answer :
122                    buffer.append(answer)
123                    if answer.endswith(FORMFEEDCHAR) :
124                        self.queue.put("".join(buffer))
125                        buffer = []
126        if buffer :             
127            self.queue.put("".join(buffer))           
128        self.parent.filter.logdebug("Reading thread ended.")
129           
130    def retrievePJLValues(self) :   
131        """Retrieves a printer's internal page counter and status via PJL."""
132        try :
133            self.sock.sendall(pjlMessage)
134        except socket.error, msg :
135            self.parent.filter.printInfo(_("Problem while sending PJL query to %s:%s : %s") % (self.printerHostname, self.port, str(msg)), "warn")
136        else :   
137            self.parent.filter.logdebug("Query sent to %s : %s" % (self.printerHostname, repr(pjlMessage)))
138            actualpagecount = self.printerStatus = None
139            while (actualpagecount is None) or (self.printerStatus is None) :
140                try :
141                    answer = self.queue.get(True, 5)
142                except Queue.Empty :   
143                    self.parent.filter.logdebug("Timeout when reading printer's answer from %s:%s" % (self.printerHostname, self.port))
144                else :   
145                    readnext = False
146                    self.parent.filter.logdebug("PJL answer : %s" % repr(answer))
147                    for line in [l.strip() for l in answer.split()] : 
148                        if line.startswith("CODE=") :
149                            self.printerStatus = line.split("=")[1]
150                            self.parent.filter.logdebug("Found status : %s" % self.printerStatus)
151                        elif line.startswith("PAGECOUNT=") :   
152                            try :
153                                actualpagecount = int(line.split('=')[1].strip())
154                            except ValueError :   
155                                self.parent.filter.logdebug("Received incorrect datas : [%s]" % line.strip())
156                            else :
157                                self.parent.filter.logdebug("Found pages counter : %s" % actualpagecount)
158                        elif line.startswith("PAGECOUNT") :   
159                            readnext = True # page counter is on next line
160                        elif readnext :   
161                            try :
162                                actualpagecount = int(line.strip())
163                            except ValueError :   
164                                self.parent.filter.logdebug("Received incorrect datas : [%s]" % line.strip())
165                            else :
166                                self.parent.filter.logdebug("Found pages counter : %s" % actualpagecount)
167                                readnext = False
168            self.printerInternalPageCounter = max(actualpagecount, self.printerInternalPageCounter)
169       
170    def waitPrinting(self) :
171        """Waits for printer status being 'printing'."""
172        statusstabilizationdelay = constants.get(self.parent.filter, "StatusStabilizationDelay")
173        noprintingmaxdelay = constants.get(self.parent.filter, "NoPrintingMaxDelay")
174        if not noprintingmaxdelay :
175            self.parent.filter.logdebug("Will wait indefinitely until printer %s is in 'printing' state." % self.parent.filter.PrinterName)
176        else :   
177            self.parent.filter.logdebug("Will wait until printer %s is in 'printing' state or %i seconds have elapsed." % (self.parent.filter.PrinterName, noprintingmaxdelay))
178        previousValue = self.parent.getLastPageCounter()
179        timebefore = time.time()
180        firstvalue = None
181        while True :
182            self.retrievePJLValues()
183            if self.printerStatus in ('10023', '10003') :
184                break
185            if self.printerInternalPageCounter is not None :   
186                if firstvalue is None :
187                    # first time we retrieved a page counter, save it
188                    firstvalue = self.printerInternalPageCounter
189                else :     
190                    # second time (or later)
191                    if firstvalue < self.printerInternalPageCounter :
192                        # Here we have a printer which lies :
193                        # it says it is not printing or warming up
194                        # BUT the page counter increases !!!
195                        # So we can probably quit being sure it is printing.
196                        self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.PrinterName, "warn")
197                        break
198                    elif noprintingmaxdelay and ((time.time() - timebefore) > noprintingmaxdelay) :
199                        # More than X seconds without the printer being in 'printing' mode
200                        # We can safely assume this won't change if printer is now 'idle'
201                        if self.printerStatus in ('10000', '10001', '35078', '40000') :
202                            if self.printerInternalPageCounter == previousValue :
203                                # Here the job won't be printed, because probably
204                                # the printer rejected it for some reason.
205                                self.parent.filter.printInfo("Printer %s probably won't print this job !!!" % self.parent.filter.PrinterName, "warn")
206                            else :     
207                                # Here the job has already been entirely printed, and
208                                # the printer has already passed from 'idle' to 'printing' to 'idle' again.
209                                self.parent.filter.printInfo("Printer %s has probably already printed this job !!!" % self.parent.filter.PrinterName, "warn")
210                            break
211            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.PrinterName)
212            time.sleep(statusstabilizationdelay)
213       
214    def waitIdle(self) :
215        """Waits for printer status being 'idle'."""
216        statusstabilizationdelay = constants.get(self.parent.filter, "StatusStabilizationDelay")
217        statusstabilizationloops = constants.get(self.parent.filter, "StatusStabilizationLoops")
218        idle_num = 0
219        while True :
220            self.retrievePJLValues()
221            if self.printerStatus in ('10000', '10001', '35078', '40000') :
222                if (self.printerInternalPageCounter is not None) \
223                   and self.skipinitialwait \
224                   and (os.environ.get("PYKOTAPHASE") == "BEFORE") :
225                    self.parent.filter.logdebug("No need to wait for the printer to be idle, it is the case already.")
226                    return 
227                idle_num += 1
228                if idle_num >= statusstabilizationloops :
229                    # printer status is stable, we can exit
230                    break
231            else :   
232                idle_num = 0
233            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.PrinterName)
234            time.sleep(statusstabilizationdelay)
235   
236    def retrieveInternalPageCounter(self) :
237        """Returns the page counter from the printer via internal PJL handling."""
238        while not self.open() :
239            self.parent.filter.logdebug("Will retry in 1 second.")
240            time.sleep(1)
241        try :
242            try :
243                if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
244                   (os.environ.get("PYKOTAACTION") == "ALLOW") and \
245                   (os.environ.get("PYKOTAPHASE") == "AFTER") and \
246                   self.parent.filter.JobSizeBytes :
247                    self.waitPrinting()
248                self.waitIdle()   
249            except :   
250                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")
251                raise
252            else :   
253                return self.printerInternalPageCounter
254        finally :       
255            self.close()
256           
257def main(hostname) :
258    """Tries PJL accounting for a printer host."""
259    class fakeFilter :
260        """Fakes a filter for testing purposes."""
261        def __init__(self) :
262            """Initializes the fake filter."""
263            self.PrinterName = "FakePrintQueue"
264            self.JobSizeBytes = 1
265           
266        def printInfo(self, msg, level="info") :
267            """Prints informational message."""
268            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
269            sys.stderr.flush()
270           
271        def logdebug(self, msg) :   
272            """Prints debug message."""
273            self.printInfo(msg, "debug")
274           
275    class fakeAccounter :       
276        """Fakes an accounter for testing purposes."""
277        def __init__(self) :
278            """Initializes fake accounter."""
279            self.arguments = "pjl:9100"
280            self.filter = fakeFilter()
281            self.protocolHandler = Handler(self, sys.argv[1])
282           
283        def getLastPageCounter(self) :   
284            """Fakes the return of a page counter."""
285            return 0
286       
287    acc = fakeAccounter()           
288    return acc.protocolHandler.retrieveInternalPageCounter()
289   
290if __name__ == "__main__" :           
291    if len(sys.argv) != 2 :   
292        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
293    else :   
294        def _(msg) :
295            return msg
296           
297        pagecounter = main(sys.argv[1])
298        print "Internal page counter's value is : %s" % pagecounter
299       
Note: See TracBrowser for help on using the browser.