root / pykota / trunk / bin / cupspykota @ 2054

Revision 2054, 40.5 kB (checked in by jalet, 19 years ago)

Big database structure changes. Upgrade script is now included as well as
the new LDAP schema.
Introduction of the -o | --overcharge command line option to edpykota.
The output of repykota is more complete, but doesn't fit in 80 columns anymore.
Introduction of the new 'maxdenybanners' directive.

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