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

Revision 3481, 14.2 kB (checked in by jerome, 15 years ago)

Changed copyright years.
Copyright years are now dynamic when displayed by a command line tool.

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