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

Revision 2180, 19.5 kB (checked in by jerome, 19 years ago)

Fixed a problem with "warmup" status for some printers

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