root / pykota / trunk / bin / cupspykota @ 1923

Revision 1923, 37.6 kB (checked in by jalet, 19 years ago)

Improved banner handling.
Fix for raw printing and banners.

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