root / pykota / trunk / bin / cupspykota @ 1600

Revision 1600, 30.1 kB (checked in by jalet, 20 years ago)

LPRng support early version

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