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

Revision 2027, 22.2 kB (checked in by jalet, 19 years ago)

Tried to fix a problem with printers which are slow to pass into printing mode.

  • 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# $Log$
24# Revision 1.35  2005/01/12 22:44:06  jalet
25# Tried to fix a problem with printers which are slow to pass into printing mode.
26#
27# Revision 1.34  2004/11/19 11:57:51  jalet
28# Modified the SNMP fix as hinted by pysnmp's maintainer
29#
30# Revision 1.33  2004/11/19 10:35:37  jalet
31# Catches TypeMismatchError in SNMP answer handling code
32#
33# Revision 1.32  2004/11/16 23:23:40  jalet
34# Fixed internal PJL handling wrt the 35078 PowerSave mode.
35#
36# Revision 1.31  2004/11/01 14:32:26  jalet
37# Fix for unneeded out of band status in pjl_over_tcp/9100
38#
39# Revision 1.30  2004/10/05 09:21:34  jalet
40# Removed misleading comments
41#
42# Revision 1.29  2004/10/05 09:20:07  jalet
43# Reduced delay from 2 to 1 seconds in internal SNMP and PJL_over_TCP
44# handlers
45#
46# Revision 1.28  2004/09/27 20:09:30  jalet
47# Lowered timeout delay for PJL queries
48#
49# Revision 1.27  2004/09/27 20:00:35  jalet
50# Typo
51#
52# Revision 1.26  2004/09/27 19:56:27  jalet
53# Added internal handling for PJL queries over port tcp/9100. Now waits
54# for printer being idle before asking, just like with SNMP.
55#
56# Revision 1.25  2004/09/27 09:21:37  jalet
57# Now includes printer's hostname in SNMP error messages
58#
59# Revision 1.24  2004/09/24 21:19:48  jalet
60# Did a pass of PyChecker
61#
62# Revision 1.23  2004/09/23 19:18:12  jalet
63# Now loops when the external hardware accounter fails, until it returns a correct value
64#
65# Revision 1.22  2004/09/22 19:48:01  jalet
66# Logs the looping message as debug instead of as info
67#
68# Revision 1.21  2004/09/22 19:27:41  jalet
69# Bad import statement
70#
71# Revision 1.20  2004/09/22 19:22:27  jalet
72# Just loop in case a network error occur
73#
74# Revision 1.19  2004/09/22 14:29:01  jalet
75# Fixed nasty typo
76#
77# Revision 1.18  2004/09/21 16:00:46  jalet
78# More informational messages
79#
80# Revision 1.17  2004/09/21 13:42:18  jalet
81# Typo
82#
83# Revision 1.16  2004/09/21 13:30:53  jalet
84# First try at full SNMP handling from the Python code.
85#
86# Revision 1.15  2004/09/14 11:38:59  jalet
87# Minor fix
88#
89# Revision 1.14  2004/09/14 06:53:53  jalet
90# Small test added
91#
92# Revision 1.13  2004/09/13 16:02:45  jalet
93# Added fix for incorrect job's size when hardware accounting fails
94#
95# Revision 1.12  2004/09/06 15:42:34  jalet
96# Fix missing import statement for the signal module
97#
98# Revision 1.11  2004/08/31 23:29:53  jalet
99# Introduction of the new 'onaccountererror' configuration directive.
100# Small fix for software accounter's return code which can't be None anymore.
101# Make software and hardware accounting code look similar : will be factorized
102# later.
103#
104# Revision 1.10  2004/08/27 22:49:04  jalet
105# No answer from subprocess now is really a fatal error. Waiting for some
106# time to make this configurable...
107#
108# Revision 1.9  2004/08/25 22:34:39  jalet
109# Now both software and hardware accounting raise an exception when no valid
110# result can be extracted from the subprocess' output.
111# Hardware accounting now reads subprocess' output until an integer is read
112# or data is exhausted : it now behaves just like software accounting in this
113# aspect.
114#
115# Revision 1.8  2004/07/22 22:41:48  jalet
116# Hardware accounting for LPRng should be OK now. UNTESTED.
117#
118# Revision 1.7  2004/07/16 12:22:47  jalet
119# LPRng support early version
120#
121# Revision 1.6  2004/07/01 19:56:42  jalet
122# Better dispatching of error messages
123#
124# Revision 1.5  2004/06/10 22:42:06  jalet
125# Better messages in logs
126#
127# Revision 1.4  2004/05/24 22:45:49  jalet
128# New 'enforcement' directive added
129# Polling loop improvements
130#
131# Revision 1.3  2004/05/24 14:36:40  jalet
132# Revert to old polling loop. Will need optimisations
133#
134# Revision 1.2  2004/05/18 14:49:22  jalet
135# Big code changes to completely remove the need for "requester" directives,
136# jsut use "hardware(... your previous requester directive's content ...)"
137#
138# Revision 1.1  2004/05/13 13:59:30  jalet
139# Code simplifications
140#
141#
142#
143
144import os
145import socket
146import time
147import signal
148import popen2
149
150from pykota.accounter import AccounterBase, PyKotaAccounterError
151
152ITERATIONDELAY = 1.0   # 1 Second
153STABILIZATIONDELAY = 3 # We must read three times the same value to consider it to be stable
154
155try :
156    from pysnmp.asn1.encoding.ber.error import TypeMismatchError
157    from pysnmp.mapping.udp.error import SnmpOverUdpError
158    from pysnmp.mapping.udp.role import Manager
159    from pysnmp.proto.api import alpha
160except ImportError :
161    hasSNMP = 0
162else :   
163    hasSNMP = 1
164    pageCounterOID = ".1.3.6.1.2.1.43.10.2.1.4.1.1"
165    hrPrinterStatusOID = ".1.3.6.1.2.1.25.3.5.1.1.1"
166    printerStatusValues = { 1 : 'other',
167                            2 : 'unknown',
168                            3 : 'idle',
169                            4 : 'printing',
170                            5 : 'warmup',
171                          }
172                         
173    class SNMPAccounter :
174        """A class for SNMP print accounting."""
175        def __init__(self, parent, printerhostname) :
176            self.parent = parent
177            self.printerHostname = printerhostname
178            self.printerInternalPageCounter = self.printerStatus = None
179           
180        def retrieveSNMPValues(self) :   
181            """Retrieves a printer's internal page counter and status via SNMP."""
182            ver = alpha.protoVersions[alpha.protoVersionId1]
183            req = ver.Message()
184            req.apiAlphaSetCommunity('public')
185            req.apiAlphaSetPdu(ver.GetRequestPdu())
186            req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()), (hrPrinterStatusOID, ver.Null()))
187            tsp = Manager()
188            try :
189                tsp.sendAndReceive(req.berEncode(), (self.printerHostname, 161), (self.handleAnswer, req))
190            except SnmpOverUdpError, msg :   
191                self.parent.filter.printInfo(_("Network error while doing SNMP queries on printer %s : %s") % (self.printerHostname, msg), "warn")
192            tsp.close()
193   
194        def handleAnswer(self, wholeMsg, notusedhere, req):
195            """Decodes and handles the SNMP answer."""
196            self.parent.filter.logdebug("SNMP message : '%s'" % repr(wholeMsg))
197            ver = alpha.protoVersions[alpha.protoVersionId1]
198            rsp = ver.Message()
199            try :
200                rsp.berDecode(wholeMsg)
201            except TypeMismatchError, msg :   
202                self.parent.filter.printInfo(_("SNMP message decoding error for printer %s : %s") % (self.printerHostname, msg), "warn")
203            else :
204                if req.apiAlphaMatch(rsp):
205                    errorStatus = rsp.apiAlphaGetPdu().apiAlphaGetErrorStatus()
206                    if errorStatus:
207                        self.parent.filter.printInfo(_("Problem encountered while doing SNMP queries on printer %s : %s") % (self.printerHostname, errorStatus), "warn")
208                    else:
209                        self.values = []
210                        for varBind in rsp.apiAlphaGetPdu().apiAlphaGetVarBindList():
211                            self.values.append(varBind.apiAlphaGetOidVal()[1].rawAsn1Value)
212                        try :   
213                            # keep maximum value seen for printer's internal page counter
214                            self.printerInternalPageCounter = max(self.printerInternalPageCounter, self.values[0])
215                            self.printerStatus = self.values[1]
216                        except IndexError :   
217                            pass
218                        else :   
219                            return 1
220                       
221        def waitPrinting(self) :
222            """Waits for printer status being 'printing'."""
223            while 1:
224                self.retrieveSNMPValues()
225                statusAsString = printerStatusValues.get(self.printerStatus)
226                if statusAsString in ('printing',) :
227                    break
228                self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.printername)   
229                time.sleep(ITERATIONDELAY)
230           
231        def waitIdle(self) :
232            """Waits for printer status being 'idle'."""
233            idle_num = idle_flag = 0
234            while 1 :
235                self.retrieveSNMPValues()
236                statusAsString = printerStatusValues.get(self.printerStatus)
237                idle_flag = 0
238                if statusAsString in ('idle',) :
239                    idle_flag = 1
240                if idle_flag :   
241                    idle_num += 1
242                    if idle_num > STABILIZATIONDELAY :
243                        # printer status is stable, we can exit
244                        break
245                else :   
246                    idle_num = 0
247                self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.printername)   
248                time.sleep(ITERATIONDELAY)
249               
250pjlMessage = "\033%-12345X@PJL USTATUSOFF\r\n@PJL INFO STATUS\r\n@PJL INFO PAGECOUNT\r\n\033%-12345X"
251pjlStatusValues = {
252                    "10000" : "Powersave Mode",
253                    "10001" : "Ready Online",
254                    "10002" : "Ready Offline",
255                    "10003" : "Warming Up",
256                    "10004" : "Self Test",
257                    "10005" : "Reset",
258                    "10023" : "Printing",
259                    "35078" : "Powersave Mode",         # 10000 is ALSO powersave !!!
260                  }
261class PJLAccounter :
262    """A class for PJL print accounting."""
263    def __init__(self, parent, printerhostname) :
264        self.parent = parent
265        self.printerHostname = printerhostname
266        self.printerInternalPageCounter = self.printerStatus = None
267        self.printerInternalPageCounter = self.printerStatus = None
268        self.timedout = 0
269       
270    def alarmHandler(self, signum, frame) :   
271        """Query has timedout, handle this."""
272        self.timedout = 1
273        raise IOError, "Waiting for PJL answer timed out. Please try again later."
274       
275    def retrievePJLValues(self) :   
276        """Retrieves a printer's internal page counter and status via PJL."""
277        port = 9100
278        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
279        try :
280            sock.connect((self.printerHostname, port))
281        except socket.error, msg :
282            self.parent.filter.printInfo(_("Problem during connection to %s:%s : %s") % (self.printerHostname, port, msg), "warn")
283        else :
284            try :
285                sock.send(pjlMessage)
286            except socket.error, msg :
287                self.parent.filter.printInfo(_("Problem while sending PJL query to %s:%s : %s") % (self.printerHostname, port, msg), "warn")
288            else :   
289                actualpagecount = self.printerStatus = None
290                self.timedout = 0
291                while (self.timedout == 0) or (actualpagecount is None) or (self.printerStatus is None) :
292                    signal.signal(signal.SIGALRM, self.alarmHandler)
293                    signal.alarm(3)
294                    try :
295                        answer = sock.recv(1024)
296                    except IOError, msg :   
297                        break   # our alarm handler was launched, probably
298                    else :   
299                        readnext = 0
300                        for line in [l.strip() for l in answer.split()] : 
301                            if line.startswith("CODE=") :
302                                self.printerStatus = line.split("=")[1]
303                            elif line.startswith("PAGECOUNT") :   
304                                readnext = 1 # page counter is on next line
305                            elif readnext :   
306                                actualpagecount = int(line.strip())
307                                readnext = 0
308                    signal.alarm(0)
309                self.printerInternalPageCounter = max(actualpagecount, self.printerInternalPageCounter)
310        sock.close()
311       
312    def waitPrinting(self) :
313        """Waits for printer status being 'printing'."""
314        while 1:
315            self.retrievePJLValues()
316            if self.printerStatus in ('10023',) :
317                break
318            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.printername)
319            time.sleep(ITERATIONDELAY)
320       
321    def waitIdle(self) :
322        """Waits for printer status being 'idle'."""
323        idle_num = idle_flag = 0
324        while 1 :
325            self.retrievePJLValues()
326            idle_flag = 0
327            if self.printerStatus in ('10000', '10001', '35078') :
328                idle_flag = 1
329            if idle_flag :   
330                idle_num += 1
331                if idle_num > STABILIZATIONDELAY :
332                    # printer status is stable, we can exit
333                    break
334            else :   
335                idle_num = 0
336            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.printername)
337            time.sleep(ITERATIONDELAY)
338   
339class Accounter(AccounterBase) :
340    def __init__(self, kotabackend, arguments) :
341        """Initializes querying accounter."""
342        AccounterBase.__init__(self, kotabackend, arguments)
343        self.isSoftware = 0
344       
345    def getPrinterInternalPageCounter(self) :   
346        """Returns the printer's internal page counter."""
347        self.filter.logdebug("Reading printer %s's internal page counter..." % self.filter.printername)
348        counter = self.askPrinterPageCounter(self.filter.printerhostname)
349        self.filter.logdebug("Printer %s's internal page counter value is : %s" % (self.filter.printername, str(counter)))
350        return counter   
351       
352    def beginJob(self, printer) :   
353        """Saves printer internal page counter at start of job."""
354        # save page counter before job
355        self.LastPageCounter = self.getPrinterInternalPageCounter()
356        self.fakeBeginJob()
357       
358    def fakeBeginJob(self) :   
359        """Fakes a begining of a job."""
360        self.counterbefore = self.getLastPageCounter()
361       
362    def endJob(self, printer) :   
363        """Saves printer internal page counter at end of job."""
364        # save page counter after job
365        self.LastPageCounter = self.counterafter = self.getPrinterInternalPageCounter()
366       
367    def getJobSize(self, printer) :   
368        """Returns the actual job size."""
369        if (not self.counterbefore) or (not self.counterafter) :
370            # there was a problem retrieving page counter
371            self.filter.printInfo(_("A problem occured while reading printer %s's internal page counter.") % printer.Name, "warn")
372            if printer.LastJob.Exists :
373                # if there's a previous job, use the last value from database
374                self.filter.printInfo(_("Retrieving printer %s's page counter from database instead.") % printer.Name, "warn")
375                if not self.counterbefore : 
376                    self.counterbefore = printer.LastJob.PrinterPageCounter or 0
377                if not self.counterafter :
378                    self.counterafter = printer.LastJob.PrinterPageCounter or 0
379                before = min(self.counterbefore, self.counterafter)   
380                after = max(self.counterbefore, self.counterafter)   
381                self.counterbefore = before
382                self.counterafter = after
383                if (not self.counterbefore) or (not self.counterafter) or (self.counterbefore == self.counterafter) :
384                    self.filter.printInfo(_("Couldn't retrieve printer %s's internal page counter either before or after printing.") % printer.Name, "warn")
385                    self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
386                    self.counterbefore = 0
387                    self.counterafter = 1
388            else :
389                self.filter.printInfo(_("No previous job in database for printer %s.") % printer.Name, "warn")
390                self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
391                self.counterbefore = 0
392                self.counterafter = 1
393               
394        jobsize = (self.counterafter - self.counterbefore)   
395        if jobsize < 0 :
396            # Try to take care of HP printers
397            # Their internal page counter is saved to NVRAM
398            # only every 10 pages. If the printer was switched
399            # off then back on during the job, and that the
400            # counters difference is negative, we know
401            # the formula (we can't know if more than eleven
402            # pages were printed though) :
403            if jobsize > -10 :
404                jobsize += 10
405            else :   
406                # here we may have got a printer being replaced
407                # DURING the job. This is HIGHLY improbable !
408                self.filter.printInfo(_("Inconsistent values for printer %s's internal page counter.") % printer.Name, "warn")
409                self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
410                jobsize = 1
411        return jobsize
412       
413    def askPrinterPageCounter(self, printer) :
414        """Returns the page counter from the printer via an external command.
415       
416           The external command must report the life time page number of the printer on stdout.
417        """
418        commandline = self.arguments.strip() % locals()
419        cmdlower = commandline.lower()
420        if cmdlower == "snmp" :
421            if hasSNMP :
422                return self.askWithSNMP(printer)
423            else :   
424                raise PyKotaAccounterError, _("Internal SNMP accounting asked, but Python-SNMP is not available. Please download it from http://pysnmp.sourceforge.net")
425        elif cmdlower == "pjl" :
426            return self.askWithPJL(printer)
427           
428        if printer is None :
429            raise PyKotaAccounterError, _("Unknown printer address in HARDWARE(%s) for printer %s") % (commandline, self.filter.printername)
430        while 1 :   
431            self.filter.printInfo(_("Launching HARDWARE(%s)...") % commandline)
432            pagecounter = None
433            child = popen2.Popen4(commandline)   
434            try :
435                answer = child.fromchild.read()
436            except IOError :   
437                # we were interrupted by a signal, certainely a SIGTERM
438                # caused by the user cancelling the current job
439                try :
440                    os.kill(child.pid, signal.SIGTERM)
441                except :   
442                    pass # already killed ?
443                self.filter.printInfo(_("SIGTERM was sent to hardware accounter %s (pid: %s)") % (commandline, child.pid))
444            else :   
445                lines = [l.strip() for l in answer.split("\n")]
446                for i in range(len(lines)) : 
447                    try :
448                        pagecounter = int(lines[i])
449                    except (AttributeError, ValueError) :
450                        self.filter.printInfo(_("Line [%s] skipped in accounter's output. Trying again...") % lines[i])
451                    else :   
452                        break
453            child.fromchild.close()   
454            child.tochild.close()
455            try :
456                status = child.wait()
457            except OSError, msg :   
458                self.filter.logdebug("Error while waiting for hardware accounter pid %s : %s" % (child.pid, msg))
459            else :   
460                if os.WIFEXITED(status) :
461                    status = os.WEXITSTATUS(status)
462                self.filter.printInfo(_("Hardware accounter %s exit code is %s") % (self.arguments, str(status)))
463               
464            if pagecounter is None :
465                message = _("Unable to query printer %s via HARDWARE(%s)") % (printer, commandline)
466                if self.onerror == "CONTINUE" :
467                    self.filter.printInfo(message, "error")
468                else :
469                    raise PyKotaAccounterError, message 
470            else :       
471                return pagecounter       
472       
473    def askWithSNMP(self, printer) :
474        """Returns the page counter from the printer via internal SNMP handling."""
475        acc = SNMPAccounter(self, printer)
476        try :
477            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
478               (os.environ.get("PYKOTAACTION") != "DENY") and \
479               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
480               self.filter.jobSizeBytes :
481                acc.waitPrinting()
482            acc.waitIdle()   
483        except :   
484            if acc.printerInternalPageCounter is None :
485                raise
486            else :   
487                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")
488        return acc.printerInternalPageCounter
489       
490    def askWithPJL(self, printer) :
491        """Returns the page counter from the printer via internal PJL handling."""
492        acc = PJLAccounter(self, printer)
493        try :
494            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
495               (os.environ.get("PYKOTAACTION") != "DENY") and \
496               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
497               self.filter.jobSizeBytes :
498                acc.waitPrinting()
499            acc.waitIdle()   
500        except :   
501            if acc.printerInternalPageCounter is None :
502                raise
503            else :   
504                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")
505        return acc.printerInternalPageCounter
Note: See TracBrowser for help on using the browser.