root / pykota / trunk / bin / cupspykota @ 1907

Revision 1902, 35.0 kB (checked in by jalet, 20 years ago)

Safer code

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