root / pykota / trunk / bin / cupspykota @ 2057

Revision 2057, 40.8 kB (checked in by jalet, 19 years ago)

Added the md5sum to the history

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
RevLine 
[1177]1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
4# CUPSPyKota accounting backend
5#
6# PyKota - Print Quotas for CUPS and LPRng
7#
[2028]8# (c) 2003, 2004, 2005 Jerome Alet <alet@librelogiciel.com>
[1177]9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
22#
23# $Id$
24#
25# $Log$
[2057]26# Revision 1.86  2005/02/13 22:48:37  jalet
27# Added the md5sum to the history
28#
[2054]29# Revision 1.85  2005/02/13 22:02:28  jalet
30# Big database structure changes. Upgrade script is now included as well as
31# the new LDAP schema.
32# Introduction of the -o | --overcharge command line option to edpykota.
33# The output of repykota is more complete, but doesn't fit in 80 columns anymore.
34# Introduction of the new 'maxdenybanners' directive.
35#
[2028]36# Revision 1.84  2005/01/17 08:44:23  jalet
37# Modified copyright years
38#
[2007]39# Revision 1.83  2005/01/06 23:23:35  jalet
40# Regain priviledge the time to read the job control file to extract the client
41# hostname
42#
[2006]43# Revision 1.82  2005/01/06 22:52:53  jalet
44# Implemented the dropping of priviledges. Beware, beware...
45#
[1974]46# Revision 1.81  2004/12/07 16:54:02  jalet
47# Now logs as errors differences between computed and precomputed job's sizes
48#
[1924]49# Revision 1.80  2004/11/15 22:12:46  jalet
50# Fix for the Fix !!!
51#
[1923]52# Revision 1.79  2004/11/15 22:01:34  jalet
53# Improved banner handling.
54# Fix for raw printing and banners.
55#
[1918]56# Revision 1.78  2004/11/15 19:59:34  jalet
57# PyKota banners now basically work !
58#
[1902]59# Revision 1.77  2004/11/06 22:40:57  jalet
60# Safer code
61#
[1901]62# Revision 1.76  2004/11/06 22:35:58  jalet
63# Added a miniparser for IPP messages (RFC 2910). The job-originating-host-name
64# retrieval is now fiable, unless the CUPS developpers change something...
65#
[1897]66# Revision 1.75  2004/11/01 15:45:25  jalet
67# Added many debug messages.
68# Added some code to prevent short writes.
69#
[1820]70# Revision 1.74  2004/10/13 20:51:27  jalet
71# Made debugging levels be the same in cupspykota and lprngpykota.
72# Now outputs more information in informational messages : user, printer, jobid
73#
[1819]74# Revision 1.73  2004/10/13 16:56:45  jalet
75# Added a space to the pattern to differentiate jobs which id begins with
76# the same digits, like jobs 87 and 879 for example : if printed by same
77# user on same printer, but from a different host this could have broken
78# the result. (In reality this couldn't happen because 879 would be the
79# last line to match anyway because of job ordering, but we never know
80# if the page_log file gets corrupt somewhat)
81#
[1713]82# Revision 1.72  2004/09/13 16:02:44  jalet
83# Added fix for incorrect job's size when hardware accounting fails
84#
[1704]85# Revision 1.71  2004/09/06 17:05:06  jalet
86# Fix for autodetection of SC_OPEN_MAX
87#
[1643]88# Revision 1.70  2004/07/26 09:20:27  jalet
89# Unneeded module
90#
[1624]91# Revision 1.69  2004/07/22 22:41:47  jalet
92# Hardware accounting for LPRng should be OK now. UNTESTED.
93#
[1606]94# Revision 1.68  2004/07/20 22:19:44  jalet
95# Sanitized a bit + use of gettext
96#
[1600]97# Revision 1.67  2004/07/16 12:22:45  jalet
98# LPRng support early version
99#
[1584]100# Revision 1.66  2004/07/01 19:56:25  jalet
101# Better dispatching of error messages
102#
[1562]103# Revision 1.65  2004/06/22 09:31:17  jalet
104# Always send some debug info to CUPS' back channel stream (stderr) as
105# informationnal messages.
106#
[1546]107# Revision 1.64  2004/06/18 13:34:46  jalet
108# Now all tracebacks include PyKota's version number
109#
[1542]110# Revision 1.63  2004/06/17 13:26:50  jalet
111# Better exception handling code
112#
[1541]113# Revision 1.62  2004/06/16 20:56:34  jalet
114# Smarter initialisation code
115#
[1530]116# Revision 1.61  2004/06/08 09:00:04  jalet
117# Fixed problem when username was passed in uppercase from Samba and we
118# tried to find correct line in CUPS page_log to extract the hostname.
119#
[1520]120# Revision 1.60  2004/06/03 23:14:08  jalet
121# Now stores the job's size in bytes in the database.
122# Preliminary work on payments storage : database schemas are OK now,
123# but no code to store payments yet.
124# Removed schema picture, not relevant anymore.
125#
[1519]126# Revision 1.59  2004/06/03 22:12:53  jalet
127# Now denies empty jobs
128#
[1517]129# Revision 1.58  2004/06/03 21:50:33  jalet
130# Improved error logging.
131# crashrecipient directive added.
132# Now exports the job's size in bytes too.
133#
[1515]134# Revision 1.57  2004/06/02 22:18:07  jalet
135# I think the bug when cancelling jobs should be fixed right now
136#
[1514]137# Revision 1.56  2004/06/02 21:50:56  jalet
138# Moved the sigterm capturing elsewhere
139#
[1513]140# Revision 1.55  2004/06/02 14:25:07  jalet
141# Should correctly capture ALL errors now
142#
[1503]143# Revision 1.54  2004/05/26 16:44:48  jalet
144# Now logs something when client hostname can't be extracted
145#
[1502]146# Revision 1.53  2004/05/26 14:49:35  jalet
147# First try at saving the job-originating-hostname in the database
148#
[1499]149# Revision 1.52  2004/05/25 09:15:13  jalet
150# accounter.py : old code deleted
151# the rest : now exports PYKOTAPRECOMPUTEDJOBSIZE and PYKOTAPRECOMPUTEDJOBPRICE
152#
[1498]153# Revision 1.51  2004/05/25 08:31:16  jalet
154# Heavy CPU usage seems to be fixed at least !
155#
[1497]156# Revision 1.50  2004/05/25 05:17:50  jalet
157# Now precomputes the job's size only if current printer's enforcement
158# is "STRICT"
159#
[1495]160# Revision 1.49  2004/05/24 22:45:48  jalet
161# New 'enforcement' directive added
162# Polling loop improvements
163#
[1494]164# Revision 1.48  2004/05/24 14:36:24  jalet
165# Revert to old polling loop. Will need optimisations
166#
[1493]167# Revision 1.47  2004/05/24 11:59:46  jalet
168# More robust (?) code
169#
[1492]170# Revision 1.46  2004/05/21 22:02:51  jalet
171# Preliminary work on pre-accounting
172#
[1484]173# Revision 1.45  2004/05/19 07:15:32  jalet
174# Could the 'misterious' bug in my loop be finally fixed ???
175#
[1483]176# Revision 1.44  2004/05/18 14:48:47  jalet
177# Big code changes to completely remove the need for "requester" directives,
178# jsut use "hardware(... your previous requester directive's content ...)"
179#
[1478]180# Revision 1.43  2004/05/17 11:46:05  jalet
181# First try at cupspykota's main loop rewrite
182#
[1467]183# Revision 1.42  2004/05/10 11:22:28  jalet
184# Typo
185#
[1466]186# Revision 1.41  2004/05/10 10:07:30  jalet
187# Catches OSError while reading
188#
[1465]189# Revision 1.40  2004/05/10 09:29:48  jalet
190# Should be more robust if we receive a SIGTERM during an I/O operation
191#
[1458]192# Revision 1.39  2004/05/07 14:44:53  jalet
193# Fix for file handles unregistered twice from the polling object
194#
[1433]195# Revision 1.38  2004/04/09 22:24:46  jalet
196# Began work on correct handling of child processes when jobs are cancelled by
197# the user. Especially important when an external requester is running for a
198# long time.
199#
[1411]200# Revision 1.37  2004/03/18 19:11:25  jalet
201# Fix for raw jobs in cupspykota
202#
[1410]203# Revision 1.36  2004/03/18 14:03:18  jalet
204# Added fsync() calls
205#
[1405]206# Revision 1.35  2004/03/16 12:05:01  jalet
207# Small fix for new waitprinter.sh : when job was denied, would wait forever
208# for printer being in printing mode.
209#
[1400]210# Revision 1.34  2004/03/15 10:47:56  jalet
211# This time the traceback formatting should be correct !
212#
[1391]213# Revision 1.33  2004/03/05 12:46:07  jalet
214# Improve tracebacks
215#
[1390]216# Revision 1.32  2004/03/05 12:31:35  jalet
217# Now should output full traceback when crashing
218#
[1375]219# Revision 1.31  2004/03/01 14:35:56  jalet
220# PYKOTAPHASE wasn't set soon enough at the start of the job
221#
[1374]222# Revision 1.30  2004/03/01 14:34:15  jalet
223# PYKOTAPHASE wasn't set at the right time at the end of data transmission
224# to underlying layer (real backend)
225#
[1372]226# Revision 1.29  2004/03/01 11:23:25  jalet
227# Pre and Post hooks to external commands are available in the cupspykota
228# backend. Forthe pykota filter they will be implemented real soon now.
229#
[1365]230# Revision 1.28  2004/02/26 14:18:07  jalet
231# Should fix the remaining bugs wrt printers groups and users groups.
232#
[1335]233# Revision 1.27  2004/02/04 23:41:27  jalet
234# Should fix the incorrect "backend died abnormally" problem.
235#
[1321]236# Revision 1.26  2004/01/30 16:35:03  jalet
237# Fixes stupid software accounting bug in CUPS backend
238#
[1302]239# Revision 1.25  2004/01/16 17:51:46  jalet
240# Fuck Fuck Fuck !!!
241#
[1291]242# Revision 1.24  2004/01/14 15:52:01  jalet
243# Small fix for job cancelling code.
244#
[1289]245# Revision 1.23  2004/01/13 10:48:28  jalet
246# Small streams polling loop modification.
247#
[1285]248# Revision 1.22  2004/01/12 22:43:40  jalet
249# New formula to compute a job's price
250#
[1280]251# Revision 1.21  2004/01/12 18:17:36  jalet
252# Denied jobs weren't stored into the history anymore, this is now fixed.
253#
[1271]254# Revision 1.20  2004/01/11 23:22:42  jalet
255# Major code refactoring, it's way cleaner, and now allows automated addition
256# of printers on first print.
257#
[1257]258# Revision 1.19  2004/01/08 14:10:32  jalet
259# Copyright year changed.
260#
[1256]261# Revision 1.18  2004/01/07 16:16:32  jalet
262# Better debugging information
263#
[1240]264# Revision 1.17  2003/12/27 16:49:25  uid67467
265# Should be ok now.
266#
267# Revision 1.17  2003/12/06 08:54:29  jalet
268# Code simplifications.
269# Added many debugging messages.
270#
[1222]271# Revision 1.16  2003/11/26 20:43:29  jalet
272# Inadvertantly introduced a bug, which is fixed.
273#
[1221]274# Revision 1.15  2003/11/26 19:17:35  jalet
275# Printing on a printer not present in the Quota Storage now results
276# in the job being stopped or cancelled depending on the system.
277#
[1210]278# Revision 1.14  2003/11/25 13:25:45  jalet
279# Boolean problem with old Python, replaced with 0
280#
[1203]281# Revision 1.13  2003/11/23 19:01:35  jalet
282# Job price added to history
283#
[1200]284# Revision 1.12  2003/11/21 14:28:43  jalet
285# More complete job history.
286#
[1196]287# Revision 1.11  2003/11/19 23:19:35  jalet
288# Code refactoring work.
289# Explicit redirection to /dev/null has to be set in external policy now, just
290# like in external mailto.
291#
[1191]292# Revision 1.10  2003/11/18 17:54:24  jalet
293# SIGTERMs are now transmitted to original backends.
294#
[1190]295# Revision 1.9  2003/11/18 14:11:07  jalet
296# Small fix for bizarre urls
297#
[1186]298# Revision 1.8  2003/11/15 14:26:44  jalet
299# General improvements to the documentation.
300# Email address changed in sample configuration file, because
301# I receive low quota messages almost every day...
302#
[1185]303# Revision 1.7  2003/11/14 22:05:12  jalet
304# New CUPS backend fully functionnal.
305# Old CUPS configuration method is now officially deprecated.
306#
[1184]307# Revision 1.6  2003/11/14 20:13:11  jalet
308# We exit the loop too soon.
309#
[1183]310# Revision 1.5  2003/11/14 18:31:27  jalet
311# Not perfect, but seems to work with the poll() loop.
312#
[1182]313# Revision 1.4  2003/11/14 17:04:15  jalet
314# More (untested) work on the CUPS backend.
315#
[1180]316# Revision 1.3  2003/11/12 23:27:44  jalet
317# More work on new backend. This commit may be unstable.
318#
[1178]319# Revision 1.2  2003/11/12 09:33:34  jalet
320# New CUPS backend supports device enumeration
321#
[1177]322# Revision 1.1  2003/11/08 16:05:31  jalet
323# CUPS backend added for people to experiment.
324#
325#
326#
327
328import sys
329import os
[1182]330import popen2
[1178]331import cStringIO
332import shlex
[1182]333import select
334import signal
[1291]335import time
[1177]336
[1546]337from pykota.tool import PyKotaFilterOrBackend, PyKotaToolError, crashed
[1177]338from pykota.config import PyKotaConfigError
339from pykota.storage import PyKotaStorageError
[1196]340from pykota.accounter import PyKotaAccounterError
[1901]341from pykota.ipp import IPPMessage, PyKotaIPPError
[1271]342   
[1478]343class PyKotaPopen4(popen2.Popen4) :
[1182]344    """Our own class to execute real backends.
345   
346       Their first argument is different from their path so using
347       native popen2.Popen3 would not be feasible.
348    """
[1478]349    def __init__(self, cmd, bufsize=-1, arg0=None) :
[1182]350        self.arg0 = arg0
[1478]351        popen2.Popen4.__init__(self, cmd, bufsize)
[1182]352       
353    def _run_child(self, cmd):
[1704]354        try :
355            MAXFD = os.sysconf("SC_OPEN_MAX")
356        except (AttributeError, ValueError) :   
357            MAXFD = 256
358        for i in range(3, MAXFD) : 
[1182]359            try:
360                os.close(i)
361            except OSError:
362                pass
363        try:
364            os.execvpe(cmd[0], [self.arg0 or cmd[0]] + cmd[1:], os.environ)
365        finally:
366            os._exit(1)
367   
[1271]368class PyKotaBackend(PyKotaFilterOrBackend) :       
369    """A class for the pykota backend."""
370    def acceptJob(self) :       
371        """Returns the appropriate exit code to tell CUPS all is OK."""
372        return 0
373           
374    def removeJob(self) :           
375        """Returns the appropriate exit code to let CUPS think all is OK.
[1177]376       
[1271]377           Returning 0 (success) prevents CUPS from stopping the print queue.
378        """   
379        return 0
[1222]380       
[1901]381    def getCupsConfigDirectives(self, directives=[]) :
[1902]382        """Retrieves some CUPS directives from its configuration file.
383       
384           Returns a mapping with lowercased directives as keys and
385           their setting as values.
386        """
[1901]387        dirvalues = {} 
[1502]388        cupsroot = os.environ.get("CUPS_SERVERROOT", "/etc/cups")
389        cupsdconf = os.path.join(cupsroot, "cupsd.conf")
390        try :
391            conffile = open(cupsdconf, "r")
392        except IOError :   
[1503]393            self.logdebug("Unable to open %s" % cupsdconf)
[1502]394        else :   
395            for line in conffile.readlines() :
396                linecopy = line.strip().lower()
[1901]397                for di in [d.lower() for d in directives] :
398                    if linecopy.startswith("%s " % di) :
399                        try :
400                            val = line.split()[1]
401                        except :   
402                            pass # ignore errors, we take the last value in any case.
403                        else :   
404                            dirvalues[di] = val
[1502]405            conffile.close()           
[1901]406        return dirvalues       
[1502]407           
[1901]408    def getJobOriginatingHostnameFromPageLog(self, cupsconfig, printername, username, jobid) :
[1502]409        """Retrieves the job-originating-hostname from the CUPS page_log file if possible."""
[1901]410        pagelogpath = cupsconfig.get("pagelog", "/var/log/cups/page_log")
411        self.logdebug("Trying to extract job-originating-host-name from %s" % pagelogpath)
[1502]412        try :
413            pagelog = open(pagelogpath, "r")
414        except IOError :   
[1503]415            self.logdebug("Unable to open %s" % pagelogpath)
[1502]416            return # no page log or can't read it, originating hostname unknown yet
417        else :   
418            # TODO : read backward so we could take first value seen
419            # TODO : here we read forward so we must take the last value seen
[1819]420            prefix = ("%s %s %s " % (printername, username, jobid)).lower()
[1502]421            matchingline = None
422            while 1 :
423                line = pagelog.readline()
424                if not line :
425                    break
426                else :
427                    line = line.strip()
[1530]428                    if line.lower().startswith(prefix) :   
[1502]429                        matchingline = line # no break, because we read forward
430            pagelog.close()       
431            if matchingline is None :
[1606]432                self.logdebug("No matching line found in %s" % pagelogpath)
[1901]433                return # correct line not found, job-originating-host-name unknown
[1502]434            else :   
435                return matchingline.split()[-1]
436               
[1271]437    def doWork(self, policy, printer, user, userpquota) :   
438        """Most of the work is done here."""
439        # Two different values possible for policy here :
440        # ALLOW means : Either printer, user or user print quota doesn't exist,
441        #               but the job should be allowed anyway.
442        # OK means : Both printer, user and user print quota exist, job should
443        #            be allowed if current user is allowed to print on this printer
444        if policy == "OK" :
[1372]445            # exports user information with initial values
446            self.exportUserInfo(userpquota)
447           
[1901]448            # tries to extract job-originating-host-name and other information
449            cupsdconf = self.getCupsConfigDirectives(["PageLog", "RequestRoot"])
450            requestroot = cupsdconf.get("requestroot", "/var/spool/cups")
451            if (len(self.jobid) < 5) and self.jobid.isdigit() :
452                ippmessagefile = "c%05i" % int(self.jobid)
453            else :   
454                ippmessagefile = "c%s" % self.jobid
455            ippmessagefile = os.path.join(requestroot, ippmessagefile)
456            ippmessage = {}
[2007]457            self.regainPriv()
[1901]458            try :
459                ippdatafile = open(ippmessagefile)
460            except :   
461                self.printInfo("Unable to open IPP message file %s" % ippmessagefile, "warn")
462            else :   
463                self.logdebug("Parsing of IPP message file %s begins." % ippmessagefile)
[1902]464                try :
465                    ippmessage = IPPMessage(ippdatafile.read())
466                except PyKotaIPPError, msg :   
467                    self.printInfo("Error while parsing %s : %s" % (ippmessagefile, msg), "warn")
468                else :   
469                    self.logdebug("Parsing of IPP message file %s ends." % ippmessagefile)
[1901]470                ippdatafile.close()
[2007]471            self.dropPriv()   
[1901]472            clienthost = ippmessage.get("job-originating-host-name") \
473                         or self.getJobOriginatingHostnameFromPageLog(cupsdconf, printer.Name, user.Name, self.jobid)
[1503]474            self.logdebug("Client Hostname : %s" % (clienthost or "Unknown"))   
[1517]475            os.environ["PYKOTAJOBORIGINATINGHOSTNAME"] = str(clienthost or "")
[1502]476           
[1901]477            # TODO : extract username (double check ?) and billing code too
478           
[1375]479            # enters first phase
[1517]480            os.environ["PYKOTAPHASE"] = "BEFORE"
[1375]481           
[1519]482            # do we want strict or laxist quota enforcement ?
[1497]483            if self.config.getPrinterEnforcement(printer.Name) == "STRICT" :
484                self.softwareJobSize = self.precomputeJobSize()
485                self.softwareJobPrice = userpquota.computeJobPrice(self.softwareJobSize)
[1606]486                self.logdebug("Precomputed job's size is %s pages, price is %s units" % (self.softwareJobSize, self.softwareJobPrice))
[1517]487            os.environ["PYKOTAPRECOMPUTEDJOBSIZE"] = str(self.softwareJobSize)
488            os.environ["PYKOTAPRECOMPUTEDJOBPRICE"] = str(self.softwareJobPrice)
[1519]489           
490            # if no data to pass to real backend, probably a filter
491            # higher in the chain failed because of a misconfiguration.
492            # we deny the job in this case (nothing to print anyway)
493            if not self.jobSizeBytes :
[1820]494                self.printMoreInfo(user, printer, _("Job contains no data. Printing is denied."), "warn")
[1519]495                action = "DENY"
[1606]496            else :   
497                # checks the user's quota
498                action = self.warnUserPQuota(userpquota)
[1519]499           
[1372]500            # exports some new environment variables
[1517]501            os.environ["PYKOTAACTION"] = action
[1372]502           
503            # launches the pre hook
504            self.prehook(userpquota)
[1918]505
506            # saves the size of banners which have to be accounted for
507            # this is needed in the case of software accounting
508            bannersize = 0
[1372]509           
[1918]510            # handle starting banner pages before accounting
511            accountbanner = self.config.getAccountBanner(printer.Name)
512            if accountbanner in ["ENDING", "NONE"] :
[2054]513                if (action == 'DENY') and (userpquota.WarnCount < self.config.getMaxDenyBanners()) :
514                    self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn")
515                else :
516                    if action == 'DENY' :
517                        userpquota.warn() # increments the warning counter
518                        self.exportUserInfo(userpquota)
519                    banner = self.startingBanner(printer.Name)
520                    if banner :
521                        self.logdebug("Printing starting banner before accounting begins.")
522                        self.handleData(banner)
[1918]523 
[1820]524            self.printMoreInfo(user, printer, _("Job accounting begins."))
[1624]525            self.accounter.beginJob(printer)
[1918]526           
527            # handle starting banner pages during accounting
528            if accountbanner in ["STARTING", "BOTH"] :
[2054]529                if (action == 'DENY') and (userpquota.WarnCount < self.config.getMaxDenyBanners()) :
530                    self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn")
531                else :
532                    if action == 'DENY' :
533                        userpquota.warn() # increments the warning counter
534                        self.exportUserInfo(userpquota)
535                    banner = self.startingBanner(printer.Name)
536                    if banner :
537                        self.logdebug("Printing starting banner during accounting.")
538                        self.handleData(banner)
539                        if self.accounter.isSoftware :
540                            bannersize += 1 # TODO : fix this by passing the banner's content through PDLAnalyzer
[1280]541        else :   
542            action = "ALLOW"
[1517]543            os.environ["PYKOTAACTION"] = action
[1271]544           
545        # pass the job's data to the real backend   
[1280]546        if action in ["ALLOW", "WARN"] :
[1291]547            if self.gotSigTerm :
[1280]548                retcode = self.removeJob()
549            else :   
550                retcode = self.handleData()       
551        else :       
[1271]552            retcode = self.removeJob()
553       
554        if policy == "OK" :       
[1374]555            # indicate phase change
[1517]556            os.environ["PYKOTAPHASE"] = "AFTER"
[1374]557           
[1918]558            # handle ending banner pages during accounting
559            if accountbanner in ["ENDING", "BOTH"] :
[2054]560                if (action == 'DENY') and (userpquota.WarnCount < self.config.getMaxDenyBanners()) :
561                    self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn")
562                else :
563                    if action == 'DENY' :
564                        userpquota.warn() # increments the warning counter
565                        self.exportUserInfo(userpquota)
566                    banner = self.endingBanner(printer.Name)
567                    if banner :
568                        self.logdebug("Printing ending banner during accounting.")
569                        self.handleData(banner)
570                        if self.accounter.isSoftware :
571                            bannersize += 1 # TODO : fix this by passing the banner's content through PDLAnalyzer
[1918]572 
[1271]573            # stops accounting.
[1624]574            self.accounter.endJob(printer)
[1820]575            self.printMoreInfo(user, printer, _("Job accounting ends."))
[1271]576               
577            # retrieve the job size   
[1321]578            if action == "DENY" :
579                jobsize = 0
[1820]580                self.printMoreInfo(user, printer, _("Job size forced to 0 because printing is denied."))
[1321]581            else :   
[2007]582                jobsize = self.accounter.getJobSize(printer)
[1974]583                if self.softwareJobSize and (jobsize != self.softwareJobSize) :
584                    self.printInfo(_("Beware : computed job size (%s) != precomputed job size (%s)") % (jobsize, self.softwareJobSize), "error")
[2007]585                jobsize += bannersize   
[1820]586            self.printMoreInfo(user, printer, _("Job size : %i") % jobsize)
[1271]587           
588            # update the quota for the current user on this printer
[1606]589            self.printInfo(_("Updating user %s's quota on printer %s") % (user.Name, printer.Name))
[1285]590            jobprice = userpquota.increasePagesUsage(jobsize)
[1271]591           
592            # adds the current job to history   
[2057]593            printer.addJobToHistory(self.jobid, user, self.accounter.getLastPageCounter(), \
594                                    action, jobsize, jobprice, self.preserveinputfile, \
595                                    self.title, self.copies, self.options, clienthost, \
596                                    self.jobSizeBytes, self.checksum)
[1820]597            self.printMoreInfo(user, printer, _("Job added to history."))
[1271]598           
[1372]599            # exports some new environment variables
[1517]600            os.environ["PYKOTAJOBSIZE"] = str(jobsize)
601            os.environ["PYKOTAJOBPRICE"] = str(jobprice)
[1372]602           
[1517]603            # then re-export user information with new value
[1372]604            self.exportUserInfo(userpquota)
605           
[1923]606            # handle ending banner pages after accounting ends
607            if accountbanner in ["STARTING", "NONE"] :
[2054]608                if (action == 'DENY') and (userpquota.WarnCount < self.config.getMaxDenyBanners()) :
609                    self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn")
610                else :
611                    if action == 'DENY' :
612                        userpquota.warn() # increments the warning counter
613                        self.exportUserInfo(userpquota)
614                    banner = self.endingBanner(printer.Name)
615                    if banner :
616                        self.logdebug("Printing ending banner after accounting ends.")
617                        self.handleData(banner)
618                       
[1372]619            # Launches the post hook
620            self.posthook(userpquota)
621           
[1271]622        return retcode   
[1478]623               
[1458]624    def unregisterFileNo(self, pollobj, fileno) :               
625        """Removes a file handle from the polling object."""
626        try :
627            pollobj.unregister(fileno)
628        except KeyError :   
[1584]629            self.printInfo(_("File number %s unregistered twice from polling object, ignored.") % fileno, "warn")
[1494]630        except :   
631            self.logdebug("Error while unregistering file number %s from polling object." % fileno)
[1458]632        else :   
633            self.logdebug("File number %s unregistered from polling object." % fileno)
634           
[1495]635    def formatFileEvent(self, fd, mask) :       
[1478]636        """Formats file debug info."""
[1495]637        maskval = []
638        if mask & select.POLLIN :
639            maskval.append("POLLIN")
640        if mask & select.POLLOUT :
641            maskval.append("POLLOUT")
642        if mask & select.POLLPRI :
643            maskval.append("POLLPRI")
644        if mask & select.POLLERR :
645            maskval.append("POLLERR")
646        if mask & select.POLLHUP :
647            maskval.append("POLLHUP")
648        if mask & select.POLLNVAL :
649            maskval.append("POLLNVAL")
650        return "%s (%s)" % (fd, " | ".join(maskval))
[1478]651       
[1918]652    def handleData(self, filehandle=None) :
[1271]653        """Pass the job's data to the real backend."""
[1222]654        # Find the real backend pathname   
[1271]655        realbackend = os.path.join(os.path.split(sys.argv[0])[0], self.originalbackend)
[1222]656       
657        # And launch it
[1923]658        if filehandle is None :
659            arguments = sys.argv
660        else :   
661            # Here we absolutely WANT to remove any filename from the command line !
662            arguments = [ "Fake this because we are printing a banner" ] + sys.argv[1:6]
[2006]663           
664        self.regainPriv()   
665       
[1924]666        self.logdebug("Starting real backend %s with args %s" % (realbackend, " ".join(['"%s"' % a for a in ([os.environ["DEVICE_URI"]] + arguments[1:])])))
667        subprocess = PyKotaPopen4([realbackend] + arguments[1:], bufsize=0, arg0=os.environ["DEVICE_URI"])
[1222]668       
669        # Save file descriptors, we will need them later.
670        stderrfno = sys.stderr.fileno()
671        fromcfno = subprocess.fromchild.fileno()
[1494]672        tocfno = subprocess.tochild.fileno()
[1222]673       
674        # We will have to be careful when dealing with I/O
675        # So we use a poll object to know when to read or write
676        pollster = select.poll()
677        pollster.register(fromcfno, select.POLLIN | select.POLLPRI)
[1494]678        pollster.register(stderrfno, select.POLLOUT)
679        pollster.register(tocfno, select.POLLOUT)
[1222]680       
[1494]681        # Initialize our buffers
682        indata = ""
683        outdata = ""
684        endinput = endoutput = 0
685        inputclosed = outputclosed = 0
[1897]686        totaltochild = totalfromcups = 0
687        totalfromchild = totaltocups = 0
[1494]688       
[1918]689        if filehandle is None:
690            if self.preserveinputfile is None :
691               # this is not a real file, we read the job's data
692                # from our temporary file which is a copy of stdin
693                infno = self.jobdatastream.fileno()
694                self.jobdatastream.seek(0)
695                pollster.register(infno, select.POLLIN | select.POLLPRI)
696            else :   
697                # job's data is in a file, no need to pass the data
698                # to the real backend
699                self.logdebug("Job's data is in %s" % self.preserveinputfile)
700                infno = None
701                endinput = 1
702        else:
703            self.logdebug("Printing data passed from filehandle")
704            indata = filehandle.read()
[1494]705            infno = None
706            endinput = 1
[1918]707            filehandle.close()
[1494]708       
[1606]709        self.logdebug("Entering streams polling loop...")
[1495]710        MEGABYTE = 1024*1024
[1494]711        killed = 0
712        status = -1
[1495]713        while (status == -1) and (not killed) and not (inputclosed and outputclosed) :
[1494]714            # First check if original backend is still alive
715            status = subprocess.poll()
716           
717            # Now if we got SIGTERM, we have
718            # to kill -TERM the original backend
719            if self.gotSigTerm and not killed :
720                try :
[1222]721                    os.kill(subprocess.pid, signal.SIGTERM)
[1495]722                except OSError, msg : # ignore but logs if process was already killed.
[1514]723                    self.logdebug("Error while sending signal to pid %s : %s" % (subprocess.pid, msg))
[1495]724                else :   
[1606]725                    self.printInfo(_("SIGTERM was sent to real backend %s (pid: %s)") % (realbackend, subprocess.pid))
[1291]726                    killed = 1
[1494]727           
728            # In any case, deal with any remaining I/O
[1498]729            try :
730                availablefds = pollster.poll(5000)
731            except select.error, msg :   
732                self.logdebug("Interrupted poll : %s" % msg)
733                availablefds = []
[1495]734            if not availablefds :
[1606]735                self.logdebug("Nothing to do, sleeping a bit...")
[1495]736                time.sleep(0.01) # give some time to the system
737            else :
738                for (fd, mask) in availablefds :
739                    # self.logdebug(self.formatFileEvent(fd, mask))
740                    try :
741                        if mask & select.POLLOUT :
742                            # We can write
743                            if fd == tocfno :
744                                if indata :
745                                    try :
[1897]746                                        nbwritten = os.write(fd, indata)   
[1515]747                                    except (OSError, IOError), msg :   
[1495]748                                        self.logdebug("Error while writing to real backend's stdin %s : %s" % (fd, msg))
749                                    else :   
[1897]750                                        if len(indata) != nbwritten :
751                                            self.logdebug("Short write to real backend's input !")
752                                        totaltochild += nbwritten   
753                                        self.logdebug("%s bytes sent to real backend so far..." % totaltochild)
754                                        indata = indata[nbwritten:]
[1498]755                                else :       
[1606]756                                    self.logdebug("No data to send to real backend yet, sleeping a bit...")
[1498]757                                    time.sleep(0.01)
758                                   
[1495]759                                if endinput :   
760                                    self.unregisterFileNo(pollster, tocfno)       
[1606]761                                    self.logdebug("Closing real backend's stdin.")
[1495]762                                    os.close(tocfno)
763                                    inputclosed = 1
764                            elif fd == stderrfno :
765                                if outdata :
766                                    try :
[1897]767                                        nbwritten = os.write(fd, outdata)
[1515]768                                    except (OSError, IOError), msg :   
[1495]769                                        self.logdebug("Error while writing to CUPS back channel (stderr) %s : %s" % (fd, msg))
770                                    else :
[1897]771                                        if len(outdata) != nbwritten :
772                                            self.logdebug("Short write to stderr (CUPS) !")
773                                        totaltocups += nbwritten   
774                                        self.logdebug("%s bytes sent back to CUPS so far..." % totaltocups)
775                                        outdata = outdata[nbwritten:]
[1498]776                                else :       
777                                    # self.logdebug("No data to send back to CUPS yet, sleeping a bit...") # Uncommenting this fills your logs
778                                    time.sleep(0.01) # Give some time to the system, stderr is ALWAYS writeable it seems.
779                                   
[1495]780                                if endoutput :   
781                                    self.unregisterFileNo(pollster, stderrfno)       
782                                    outputclosed = 1
[1498]783                            else :   
784                                self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
785                                time.sleep(0.01)
786                               
[1495]787                        if mask & (select.POLLIN | select.POLLPRI) :     
788                            # We have something to read
789                            try :
790                                data = os.read(fd, MEGABYTE)
791                            except (IOError, OSError), msg :   
792                                self.logdebug("Error while reading file %s : %s" % (fd, msg))
793                            else :
794                                if fd == infno :
795                                    if not data :    # If yes, then no more input data
796                                        self.unregisterFileNo(pollster, infno)
[1606]797                                        self.logdebug("Input data ends.")
[1495]798                                        endinput = 1 # this happens with real files.
[1498]799                                    else :   
800                                        indata += data
[1897]801                                        totalfromcups += len(data)
802                                        self.logdebug("%s bytes read from CUPS so far..." % totalfromcups)
[1495]803                                elif fd == fromcfno :
[1498]804                                    if not data :
[1606]805                                        self.logdebug("No back channel data to read from real backend yet, sleeping a bit...")
[1498]806                                        time.sleep(0.01)
807                                    else :
808                                        outdata += data
[1897]809                                        totalfromchild += len(data)
810                                        self.logdebug("%s bytes read from real backend so far..." % totalfromchild)
[1498]811                                else :   
812                                    self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
813                                    time.sleep(0.01)
814                                   
[1495]815                        if mask & (select.POLLHUP | select.POLLERR) :
816                            # Treat POLLERR as an EOF.
817                            # Some standard I/O stream has no more datas
818                            self.unregisterFileNo(pollster, fd)
[1494]819                            if fd == infno :
[1495]820                                # Here we are in the case where the input file is stdin.
821                                # which has no more data to be read.
[1606]822                                self.logdebug("Input data ends.")
[1495]823                                endinput = 1
824                            elif fd == fromcfno :   
825                                # We are no more interested in this file descriptor       
[1606]826                                self.logdebug("Closing real backend's stdout+stderr.")
[1495]827                                os.close(fromcfno)
828                                endoutput = 1
[1498]829                            else :   
830                                self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
831                                time.sleep(0.01)
[1495]832                               
833                        if mask & select.POLLNVAL :       
[1606]834                            self.logdebug("File %s was closed. Unregistering from polling object." % fd)
[1495]835                            self.unregisterFileNo(pollster, fd)
836                    except IOError, msg :           
837                        self.logdebug("Got an IOError : %s" % msg) # we got signalled during an I/O
[1191]838               
[1494]839        # We must close the real backend's input stream
840        if killed and not inputclosed :
[1606]841            self.logdebug("Forcing close of real backend's stdin.")
[1494]842            os.close(tocfno)
843       
[1606]844        self.logdebug("Exiting streams polling loop...")
[1492]845       
[1897]846        self.logdebug("input data's final length : %s" % len(indata))
847        self.logdebug("back-channel data's final length : %s" % len(outdata))
848       
849        self.logdebug("Total bytes read from CUPS (job's datas) : %s" % totalfromcups)
850        self.logdebug("Total bytes sent to real backend (job's datas) : %s" % totaltochild)
851       
852        self.logdebug("Total bytes read from real backend (back-channel datas) : %s" % totalfromchild)
853        self.logdebug("Total bytes sent back to CUPS (back-channel datas) : %s" % totaltocups)
854       
[1494]855        # Check exit code of original CUPS backend.   
856        if status == -1 :
857            # we exited the loop before the real backend exited
858            # now we have to wait for it to finish and get its status
[1606]859            self.logdebug("Waiting for real backend to exit...")
[1494]860            try :
861                status = subprocess.wait()
[2054]862            except OSError : # already dead : TODO : detect when abnormal
[1494]863                status = 0
[1222]864        if os.WIFEXITED(status) :
865            retcode = os.WEXITSTATUS(status)
[1291]866        elif not killed :   
[1606]867            self.sendBackChannelData(_("CUPS backend %s died abnormally.") % realbackend, "error")
[1222]868            retcode = -1
[1291]869        else :   
870            retcode = self.removeJob()
[2006]871           
872        self.dropPriv()   
873       
[1271]874        return retcode   
[1222]875   
[1177]876if __name__ == "__main__" :   
877    # This is a CUPS backend, we should act and die like a CUPS backend
[1542]878    retcode = 0
[1177]879    if len(sys.argv) == 1 :
[1178]880        # we will execute each existing backend in device enumeration mode
881        # and generate their PyKota accounting counterpart
882        (directory, myname) = os.path.split(sys.argv[0])
883        for backend in [os.path.join(directory, b) for b in os.listdir(directory) if os.path.isfile(os.path.join(directory, b)) and (b != myname)] :
884            answer = os.popen(backend, "r")
885            try :
886                devices = [line.strip() for line in answer.readlines()]
887            except :   
888                devices = []
889            status = answer.close()
890            if status is None :
891                for d in devices :
[1180]892                    # each line is of the form : 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
893                    # so we have to decompose it carefully
[1178]894                    fdevice = cStringIO.StringIO("%s" % d)
895                    tokenizer = shlex.shlex(fdevice)
896                    tokenizer.wordchars = tokenizer.wordchars + r".:,?!~/\_$*-+={}[]()#"
897                    arguments = []
898                    while 1 :
899                        token = tokenizer.get_token()
900                        if token :
901                            arguments.append(token)
902                        else :
903                            break
904                    fdevice.close()
[1180]905                    try :
906                        (devicetype, device, name, fullname) = arguments
907                    except ValueError :   
908                        pass    # ignore this 'bizarre' device
909                    else :   
910                        if name.startswith('"') and name.endswith('"') :
911                            name = name[1:-1]
912                        if fullname.startswith('"') and fullname.endswith('"') :
913                            fullname = fullname[1:-1]
[1191]914                        print '%s cupspykota:%s "PyKota+%s" "PyKota managed %s"' % (devicetype, device, name, fullname)
[1542]915        retcode = 0               
[1177]916    elif len(sys.argv) not in (6, 7) :   
917        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n" % sys.argv[0])
918        retcode = 1
919    else :   
920        try :
[1542]921            try :
922                # Initializes the backend
923                kotabackend = PyKotaBackend()   
924            except SystemExit :   
925                retcode = -1
926            except :   
927                crashed("cupspykota backend initialization failed")
928                retcode = 1
929            else :   
930                retcode = kotabackend.mainWork()
931                kotabackend.storage.close()
932                kotabackend.closeJobDataStream()   
[1513]933        except :
[1517]934            try :
935                kotabackend.crashed("cupspykota backend failed")
936            except :   
[1542]937                crashed("cupspykota backend failed")
938            retcode = 1   
[1177]939       
940    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.