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

Revision 1933, 21.4 kB (checked in by jalet, 19 years ago)

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