root / pykota / trunk / bin / cupspykota @ 1643

Revision 1643, 30.2 kB (checked in by jalet, 20 years ago)

Unneeded module

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