root / pykota / trunk / bin / cupspykota @ 1519

Revision 1519, 28.7 kB (checked in by jalet, 20 years ago)

Now denies empty jobs

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