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

Revision 1749, 21.1 kB (checked in by jalet, 20 years ago)

Lowered timeout delay for PJL queries

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