root / pykota / trunk / pykota / accounters / hardware.py @ 2200

Revision 2200, 21.8 kB (checked in by jerome, 19 years ago)

Added PJL code for sleep mode, thanks to Rong-En Fan

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1# PyKota
2# -*- coding: ISO-8859-15 -*-
3#
4# PyKota - Print Quotas for CUPS and LPRng
5#
6# (c) 2003-2004 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
20#
21# $Id$
22#
23#
24
25import os
26import socket
27import time
28import signal
29import popen2
30
31from pykota.accounter import AccounterBase, PyKotaAccounterError
32
33ITERATIONDELAY = 1.0   # 1 Second
34STABILIZATIONDELAY = 3 # We must read three times the same value to consider it to be stable
35
36try :
37    from pysnmp.asn1.encoding.ber.error import TypeMismatchError
38    from pysnmp.mapping.udp.error import SnmpOverUdpError
39    from pysnmp.mapping.udp.role import Manager
40    from pysnmp.proto.api import alpha
41except ImportError :
42    hasSNMP = 0
43else :   
44    hasSNMP = 1
45    pageCounterOID = ".1.3.6.1.2.1.43.10.2.1.4.1.1"  # SNMPv2-SMI::mib-2.43.10.2.1.4.1.1
46    pageCounterOID2 = ".1.3.6.1.2.1.43.10.2.1.5.1.1"  # SNMPv2-SMI::mib-2.43.10.2.1.5.1.1
47    hrPrinterStatusOID = ".1.3.6.1.2.1.25.3.5.1.1.1" # SNMPv2-SMI::mib-2.25.3.5.1.1.1
48    printerStatusValues = { 1 : 'other',
49                            2 : 'unknown',
50                            3 : 'idle',
51                            4 : 'printing',
52                            5 : 'warmup',
53                          }
54    hrDeviceStatusOID = ".1.3.6.1.2.1.25.3.2.1.5.1" # SNMPv2-SMI::mib-2.25.3.2.1.5.1
55    deviceStatusValues = { 1 : 'unknown',
56                           2 : 'running',
57                           3 : 'warning',
58                           4 : 'testing',
59                           5 : 'down',
60                         } 
61    hrPrinterDetectedErrorStateOID = ".1.3.6.1.2.1.25.3.5.1.2.1" # SNMPv2-SMI::mib-2.25.3.5.1.2.1
62    prtConsoleDisplayBufferTextOID = ".1.3.6.1.2.1.43.16.5.1.2.1.1" # SNMPv2-SMI::mib-2.43.16.5.1.2.1.1
63                         
64    #                     
65    # Documentation taken from RFC 3805 (Printer MIB v2) and RFC 2790 (Host Resource MIB)
66    #
67    # TODO : if hrDeviceStatus==2 and hrPrinterStatus==1 then it's powersave mode.
68    #
69    class SNMPAccounter :
70        """A class for SNMP print accounting."""
71        def __init__(self, parent, printerhostname) :
72            self.parent = parent
73            self.printerHostname = printerhostname
74            self.printerInternalPageCounter = None
75            self.printerInternalPageCounter2 = None
76            self.printerStatus = None
77            self.deviceStatus = None
78           
79        def retrieveSNMPValues(self) :   
80            """Retrieves a printer's internal page counter and status via SNMP."""
81            ver = alpha.protoVersions[alpha.protoVersionId1]
82            req = ver.Message()
83            req.apiAlphaSetCommunity('public')
84            req.apiAlphaSetPdu(ver.GetRequestPdu())
85            req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()), \
86                                                        (pageCounterOID2, ver.Null()), \
87                                                        (hrPrinterStatusOID, ver.Null()), \
88                                                        (hrDeviceStatusOID, ver.Null()))
89            tsp = Manager()
90            try :
91                tsp.sendAndReceive(req.berEncode(), (self.printerHostname, 161), (self.handleAnswer, req))
92            except SnmpOverUdpError, msg :   
93                self.parent.filter.printInfo(_("Network error while doing SNMP queries on printer %s : %s") % (self.printerHostname, msg), "warn")
94            tsp.close()
95   
96        def handleAnswer(self, wholeMsg, notusedhere, req):
97            """Decodes and handles the SNMP answer."""
98            self.parent.filter.logdebug("SNMP message : '%s'" % repr(wholeMsg))
99            ver = alpha.protoVersions[alpha.protoVersionId1]
100            rsp = ver.Message()
101            try :
102                rsp.berDecode(wholeMsg)
103            except TypeMismatchError, msg :   
104                self.parent.filter.printInfo(_("SNMP message decoding error for printer %s : %s") % (self.printerHostname, msg), "warn")
105            else :
106                if req.apiAlphaMatch(rsp):
107                    errorStatus = rsp.apiAlphaGetPdu().apiAlphaGetErrorStatus()
108                    if errorStatus:
109                        self.parent.filter.printInfo(_("Problem encountered while doing SNMP queries on printer %s : %s") % (self.printerHostname, errorStatus), "warn")
110                    else:
111                        self.values = []
112                        for varBind in rsp.apiAlphaGetPdu().apiAlphaGetVarBindList():
113                            self.values.append(varBind.apiAlphaGetOidVal()[1].rawAsn1Value)
114                        try :   
115                            # keep maximum value seen for printer's internal page counter
116                            self.printerInternalPageCounter = max(self.printerInternalPageCounter, self.values[0])
117                            self.printerInternalPageCounter2 = max(self.printerInternalPageCounter2, self.values[1])
118                            self.printerStatus = self.values[2]
119                            self.deviceStatus = self.values[3]
120                            self.parent.filter.logdebug("SNMP answer is decoded : PageCounters : (%s, %s)  PrinterStatus : %s  DeviceStatus : %s" % tuple(self.values))
121                        except IndexError :   
122                            self.parent.filter.logdebug("SNMP answer is incomplete : %s" % str(self.values))
123                            pass
124                        else :   
125                            return 1
126                       
127        def waitPrinting(self) :
128            """Waits for printer status being 'printing'."""
129            firstvalue = None
130            while 1:
131                self.retrieveSNMPValues()
132                statusAsString = printerStatusValues.get(self.printerStatus)
133                if statusAsString in ('printing', 'warmup') :
134                    break
135                if self.printerInternalPageCounter is not None :   
136                    if firstvalue is None :
137                        # first time we retrieved a page counter, save it
138                        firstvalue = self.printerInternalPageCounter
139                    else :     
140                        # second time (or later)
141                        if firstvalue < self.printerInternalPageCounter :
142                            # Here we have a printer which lies :
143                            # it says it is not printing or warming up
144                            # BUT the page counter increases !!!
145                            # So we can probably quit being sure it is printing.
146                            self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.printername, "warn")
147                            break
148                self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.printername)   
149                time.sleep(ITERATIONDELAY)
150           
151        def waitIdle(self) :
152            """Waits for printer status being 'idle'."""
153            idle_num = idle_flag = 0
154            while 1 :
155                self.retrieveSNMPValues()
156                statusAsString = printerStatusValues.get(self.printerStatus)
157                idle_flag = 0
158                if statusAsString in ('idle',) :
159                    idle_flag = 1
160                if idle_flag :   
161                    idle_num += 1
162                    if idle_num > STABILIZATIONDELAY :
163                        # printer status is stable, we can exit
164                        break
165                else :   
166                    idle_num = 0
167                self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.printername)   
168                time.sleep(ITERATIONDELAY)
169               
170pjlMessage = "\033%-12345X@PJL USTATUSOFF\r\n@PJL INFO STATUS\r\n@PJL INFO PAGECOUNT\r\n\033%-12345X"
171pjlStatusValues = {
172                    "10000" : "Powersave Mode",
173                    "10001" : "Ready Online",
174                    "10002" : "Ready Offline",
175                    "10003" : "Warming Up",
176                    "10004" : "Self Test",
177                    "10005" : "Reset",
178                    "10023" : "Printing",
179                    "35078" : "Powersave Mode",         # 10000 is ALSO powersave !!!
180                    "40000" : "Sleep Mode",             # Standby
181                  }
182class PJLAccounter :
183    """A class for PJL print accounting."""
184    def __init__(self, parent, printerhostname) :
185        self.parent = parent
186        self.printerHostname = printerhostname
187        self.printerInternalPageCounter = self.printerStatus = None
188        self.timedout = 0
189       
190    def alarmHandler(self, signum, frame) :   
191        """Query has timedout, handle this."""
192        self.timedout = 1
193        raise IOError, "Waiting for PJL answer timed out. Please try again later."
194       
195    def retrievePJLValues(self) :   
196        """Retrieves a printer's internal page counter and status via PJL."""
197        port = 9100
198        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
199        try :
200            sock.connect((self.printerHostname, port))
201        except socket.error, msg :
202            self.parent.filter.printInfo(_("Problem during connection to %s:%s : %s") % (self.printerHostname, port, msg), "warn")
203        else :
204            try :
205                sock.send(pjlMessage)
206            except socket.error, msg :
207                self.parent.filter.printInfo(_("Problem while sending PJL query to %s:%s : %s") % (self.printerHostname, port, msg), "warn")
208            else :   
209                actualpagecount = self.printerStatus = None
210                self.timedout = 0
211                while (self.timedout == 0) or (actualpagecount is None) or (self.printerStatus is None) :
212                    signal.signal(signal.SIGALRM, self.alarmHandler)
213                    signal.alarm(3)
214                    try :
215                        answer = sock.recv(1024)
216                    except IOError, msg :   
217                        break   # our alarm handler was launched, probably
218                    else :   
219                        readnext = 0
220                        for line in [l.strip() for l in answer.split()] : 
221                            if line.startswith("CODE=") :
222                                self.printerStatus = line.split("=")[1]
223                            elif line.startswith("PAGECOUNT") :   
224                                readnext = 1 # page counter is on next line
225                            elif readnext :   
226                                actualpagecount = int(line.strip())
227                                readnext = 0
228                    signal.alarm(0)
229                self.printerInternalPageCounter = max(actualpagecount, self.printerInternalPageCounter)
230        sock.close()
231       
232    def waitPrinting(self) :
233        """Waits for printer status being 'printing'."""
234        firstvalue = None
235        while 1:
236            self.retrievePJLValues()
237            if self.printerStatus in ('10023', '10003') :
238                break
239            if self.printerInternalPageCounter is not None :   
240                if firstvalue is None :
241                    # first time we retrieved a page counter, save it
242                    firstvalue = self.printerInternalPageCounter
243                else :     
244                    # second time (or later)
245                    if firstvalue < self.printerInternalPageCounter :
246                        # Here we have a printer which lies :
247                        # it says it is not printing or warming up
248                        # BUT the page counter increases !!!
249                        # So we can probably quit being sure it is printing.
250                        self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.printername, "warn")
251                        break
252            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.printername)
253            time.sleep(ITERATIONDELAY)
254       
255    def waitIdle(self) :
256        """Waits for printer status being 'idle'."""
257        idle_num = idle_flag = 0
258        while 1 :
259            self.retrievePJLValues()
260            idle_flag = 0
261            if self.printerStatus in ('10000', '10001', '35078', '40000') :
262                idle_flag = 1
263            if idle_flag :   
264                idle_num += 1
265                if idle_num > STABILIZATIONDELAY :
266                    # printer status is stable, we can exit
267                    break
268            else :   
269                idle_num = 0
270            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.printername)
271            time.sleep(ITERATIONDELAY)
272   
273class Accounter(AccounterBase) :
274    def __init__(self, kotabackend, arguments) :
275        """Initializes querying accounter."""
276        AccounterBase.__init__(self, kotabackend, arguments)
277        self.isSoftware = 0
278       
279    def getPrinterInternalPageCounter(self) :   
280        """Returns the printer's internal page counter."""
281        self.filter.logdebug("Reading printer %s's internal page counter..." % self.filter.printername)
282        counter = self.askPrinterPageCounter(self.filter.printerhostname)
283        self.filter.logdebug("Printer %s's internal page counter value is : %s" % (self.filter.printername, str(counter)))
284        return counter   
285       
286    def beginJob(self, printer) :   
287        """Saves printer internal page counter at start of job."""
288        # save page counter before job
289        self.LastPageCounter = self.getPrinterInternalPageCounter()
290        self.fakeBeginJob()
291       
292    def fakeBeginJob(self) :   
293        """Fakes a begining of a job."""
294        self.counterbefore = self.getLastPageCounter()
295       
296    def endJob(self, printer) :   
297        """Saves printer internal page counter at end of job."""
298        # save page counter after job
299        self.LastPageCounter = self.counterafter = self.getPrinterInternalPageCounter()
300       
301    def getJobSize(self, printer) :   
302        """Returns the actual job size."""
303        if (not self.counterbefore) or (not self.counterafter) :
304            # there was a problem retrieving page counter
305            self.filter.printInfo(_("A problem occured while reading printer %s's internal page counter.") % printer.Name, "warn")
306            if printer.LastJob.Exists :
307                # if there's a previous job, use the last value from database
308                self.filter.printInfo(_("Retrieving printer %s's page counter from database instead.") % printer.Name, "warn")
309                if not self.counterbefore : 
310                    self.counterbefore = printer.LastJob.PrinterPageCounter or 0
311                if not self.counterafter :
312                    self.counterafter = printer.LastJob.PrinterPageCounter or 0
313                before = min(self.counterbefore, self.counterafter)   
314                after = max(self.counterbefore, self.counterafter)   
315                self.counterbefore = before
316                self.counterafter = after
317                if (not self.counterbefore) or (not self.counterafter) or (self.counterbefore == self.counterafter) :
318                    self.filter.printInfo(_("Couldn't retrieve printer %s's internal page counter either before or after printing.") % printer.Name, "warn")
319                    self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
320                    self.counterbefore = 0
321                    self.counterafter = 1
322            else :
323                self.filter.printInfo(_("No previous job in database for printer %s.") % printer.Name, "warn")
324                self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
325                self.counterbefore = 0
326                self.counterafter = 1
327               
328        jobsize = (self.counterafter - self.counterbefore)   
329        if jobsize < 0 :
330            # Try to take care of HP printers
331            # Their internal page counter is saved to NVRAM
332            # only every 10 pages. If the printer was switched
333            # off then back on during the job, and that the
334            # counters difference is negative, we know
335            # the formula (we can't know if more than eleven
336            # pages were printed though) :
337            if jobsize > -10 :
338                jobsize += 10
339            else :   
340                # here we may have got a printer being replaced
341                # DURING the job. This is HIGHLY improbable (but already happened) !
342                self.filter.printInfo(_("Inconsistent values for printer %s's internal page counter.") % printer.Name, "warn")
343                self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
344                jobsize = 1
345        return jobsize
346       
347    def askPrinterPageCounter(self, printer) :
348        """Returns the page counter from the printer via an external command.
349       
350           The external command must report the life time page number of the printer on stdout.
351        """
352        commandline = self.arguments.strip() % locals()
353        cmdlower = commandline.lower()
354        if cmdlower == "snmp" :
355            if hasSNMP :
356                return self.askWithSNMP(printer)
357            else :   
358                raise PyKotaAccounterError, _("Internal SNMP accounting asked, but Python-SNMP is not available. Please download it from http://pysnmp.sourceforge.net")
359        elif cmdlower == "pjl" :
360            return self.askWithPJL(printer)
361           
362        if printer is None :
363            raise PyKotaAccounterError, _("Unknown printer address in HARDWARE(%s) for printer %s") % (commandline, self.filter.printername)
364        while 1 :   
365            self.filter.printInfo(_("Launching HARDWARE(%s)...") % commandline)
366            pagecounter = None
367            child = popen2.Popen4(commandline)   
368            try :
369                answer = child.fromchild.read()
370            except IOError :   
371                # we were interrupted by a signal, certainely a SIGTERM
372                # caused by the user cancelling the current job
373                try :
374                    os.kill(child.pid, signal.SIGTERM)
375                except :   
376                    pass # already killed ?
377                self.filter.printInfo(_("SIGTERM was sent to hardware accounter %s (pid: %s)") % (commandline, child.pid))
378            else :   
379                lines = [l.strip() for l in answer.split("\n")]
380                for i in range(len(lines)) : 
381                    try :
382                        pagecounter = int(lines[i])
383                    except (AttributeError, ValueError) :
384                        self.filter.printInfo(_("Line [%s] skipped in accounter's output. Trying again...") % lines[i])
385                    else :   
386                        break
387            child.fromchild.close()   
388            child.tochild.close()
389            try :
390                status = child.wait()
391            except OSError, msg :   
392                self.filter.logdebug("Error while waiting for hardware accounter pid %s : %s" % (child.pid, msg))
393            else :   
394                if os.WIFEXITED(status) :
395                    status = os.WEXITSTATUS(status)
396                self.filter.printInfo(_("Hardware accounter %s exit code is %s") % (self.arguments, str(status)))
397               
398            if pagecounter is None :
399                message = _("Unable to query printer %s via HARDWARE(%s)") % (printer, commandline)
400                if self.onerror == "CONTINUE" :
401                    self.filter.printInfo(message, "error")
402                else :
403                    raise PyKotaAccounterError, message 
404            else :       
405                return pagecounter       
406       
407    def askWithSNMP(self, printer) :
408        """Returns the page counter from the printer via internal SNMP handling."""
409        acc = SNMPAccounter(self, printer)
410        try :
411            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
412               (os.environ.get("PYKOTAACTION") != "DENY") and \
413               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
414               self.filter.jobSizeBytes :
415                acc.waitPrinting()
416            acc.waitIdle()   
417        except :   
418            if acc.printerInternalPageCounter is None :
419                raise
420            else :   
421                self.filter.printInfo(_("SNMP querying stage interrupted. Using latest value seen for internal page counter (%s) on printer %s.") % (acc.printerInternalPageCounter, self.filter.printername), "warn")
422        return acc.printerInternalPageCounter
423       
424    def askWithPJL(self, printer) :
425        """Returns the page counter from the printer via internal PJL handling."""
426        acc = PJLAccounter(self, printer)
427        try :
428            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
429               (os.environ.get("PYKOTAACTION") != "DENY") and \
430               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
431               self.filter.jobSizeBytes :
432                acc.waitPrinting()
433            acc.waitIdle()   
434        except :   
435            if acc.printerInternalPageCounter is None :
436                raise
437            else :   
438                self.filter.printInfo(_("PJL querying stage interrupted. Using latest value seen for internal page counter (%s) on printer %s.") % (acc.printerInternalPageCounter, self.filter.printername), "warn")
439        return acc.printerInternalPageCounter
Note: See TracBrowser for help on using the browser.