root / pykota / trunk / bin / cupspykota @ 1704

Revision 1704, 30.3 kB (checked in by jalet, 20 years ago)

Fix for autodetection of SC_OPEN_MAX

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