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
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.86  2005/02/13 22:48:37  jalet
27# Added the md5sum to the history
28#
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#
36# Revision 1.84  2005/01/17 08:44:23  jalet
37# Modified copyright years
38#
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#
43# Revision 1.82  2005/01/06 22:52:53  jalet
44# Implemented the dropping of priviledges. Beware, beware...
45#
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#
49# Revision 1.80  2004/11/15 22:12:46  jalet
50# Fix for the Fix !!!
51#
52# Revision 1.79  2004/11/15 22:01:34  jalet
53# Improved banner handling.
54# Fix for raw printing and banners.
55#
56# Revision 1.78  2004/11/15 19:59:34  jalet
57# PyKota banners now basically work !
58#
59# Revision 1.77  2004/11/06 22:40:57  jalet
60# Safer code
61#
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#
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#
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#
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#
82# Revision 1.72  2004/09/13 16:02:44  jalet
83# Added fix for incorrect job's size when hardware accounting fails
84#
85# Revision 1.71  2004/09/06 17:05:06  jalet
86# Fix for autodetection of SC_OPEN_MAX
87#
88# Revision 1.70  2004/07/26 09:20:27  jalet
89# Unneeded module
90#
91# Revision 1.69  2004/07/22 22:41:47  jalet
92# Hardware accounting for LPRng should be OK now. UNTESTED.
93#
94# Revision 1.68  2004/07/20 22:19:44  jalet
95# Sanitized a bit + use of gettext
96#
97# Revision 1.67  2004/07/16 12:22:45  jalet
98# LPRng support early version
99#
100# Revision 1.66  2004/07/01 19:56:25  jalet
101# Better dispatching of error messages
102#
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#
107# Revision 1.64  2004/06/18 13:34:46  jalet
108# Now all tracebacks include PyKota's version number
109#
110# Revision 1.63  2004/06/17 13:26:50  jalet
111# Better exception handling code
112#
113# Revision 1.62  2004/06/16 20:56:34  jalet
114# Smarter initialisation code
115#
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#
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#
126# Revision 1.59  2004/06/03 22:12:53  jalet
127# Now denies empty jobs
128#
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#
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#
137# Revision 1.56  2004/06/02 21:50:56  jalet
138# Moved the sigterm capturing elsewhere
139#
140# Revision 1.55  2004/06/02 14:25:07  jalet
141# Should correctly capture ALL errors now
142#
143# Revision 1.54  2004/05/26 16:44:48  jalet
144# Now logs something when client hostname can't be extracted
145#
146# Revision 1.53  2004/05/26 14:49:35  jalet
147# First try at saving the job-originating-hostname in the database
148#
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#
153# Revision 1.51  2004/05/25 08:31:16  jalet
154# Heavy CPU usage seems to be fixed at least !
155#
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#
160# Revision 1.49  2004/05/24 22:45:48  jalet
161# New 'enforcement' directive added
162# Polling loop improvements
163#
164# Revision 1.48  2004/05/24 14:36:24  jalet
165# Revert to old polling loop. Will need optimisations
166#
167# Revision 1.47  2004/05/24 11:59:46  jalet
168# More robust (?) code
169#
170# Revision 1.46  2004/05/21 22:02:51  jalet
171# Preliminary work on pre-accounting
172#
173# Revision 1.45  2004/05/19 07:15:32  jalet
174# Could the 'misterious' bug in my loop be finally fixed ???
175#
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#
180# Revision 1.43  2004/05/17 11:46:05  jalet
181# First try at cupspykota's main loop rewrite
182#
183# Revision 1.42  2004/05/10 11:22:28  jalet
184# Typo
185#
186# Revision 1.41  2004/05/10 10:07:30  jalet
187# Catches OSError while reading
188#
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#
192# Revision 1.39  2004/05/07 14:44:53  jalet
193# Fix for file handles unregistered twice from the polling object
194#
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#
200# Revision 1.37  2004/03/18 19:11:25  jalet
201# Fix for raw jobs in cupspykota
202#
203# Revision 1.36  2004/03/18 14:03:18  jalet
204# Added fsync() calls
205#
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#
210# Revision 1.34  2004/03/15 10:47:56  jalet
211# This time the traceback formatting should be correct !
212#
213# Revision 1.33  2004/03/05 12:46:07  jalet
214# Improve tracebacks
215#
216# Revision 1.32  2004/03/05 12:31:35  jalet
217# Now should output full traceback when crashing
218#
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#
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#
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#
230# Revision 1.28  2004/02/26 14:18:07  jalet
231# Should fix the remaining bugs wrt printers groups and users groups.
232#
233# Revision 1.27  2004/02/04 23:41:27  jalet
234# Should fix the incorrect "backend died abnormally" problem.
235#
236# Revision 1.26  2004/01/30 16:35:03  jalet
237# Fixes stupid software accounting bug in CUPS backend
238#
239# Revision 1.25  2004/01/16 17:51:46  jalet
240# Fuck Fuck Fuck !!!
241#
242# Revision 1.24  2004/01/14 15:52:01  jalet
243# Small fix for job cancelling code.
244#
245# Revision 1.23  2004/01/13 10:48:28  jalet
246# Small streams polling loop modification.
247#
248# Revision 1.22  2004/01/12 22:43:40  jalet
249# New formula to compute a job's price
250#
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#
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#
258# Revision 1.19  2004/01/08 14:10:32  jalet
259# Copyright year changed.
260#
261# Revision 1.18  2004/01/07 16:16:32  jalet
262# Better debugging information
263#
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#
271# Revision 1.16  2003/11/26 20:43:29  jalet
272# Inadvertantly introduced a bug, which is fixed.
273#
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#
278# Revision 1.14  2003/11/25 13:25:45  jalet
279# Boolean problem with old Python, replaced with 0
280#
281# Revision 1.13  2003/11/23 19:01:35  jalet
282# Job price added to history
283#
284# Revision 1.12  2003/11/21 14:28:43  jalet
285# More complete job history.
286#
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#
292# Revision 1.10  2003/11/18 17:54:24  jalet
293# SIGTERMs are now transmitted to original backends.
294#
295# Revision 1.9  2003/11/18 14:11:07  jalet
296# Small fix for bizarre urls
297#
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#
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#
307# Revision 1.6  2003/11/14 20:13:11  jalet
308# We exit the loop too soon.
309#
310# Revision 1.5  2003/11/14 18:31:27  jalet
311# Not perfect, but seems to work with the poll() loop.
312#
313# Revision 1.4  2003/11/14 17:04:15  jalet
314# More (untested) work on the CUPS backend.
315#
316# Revision 1.3  2003/11/12 23:27:44  jalet
317# More work on new backend. This commit may be unstable.
318#
319# Revision 1.2  2003/11/12 09:33:34  jalet
320# New CUPS backend supports device enumeration
321#
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
330import popen2
331import cStringIO
332import shlex
333import select
334import signal
335import time
336
337from pykota.tool import PyKotaFilterOrBackend, PyKotaToolError, crashed
338from pykota.config import PyKotaConfigError
339from pykota.storage import PyKotaStorageError
340from pykota.accounter import PyKotaAccounterError
341from pykota.ipp import IPPMessage, PyKotaIPPError
342   
343class PyKotaPopen4(popen2.Popen4) :
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    """
349    def __init__(self, cmd, bufsize=-1, arg0=None) :
350        self.arg0 = arg0
351        popen2.Popen4.__init__(self, cmd, bufsize)
352       
353    def _run_child(self, cmd):
354        try :
355            MAXFD = os.sysconf("SC_OPEN_MAX")
356        except (AttributeError, ValueError) :   
357            MAXFD = 256
358        for i in range(3, MAXFD) : 
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   
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.
376       
377           Returning 0 (success) prevents CUPS from stopping the print queue.
378        """   
379        return 0
380       
381    def getCupsConfigDirectives(self, directives=[]) :
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        """
387        dirvalues = {} 
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 :   
393            self.logdebug("Unable to open %s" % cupsdconf)
394        else :   
395            for line in conffile.readlines() :
396                linecopy = line.strip().lower()
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
405            conffile.close()           
406        return dirvalues       
407           
408    def getJobOriginatingHostnameFromPageLog(self, cupsconfig, printername, username, jobid) :
409        """Retrieves the job-originating-hostname from the CUPS page_log file if possible."""
410        pagelogpath = cupsconfig.get("pagelog", "/var/log/cups/page_log")
411        self.logdebug("Trying to extract job-originating-host-name from %s" % pagelogpath)
412        try :
413            pagelog = open(pagelogpath, "r")
414        except IOError :   
415            self.logdebug("Unable to open %s" % pagelogpath)
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
420            prefix = ("%s %s %s " % (printername, username, jobid)).lower()
421            matchingline = None
422            while 1 :
423                line = pagelog.readline()
424                if not line :
425                    break
426                else :
427                    line = line.strip()
428                    if line.lower().startswith(prefix) :   
429                        matchingline = line # no break, because we read forward
430            pagelog.close()       
431            if matchingline is None :
432                self.logdebug("No matching line found in %s" % pagelogpath)
433                return # correct line not found, job-originating-host-name unknown
434            else :   
435                return matchingline.split()[-1]
436               
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" :
445            # exports user information with initial values
446            self.exportUserInfo(userpquota)
447           
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 = {}
457            self.regainPriv()
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)
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)
470                ippdatafile.close()
471            self.dropPriv()   
472            clienthost = ippmessage.get("job-originating-host-name") \
473                         or self.getJobOriginatingHostnameFromPageLog(cupsdconf, printer.Name, user.Name, self.jobid)
474            self.logdebug("Client Hostname : %s" % (clienthost or "Unknown"))   
475            os.environ["PYKOTAJOBORIGINATINGHOSTNAME"] = str(clienthost or "")
476           
477            # TODO : extract username (double check ?) and billing code too
478           
479            # enters first phase
480            os.environ["PYKOTAPHASE"] = "BEFORE"
481           
482            # do we want strict or laxist quota enforcement ?
483            if self.config.getPrinterEnforcement(printer.Name) == "STRICT" :
484                self.softwareJobSize = self.precomputeJobSize()
485                self.softwareJobPrice = userpquota.computeJobPrice(self.softwareJobSize)
486                self.logdebug("Precomputed job's size is %s pages, price is %s units" % (self.softwareJobSize, self.softwareJobPrice))
487            os.environ["PYKOTAPRECOMPUTEDJOBSIZE"] = str(self.softwareJobSize)
488            os.environ["PYKOTAPRECOMPUTEDJOBPRICE"] = str(self.softwareJobPrice)
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 :
494                self.printMoreInfo(user, printer, _("Job contains no data. Printing is denied."), "warn")
495                action = "DENY"
496            else :   
497                # checks the user's quota
498                action = self.warnUserPQuota(userpquota)
499           
500            # exports some new environment variables
501            os.environ["PYKOTAACTION"] = action
502           
503            # launches the pre hook
504            self.prehook(userpquota)
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
509           
510            # handle starting banner pages before accounting
511            accountbanner = self.config.getAccountBanner(printer.Name)
512            if accountbanner in ["ENDING", "NONE"] :
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)
523 
524            self.printMoreInfo(user, printer, _("Job accounting begins."))
525            self.accounter.beginJob(printer)
526           
527            # handle starting banner pages during accounting
528            if accountbanner in ["STARTING", "BOTH"] :
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
541        else :   
542            action = "ALLOW"
543            os.environ["PYKOTAACTION"] = action
544           
545        # pass the job's data to the real backend   
546        if action in ["ALLOW", "WARN"] :
547            if self.gotSigTerm :
548                retcode = self.removeJob()
549            else :   
550                retcode = self.handleData()       
551        else :       
552            retcode = self.removeJob()
553       
554        if policy == "OK" :       
555            # indicate phase change
556            os.environ["PYKOTAPHASE"] = "AFTER"
557           
558            # handle ending banner pages during accounting
559            if accountbanner in ["ENDING", "BOTH"] :
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
572 
573            # stops accounting.
574            self.accounter.endJob(printer)
575            self.printMoreInfo(user, printer, _("Job accounting ends."))
576               
577            # retrieve the job size   
578            if action == "DENY" :
579                jobsize = 0
580                self.printMoreInfo(user, printer, _("Job size forced to 0 because printing is denied."))
581            else :   
582                jobsize = self.accounter.getJobSize(printer)
583                if self.softwareJobSize and (jobsize != self.softwareJobSize) :
584                    self.printInfo(_("Beware : computed job size (%s) != precomputed job size (%s)") % (jobsize, self.softwareJobSize), "error")
585                jobsize += bannersize   
586            self.printMoreInfo(user, printer, _("Job size : %i") % jobsize)
587           
588            # update the quota for the current user on this printer
589            self.printInfo(_("Updating user %s's quota on printer %s") % (user.Name, printer.Name))
590            jobprice = userpquota.increasePagesUsage(jobsize)
591           
592            # adds the current job to history   
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)
597            self.printMoreInfo(user, printer, _("Job added to history."))
598           
599            # exports some new environment variables
600            os.environ["PYKOTAJOBSIZE"] = str(jobsize)
601            os.environ["PYKOTAJOBPRICE"] = str(jobprice)
602           
603            # then re-export user information with new value
604            self.exportUserInfo(userpquota)
605           
606            # handle ending banner pages after accounting ends
607            if accountbanner in ["STARTING", "NONE"] :
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                       
619            # Launches the post hook
620            self.posthook(userpquota)
621           
622        return retcode   
623               
624    def unregisterFileNo(self, pollobj, fileno) :               
625        """Removes a file handle from the polling object."""
626        try :
627            pollobj.unregister(fileno)
628        except KeyError :   
629            self.printInfo(_("File number %s unregistered twice from polling object, ignored.") % fileno, "warn")
630        except :   
631            self.logdebug("Error while unregistering file number %s from polling object." % fileno)
632        else :   
633            self.logdebug("File number %s unregistered from polling object." % fileno)
634           
635    def formatFileEvent(self, fd, mask) :       
636        """Formats file debug info."""
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))
651       
652    def handleData(self, filehandle=None) :
653        """Pass the job's data to the real backend."""
654        # Find the real backend pathname   
655        realbackend = os.path.join(os.path.split(sys.argv[0])[0], self.originalbackend)
656       
657        # And launch it
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]
663           
664        self.regainPriv()   
665       
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"])
668       
669        # Save file descriptors, we will need them later.
670        stderrfno = sys.stderr.fileno()
671        fromcfno = subprocess.fromchild.fileno()
672        tocfno = subprocess.tochild.fileno()
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)
678        pollster.register(stderrfno, select.POLLOUT)
679        pollster.register(tocfno, select.POLLOUT)
680       
681        # Initialize our buffers
682        indata = ""
683        outdata = ""
684        endinput = endoutput = 0
685        inputclosed = outputclosed = 0
686        totaltochild = totalfromcups = 0
687        totalfromchild = totaltocups = 0
688       
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()
705            infno = None
706            endinput = 1
707            filehandle.close()
708       
709        self.logdebug("Entering streams polling loop...")
710        MEGABYTE = 1024*1024
711        killed = 0
712        status = -1
713        while (status == -1) and (not killed) and not (inputclosed and outputclosed) :
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 :
721                    os.kill(subprocess.pid, signal.SIGTERM)
722                except OSError, msg : # ignore but logs if process was already killed.
723                    self.logdebug("Error while sending signal to pid %s : %s" % (subprocess.pid, msg))
724                else :   
725                    self.printInfo(_("SIGTERM was sent to real backend %s (pid: %s)") % (realbackend, subprocess.pid))
726                    killed = 1
727           
728            # In any case, deal with any remaining I/O
729            try :
730                availablefds = pollster.poll(5000)
731            except select.error, msg :   
732                self.logdebug("Interrupted poll : %s" % msg)
733                availablefds = []
734            if not availablefds :
735                self.logdebug("Nothing to do, sleeping a bit...")
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 :
746                                        nbwritten = os.write(fd, indata)   
747                                    except (OSError, IOError), msg :   
748                                        self.logdebug("Error while writing to real backend's stdin %s : %s" % (fd, msg))
749                                    else :   
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:]
755                                else :       
756                                    self.logdebug("No data to send to real backend yet, sleeping a bit...")
757                                    time.sleep(0.01)
758                                   
759                                if endinput :   
760                                    self.unregisterFileNo(pollster, tocfno)       
761                                    self.logdebug("Closing real backend's stdin.")
762                                    os.close(tocfno)
763                                    inputclosed = 1
764                            elif fd == stderrfno :
765                                if outdata :
766                                    try :
767                                        nbwritten = os.write(fd, outdata)
768                                    except (OSError, IOError), msg :   
769                                        self.logdebug("Error while writing to CUPS back channel (stderr) %s : %s" % (fd, msg))
770                                    else :
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:]
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                                   
780                                if endoutput :   
781                                    self.unregisterFileNo(pollster, stderrfno)       
782                                    outputclosed = 1
783                            else :   
784                                self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
785                                time.sleep(0.01)
786                               
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)
797                                        self.logdebug("Input data ends.")
798                                        endinput = 1 # this happens with real files.
799                                    else :   
800                                        indata += data
801                                        totalfromcups += len(data)
802                                        self.logdebug("%s bytes read from CUPS so far..." % totalfromcups)
803                                elif fd == fromcfno :
804                                    if not data :
805                                        self.logdebug("No back channel data to read from real backend yet, sleeping a bit...")
806                                        time.sleep(0.01)
807                                    else :
808                                        outdata += data
809                                        totalfromchild += len(data)
810                                        self.logdebug("%s bytes read from real backend so far..." % totalfromchild)
811                                else :   
812                                    self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
813                                    time.sleep(0.01)
814                                   
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)
819                            if fd == infno :
820                                # Here we are in the case where the input file is stdin.
821                                # which has no more data to be read.
822                                self.logdebug("Input data ends.")
823                                endinput = 1
824                            elif fd == fromcfno :   
825                                # We are no more interested in this file descriptor       
826                                self.logdebug("Closing real backend's stdout+stderr.")
827                                os.close(fromcfno)
828                                endoutput = 1
829                            else :   
830                                self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
831                                time.sleep(0.01)
832                               
833                        if mask & select.POLLNVAL :       
834                            self.logdebug("File %s was closed. Unregistering from polling object." % fd)
835                            self.unregisterFileNo(pollster, fd)
836                    except IOError, msg :           
837                        self.logdebug("Got an IOError : %s" % msg) # we got signalled during an I/O
838               
839        # We must close the real backend's input stream
840        if killed and not inputclosed :
841            self.logdebug("Forcing close of real backend's stdin.")
842            os.close(tocfno)
843       
844        self.logdebug("Exiting streams polling loop...")
845       
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       
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
859            self.logdebug("Waiting for real backend to exit...")
860            try :
861                status = subprocess.wait()
862            except OSError : # already dead : TODO : detect when abnormal
863                status = 0
864        if os.WIFEXITED(status) :
865            retcode = os.WEXITSTATUS(status)
866        elif not killed :   
867            self.sendBackChannelData(_("CUPS backend %s died abnormally.") % realbackend, "error")
868            retcode = -1
869        else :   
870            retcode = self.removeJob()
871           
872        self.dropPriv()   
873       
874        return retcode   
875   
876if __name__ == "__main__" :   
877    # This is a CUPS backend, we should act and die like a CUPS backend
878    retcode = 0
879    if len(sys.argv) == 1 :
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 :
892                    # each line is of the form : 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
893                    # so we have to decompose it carefully
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()
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]
914                        print '%s cupspykota:%s "PyKota+%s" "PyKota managed %s"' % (devicetype, device, name, fullname)
915        retcode = 0               
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 :
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()   
933        except :
934            try :
935                kotabackend.crashed("cupspykota backend failed")
936            except :   
937                crashed("cupspykota backend failed")
938            retcode = 1   
939       
940    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.