root / pykota / trunk / bin / cupspykota @ 1935

Revision 1924, 37.6 kB (checked in by jalet, 20 years ago)

Fix for the Fix !!!

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