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

Revision 3260, 14.3 kB (checked in by jerome, 16 years ago)

Changed license to GNU GPL v3 or later.
Changed Python source encoding from ISO-8859-15 to UTF-8 (only ASCII
was used anyway).

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