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

Revision 1896, 21.2 kB (checked in by jalet, 20 years ago)

Fix for unneeded out of band status in pjl_over_tcp/9100

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