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

Revision 2182, 20.4 kB (checked in by jerome, 19 years ago)

Fixed f.cking typo

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