root / pykota / trunk / bin / cupspykota @ 1918

Revision 1918, 37.2 kB (checked in by jalet, 19 years ago)

PyKota banners now basically work !

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