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

Revision 2196, 21.7 kB (checked in by jerome, 19 years ago)

Prepare the road to a fix for the high end Kyocera printer
which apparently wants to save some EEPROM writes...

  • 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                  }
181class PJLAccounter :
182    """A class for PJL print accounting."""
183    def __init__(self, parent, printerhostname) :
184        self.parent = parent
185        self.printerHostname = printerhostname
186        self.printerInternalPageCounter = self.printerStatus = None
187        self.timedout = 0
188       
189    def alarmHandler(self, signum, frame) :   
190        """Query has timedout, handle this."""
191        self.timedout = 1
192        raise IOError, "Waiting for PJL answer timed out. Please try again later."
193       
194    def retrievePJLValues(self) :   
195        """Retrieves a printer's internal page counter and status via PJL."""
196        port = 9100
197        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
198        try :
199            sock.connect((self.printerHostname, port))
200        except socket.error, msg :
201            self.parent.filter.printInfo(_("Problem during connection to %s:%s : %s") % (self.printerHostname, port, msg), "warn")
202        else :
203            try :
204                sock.send(pjlMessage)
205            except socket.error, msg :
206                self.parent.filter.printInfo(_("Problem while sending PJL query to %s:%s : %s") % (self.printerHostname, port, msg), "warn")
207            else :   
208                actualpagecount = self.printerStatus = None
209                self.timedout = 0
210                while (self.timedout == 0) or (actualpagecount is None) or (self.printerStatus is None) :
211                    signal.signal(signal.SIGALRM, self.alarmHandler)
212                    signal.alarm(3)
213                    try :
214                        answer = sock.recv(1024)
215                    except IOError, msg :   
216                        break   # our alarm handler was launched, probably
217                    else :   
218                        readnext = 0
219                        for line in [l.strip() for l in answer.split()] : 
220                            if line.startswith("CODE=") :
221                                self.printerStatus = line.split("=")[1]
222                            elif line.startswith("PAGECOUNT") :   
223                                readnext = 1 # page counter is on next line
224                            elif readnext :   
225                                actualpagecount = int(line.strip())
226                                readnext = 0
227                    signal.alarm(0)
228                self.printerInternalPageCounter = max(actualpagecount, self.printerInternalPageCounter)
229        sock.close()
230       
231    def waitPrinting(self) :
232        """Waits for printer status being 'printing'."""
233        firstvalue = None
234        while 1:
235            self.retrievePJLValues()
236            if self.printerStatus in ('10023', '10003') :
237                break
238            if self.printerInternalPageCounter is not None :   
239                if firstvalue is None :
240                    # first time we retrieved a page counter, save it
241                    firstvalue = self.printerInternalPageCounter
242                else :     
243                    # second time (or later)
244                    if firstvalue < self.printerInternalPageCounter :
245                        # Here we have a printer which lies :
246                        # it says it is not printing or warming up
247                        # BUT the page counter increases !!!
248                        # So we can probably quit being sure it is printing.
249                        self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.printername, "warn")
250                        break
251            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.printername)
252            time.sleep(ITERATIONDELAY)
253       
254    def waitIdle(self) :
255        """Waits for printer status being 'idle'."""
256        idle_num = idle_flag = 0
257        while 1 :
258            self.retrievePJLValues()
259            idle_flag = 0
260            if self.printerStatus in ('10000', '10001', '35078') :
261                idle_flag = 1
262            if idle_flag :   
263                idle_num += 1
264                if idle_num > STABILIZATIONDELAY :
265                    # printer status is stable, we can exit
266                    break
267            else :   
268                idle_num = 0
269            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.printername)
270            time.sleep(ITERATIONDELAY)
271   
272class Accounter(AccounterBase) :
273    def __init__(self, kotabackend, arguments) :
274        """Initializes querying accounter."""
275        AccounterBase.__init__(self, kotabackend, arguments)
276        self.isSoftware = 0
277       
278    def getPrinterInternalPageCounter(self) :   
279        """Returns the printer's internal page counter."""
280        self.filter.logdebug("Reading printer %s's internal page counter..." % self.filter.printername)
281        counter = self.askPrinterPageCounter(self.filter.printerhostname)
282        self.filter.logdebug("Printer %s's internal page counter value is : %s" % (self.filter.printername, str(counter)))
283        return counter   
284       
285    def beginJob(self, printer) :   
286        """Saves printer internal page counter at start of job."""
287        # save page counter before job
288        self.LastPageCounter = self.getPrinterInternalPageCounter()
289        self.fakeBeginJob()
290       
291    def fakeBeginJob(self) :   
292        """Fakes a begining of a job."""
293        self.counterbefore = self.getLastPageCounter()
294       
295    def endJob(self, printer) :   
296        """Saves printer internal page counter at end of job."""
297        # save page counter after job
298        self.LastPageCounter = self.counterafter = self.getPrinterInternalPageCounter()
299       
300    def getJobSize(self, printer) :   
301        """Returns the actual job size."""
302        if (not self.counterbefore) or (not self.counterafter) :
303            # there was a problem retrieving page counter
304            self.filter.printInfo(_("A problem occured while reading printer %s's internal page counter.") % printer.Name, "warn")
305            if printer.LastJob.Exists :
306                # if there's a previous job, use the last value from database
307                self.filter.printInfo(_("Retrieving printer %s's page counter from database instead.") % printer.Name, "warn")
308                if not self.counterbefore : 
309                    self.counterbefore = printer.LastJob.PrinterPageCounter or 0
310                if not self.counterafter :
311                    self.counterafter = printer.LastJob.PrinterPageCounter or 0
312                before = min(self.counterbefore, self.counterafter)   
313                after = max(self.counterbefore, self.counterafter)   
314                self.counterbefore = before
315                self.counterafter = after
316                if (not self.counterbefore) or (not self.counterafter) or (self.counterbefore == self.counterafter) :
317                    self.filter.printInfo(_("Couldn't retrieve printer %s's internal page counter either before or after printing.") % printer.Name, "warn")
318                    self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
319                    self.counterbefore = 0
320                    self.counterafter = 1
321            else :
322                self.filter.printInfo(_("No previous job in database for printer %s.") % printer.Name, "warn")
323                self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
324                self.counterbefore = 0
325                self.counterafter = 1
326               
327        jobsize = (self.counterafter - self.counterbefore)   
328        if jobsize < 0 :
329            # Try to take care of HP printers
330            # Their internal page counter is saved to NVRAM
331            # only every 10 pages. If the printer was switched
332            # off then back on during the job, and that the
333            # counters difference is negative, we know
334            # the formula (we can't know if more than eleven
335            # pages were printed though) :
336            if jobsize > -10 :
337                jobsize += 10
338            else :   
339                # here we may have got a printer being replaced
340                # DURING the job. This is HIGHLY improbable !
341                self.filter.printInfo(_("Inconsistent values for printer %s's internal page counter.") % printer.Name, "warn")
342                self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
343                jobsize = 1
344        return jobsize
345       
346    def askPrinterPageCounter(self, printer) :
347        """Returns the page counter from the printer via an external command.
348       
349           The external command must report the life time page number of the printer on stdout.
350        """
351        commandline = self.arguments.strip() % locals()
352        cmdlower = commandline.lower()
353        if cmdlower == "snmp" :
354            if hasSNMP :
355                return self.askWithSNMP(printer)
356            else :   
357                raise PyKotaAccounterError, _("Internal SNMP accounting asked, but Python-SNMP is not available. Please download it from http://pysnmp.sourceforge.net")
358        elif cmdlower == "pjl" :
359            return self.askWithPJL(printer)
360           
361        if printer is None :
362            raise PyKotaAccounterError, _("Unknown printer address in HARDWARE(%s) for printer %s") % (commandline, self.filter.printername)
363        while 1 :   
364            self.filter.printInfo(_("Launching HARDWARE(%s)...") % commandline)
365            pagecounter = None
366            child = popen2.Popen4(commandline)   
367            try :
368                answer = child.fromchild.read()
369            except IOError :   
370                # we were interrupted by a signal, certainely a SIGTERM
371                # caused by the user cancelling the current job
372                try :
373                    os.kill(child.pid, signal.SIGTERM)
374                except :   
375                    pass # already killed ?
376                self.filter.printInfo(_("SIGTERM was sent to hardware accounter %s (pid: %s)") % (commandline, child.pid))
377            else :   
378                lines = [l.strip() for l in answer.split("\n")]
379                for i in range(len(lines)) : 
380                    try :
381                        pagecounter = int(lines[i])
382                    except (AttributeError, ValueError) :
383                        self.filter.printInfo(_("Line [%s] skipped in accounter's output. Trying again...") % lines[i])
384                    else :   
385                        break
386            child.fromchild.close()   
387            child.tochild.close()
388            try :
389                status = child.wait()
390            except OSError, msg :   
391                self.filter.logdebug("Error while waiting for hardware accounter pid %s : %s" % (child.pid, msg))
392            else :   
393                if os.WIFEXITED(status) :
394                    status = os.WEXITSTATUS(status)
395                self.filter.printInfo(_("Hardware accounter %s exit code is %s") % (self.arguments, str(status)))
396               
397            if pagecounter is None :
398                message = _("Unable to query printer %s via HARDWARE(%s)") % (printer, commandline)
399                if self.onerror == "CONTINUE" :
400                    self.filter.printInfo(message, "error")
401                else :
402                    raise PyKotaAccounterError, message 
403            else :       
404                return pagecounter       
405       
406    def askWithSNMP(self, printer) :
407        """Returns the page counter from the printer via internal SNMP handling."""
408        acc = SNMPAccounter(self, printer)
409        try :
410            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
411               (os.environ.get("PYKOTAACTION") != "DENY") and \
412               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
413               self.filter.jobSizeBytes :
414                acc.waitPrinting()
415            acc.waitIdle()   
416        except :   
417            if acc.printerInternalPageCounter is None :
418                raise
419            else :   
420                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")
421        return acc.printerInternalPageCounter
422       
423    def askWithPJL(self, printer) :
424        """Returns the page counter from the printer via internal PJL handling."""
425        acc = PJLAccounter(self, printer)
426        try :
427            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
428               (os.environ.get("PYKOTAACTION") != "DENY") and \
429               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
430               self.filter.jobSizeBytes :
431                acc.waitPrinting()
432            acc.waitIdle()   
433        except :   
434            if acc.printerInternalPageCounter is None :
435                raise
436            else :   
437                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")
438        return acc.printerInternalPageCounter
Note: See TracBrowser for help on using the browser.