root / pykota / trunk / bin / cupspykota @ 2147

Revision 2147, 34.5 kB (checked in by jerome, 19 years ago)

Removed all references to $Log$

  • 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#
[2028]8# (c) 2003, 2004, 2005 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#
[2066]25#
[1177]26
27import sys
28import os
[1182]29import popen2
[1178]30import cStringIO
31import shlex
[1182]32import select
33import signal
[1291]34import time
[1177]35
[1546]36from pykota.tool import PyKotaFilterOrBackend, PyKotaToolError, crashed
[1177]37from pykota.config import PyKotaConfigError
38from pykota.storage import PyKotaStorageError
[1196]39from pykota.accounter import PyKotaAccounterError
[1901]40from pykota.ipp import IPPMessage, PyKotaIPPError
[1271]41   
[1478]42class PyKotaPopen4(popen2.Popen4) :
[1182]43    """Our own class to execute real backends.
44   
45       Their first argument is different from their path so using
46       native popen2.Popen3 would not be feasible.
47    """
[1478]48    def __init__(self, cmd, bufsize=-1, arg0=None) :
[1182]49        self.arg0 = arg0
[1478]50        popen2.Popen4.__init__(self, cmd, bufsize)
[1182]51       
52    def _run_child(self, cmd):
[1704]53        try :
54            MAXFD = os.sysconf("SC_OPEN_MAX")
55        except (AttributeError, ValueError) :   
56            MAXFD = 256
57        for i in range(3, MAXFD) : 
[1182]58            try:
59                os.close(i)
60            except OSError:
61                pass
62        try:
63            os.execvpe(cmd[0], [self.arg0 or cmd[0]] + cmd[1:], os.environ)
64        finally:
65            os._exit(1)
66   
[1271]67class PyKotaBackend(PyKotaFilterOrBackend) :       
68    """A class for the pykota backend."""
69    def acceptJob(self) :       
70        """Returns the appropriate exit code to tell CUPS all is OK."""
71        return 0
72           
73    def removeJob(self) :           
74        """Returns the appropriate exit code to let CUPS think all is OK.
[1177]75       
[1271]76           Returning 0 (success) prevents CUPS from stopping the print queue.
77        """   
78        return 0
[1222]79       
[2060]80    def genBanner(self, bannerfileorcommand) :
81        """Reads a banner or generates one through an external command.
82       
83           Returns the banner's content in a format which MUST be accepted
84           by the printer.
85        """
86        if bannerfileorcommand :
87            banner = "" # no banner by default
88            if os.access(bannerfileorcommand, os.X_OK) or not os.path.isfile(bannerfileorcommand) :
89                self.logdebug("Launching %s to generate a banner." % bannerfileorcommand)
90                child = popen2.Popen3(bannerfileorcommand, capturestderr=1)
91                banner = child.fromchild.read()
92                child.tochild.close()
93                child.childerr.close()
94                child.fromchild.close()
95                status = child.wait()
96                if os.WIFEXITED(status) :
97                    status = os.WEXITSTATUS(status)
98                self.printInfo(_("Banner generator %s exit code is %s") % (bannerfileorcommand, str(status)))
99            else :
100                self.logdebug("Using %s as the banner." % bannerfileorcommand)
101                try :
102                    fh = open(bannerfileorcommand, 'r')
103                except IOError, msg :   
104                    self.printInfo("Impossible to open %s : %s" % (bannerfileorcommand, msg), "error")
105                else :   
106                    banner = fh.read()
107                    fh.close()
108            if banner :       
109                return cStringIO.StringIO(banner)
110   
111    def startingBanner(self, printername) :
112        """Retrieves a starting banner for current printer and returns its content."""
113        self.logdebug("Retrieving starting banner...")
114        return self.genBanner(self.config.getStartingBanner(printername))
115   
116    def endingBanner(self, printername) :
117        """Retrieves an ending banner for current printer and returns its content."""
118        self.logdebug("Retrieving ending banner...")
119        return self.genBanner(self.config.getEndingBanner(printername))
120       
[1901]121    def getCupsConfigDirectives(self, directives=[]) :
[1902]122        """Retrieves some CUPS directives from its configuration file.
123       
124           Returns a mapping with lowercased directives as keys and
125           their setting as values.
126        """
[1901]127        dirvalues = {} 
[1502]128        cupsroot = os.environ.get("CUPS_SERVERROOT", "/etc/cups")
129        cupsdconf = os.path.join(cupsroot, "cupsd.conf")
130        try :
131            conffile = open(cupsdconf, "r")
132        except IOError :   
[1503]133            self.logdebug("Unable to open %s" % cupsdconf)
[1502]134        else :   
135            for line in conffile.readlines() :
136                linecopy = line.strip().lower()
[1901]137                for di in [d.lower() for d in directives] :
138                    if linecopy.startswith("%s " % di) :
139                        try :
140                            val = line.split()[1]
141                        except :   
142                            pass # ignore errors, we take the last value in any case.
143                        else :   
144                            dirvalues[di] = val
[1502]145            conffile.close()           
[1901]146        return dirvalues       
[1502]147           
[1901]148    def getJobOriginatingHostnameFromPageLog(self, cupsconfig, printername, username, jobid) :
[1502]149        """Retrieves the job-originating-hostname from the CUPS page_log file if possible."""
[1901]150        pagelogpath = cupsconfig.get("pagelog", "/var/log/cups/page_log")
151        self.logdebug("Trying to extract job-originating-host-name from %s" % pagelogpath)
[1502]152        try :
153            pagelog = open(pagelogpath, "r")
154        except IOError :   
[1503]155            self.logdebug("Unable to open %s" % pagelogpath)
[1502]156            return # no page log or can't read it, originating hostname unknown yet
157        else :   
158            # TODO : read backward so we could take first value seen
159            # TODO : here we read forward so we must take the last value seen
[1819]160            prefix = ("%s %s %s " % (printername, username, jobid)).lower()
[1502]161            matchingline = None
162            while 1 :
163                line = pagelog.readline()
164                if not line :
165                    break
166                else :
167                    line = line.strip()
[1530]168                    if line.lower().startswith(prefix) :   
[1502]169                        matchingline = line # no break, because we read forward
170            pagelog.close()       
171            if matchingline is None :
[1606]172                self.logdebug("No matching line found in %s" % pagelogpath)
[1901]173                return # correct line not found, job-originating-host-name unknown
[1502]174            else :   
175                return matchingline.split()[-1]
176               
[1271]177    def doWork(self, policy, printer, user, userpquota) :   
178        """Most of the work is done here."""
179        # Two different values possible for policy here :
180        # ALLOW means : Either printer, user or user print quota doesn't exist,
181        #               but the job should be allowed anyway.
182        # OK means : Both printer, user and user print quota exist, job should
183        #            be allowed if current user is allowed to print on this printer
184        if policy == "OK" :
[1372]185            # exports user information with initial values
186            self.exportUserInfo(userpquota)
187           
[1901]188            # tries to extract job-originating-host-name and other information
189            cupsdconf = self.getCupsConfigDirectives(["PageLog", "RequestRoot"])
190            requestroot = cupsdconf.get("requestroot", "/var/spool/cups")
191            if (len(self.jobid) < 5) and self.jobid.isdigit() :
192                ippmessagefile = "c%05i" % int(self.jobid)
193            else :   
194                ippmessagefile = "c%s" % self.jobid
195            ippmessagefile = os.path.join(requestroot, ippmessagefile)
196            ippmessage = {}
[2007]197            self.regainPriv()
[1901]198            try :
199                ippdatafile = open(ippmessagefile)
200            except :   
201                self.printInfo("Unable to open IPP message file %s" % ippmessagefile, "warn")
202            else :   
203                self.logdebug("Parsing of IPP message file %s begins." % ippmessagefile)
[1902]204                try :
205                    ippmessage = IPPMessage(ippdatafile.read())
206                except PyKotaIPPError, msg :   
207                    self.printInfo("Error while parsing %s : %s" % (ippmessagefile, msg), "warn")
208                else :   
209                    self.logdebug("Parsing of IPP message file %s ends." % ippmessagefile)
[1901]210                ippdatafile.close()
[2007]211            self.dropPriv()   
[1901]212            clienthost = ippmessage.get("job-originating-host-name") \
213                         or self.getJobOriginatingHostnameFromPageLog(cupsdconf, printer.Name, user.Name, self.jobid)
[1503]214            self.logdebug("Client Hostname : %s" % (clienthost or "Unknown"))   
[1517]215            os.environ["PYKOTAJOBORIGINATINGHOSTNAME"] = str(clienthost or "")
[1502]216           
[1901]217            # TODO : extract username (double check ?) and billing code too
218           
[1375]219            # enters first phase
[1517]220            os.environ["PYKOTAPHASE"] = "BEFORE"
[1375]221           
[2060]222            # precomputes the job's price
223            self.softwareJobPrice = userpquota.computeJobPrice(self.softwareJobSize)
[1517]224            os.environ["PYKOTAPRECOMPUTEDJOBPRICE"] = str(self.softwareJobPrice)
[2060]225            self.logdebug("Precomputed job's size is %s pages, price is %s units" % (self.softwareJobSize, self.softwareJobPrice))
[1519]226           
227            if not self.jobSizeBytes :
[2066]228                # if no data to pass to real backend, probably a filter
229                # higher in the chain failed because of a misconfiguration.
230                # we deny the job in this case (nothing to print anyway)
[1820]231                self.printMoreInfo(user, printer, _("Job contains no data. Printing is denied."), "warn")
[1519]232                action = "DENY"
[2066]233            elif self.config.getDenyDuplicates(printer.Name) \
234                 and printer.LastJob.Exists \
235                 and (printer.LastJob.UserName == user.Name) \
236                 and (printer.LastJob.JobMD5Sum == self.checksum) :
237                self.printMoreInfo(user, printer, _("Job is a duplicate. Printing is denied."), "warn")
238                action = "DENY" 
[1606]239            else :   
240                # checks the user's quota
241                action = self.warnUserPQuota(userpquota)
[1519]242           
[1372]243            # exports some new environment variables
[1517]244            os.environ["PYKOTAACTION"] = action
[1372]245           
246            # launches the pre hook
247            self.prehook(userpquota)
[1918]248
249            # saves the size of banners which have to be accounted for
250            # this is needed in the case of software accounting
251            bannersize = 0
[1372]252           
[1918]253            # handle starting banner pages before accounting
254            accountbanner = self.config.getAccountBanner(printer.Name)
255            if accountbanner in ["ENDING", "NONE"] :
[2066]256                if (action == 'DENY') and (userpquota.WarnCount >= self.config.getMaxDenyBanners(printer.Name)) :
[2054]257                    self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn")
258                else :
259                    if action == 'DENY' :
[2066]260                        self.logdebug("Incrementing the number of deny banners for user %s on printer %s" % (user.Name, printer.Name))
261                        userpquota.incDenyBannerCounter() # increments the warning counter
[2054]262                        self.exportUserInfo(userpquota)
263                    banner = self.startingBanner(printer.Name)
264                    if banner :
265                        self.logdebug("Printing starting banner before accounting begins.")
266                        self.handleData(banner)
[1918]267 
[1820]268            self.printMoreInfo(user, printer, _("Job accounting begins."))
[1624]269            self.accounter.beginJob(printer)
[1918]270           
271            # handle starting banner pages during accounting
272            if accountbanner in ["STARTING", "BOTH"] :
[2066]273                if (action == 'DENY') and (userpquota.WarnCount >= self.config.getMaxDenyBanners(printer.Name)) :
[2054]274                    self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn")
275                else :
276                    if action == 'DENY' :
[2066]277                        self.logdebug("Incrementing the number of deny banners for user %s on printer %s" % (user.Name, printer.Name))
278                        userpquota.incDenyBannerCounter() # increments the warning counter
[2054]279                        self.exportUserInfo(userpquota)
280                    banner = self.startingBanner(printer.Name)
281                    if banner :
282                        self.logdebug("Printing starting banner during accounting.")
283                        self.handleData(banner)
284                        if self.accounter.isSoftware :
285                            bannersize += 1 # TODO : fix this by passing the banner's content through PDLAnalyzer
[1280]286        else :   
287            action = "ALLOW"
[1517]288            os.environ["PYKOTAACTION"] = action
[1271]289           
290        # pass the job's data to the real backend   
[1280]291        if action in ["ALLOW", "WARN"] :
[1291]292            if self.gotSigTerm :
[1280]293                retcode = self.removeJob()
294            else :   
295                retcode = self.handleData()       
296        else :       
[1271]297            retcode = self.removeJob()
298       
299        if policy == "OK" :       
[1374]300            # indicate phase change
[1517]301            os.environ["PYKOTAPHASE"] = "AFTER"
[1374]302           
[1918]303            # handle ending banner pages during accounting
304            if accountbanner in ["ENDING", "BOTH"] :
[2066]305                if (action == 'DENY') and (userpquota.WarnCount >= self.config.getMaxDenyBanners(printer.Name)) :
[2054]306                    self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn")
307                else :
308                    if action == 'DENY' :
[2066]309                        self.logdebug("Incrementing the number of deny banners for user %s on printer %s" % (user.Name, printer.Name))
310                        userpquota.incDenyBannerCounter() # increments the warning counter
[2054]311                        self.exportUserInfo(userpquota)
312                    banner = self.endingBanner(printer.Name)
313                    if banner :
314                        self.logdebug("Printing ending banner during accounting.")
315                        self.handleData(banner)
316                        if self.accounter.isSoftware :
317                            bannersize += 1 # TODO : fix this by passing the banner's content through PDLAnalyzer
[1918]318 
[1271]319            # stops accounting.
[1624]320            self.accounter.endJob(printer)
[1820]321            self.printMoreInfo(user, printer, _("Job accounting ends."))
[1271]322               
323            # retrieve the job size   
[1321]324            if action == "DENY" :
325                jobsize = 0
[1820]326                self.printMoreInfo(user, printer, _("Job size forced to 0 because printing is denied."))
[1321]327            else :   
[2066]328                userpquota.resetDenyBannerCounter()
[2007]329                jobsize = self.accounter.getJobSize(printer)
[1974]330                if self.softwareJobSize and (jobsize != self.softwareJobSize) :
331                    self.printInfo(_("Beware : computed job size (%s) != precomputed job size (%s)") % (jobsize, self.softwareJobSize), "error")
[2062]332                    (limit, replacement) = self.config.getTrustJobSize(printer.Name)
333                    if limit is None :
334                        self.printInfo(_("The job size will be trusted anyway according to the 'trustjobsize' directive"), "warn")
335                    else :
336                        if jobsize <= limit :
337                            self.printInfo(_("The job size will be trusted because it is inferior to the 'trustjobsize' directive's limit %s") % limit, "warn")
338                        else :
339                            self.printInfo(_("The job size will be modified according to the 'trustjobsize' directive : %s") % replacement, "warn")
340                            if replacement == "PRECOMPUTED" :
341                                jobsize = self.softwareJobSize
342                            else :   
343                                jobsize = replacement
[2007]344                jobsize += bannersize   
[1820]345            self.printMoreInfo(user, printer, _("Job size : %i") % jobsize)
[1271]346           
347            # update the quota for the current user on this printer
[1606]348            self.printInfo(_("Updating user %s's quota on printer %s") % (user.Name, printer.Name))
[1285]349            jobprice = userpquota.increasePagesUsage(jobsize)
[1271]350           
351            # adds the current job to history   
[2057]352            printer.addJobToHistory(self.jobid, user, self.accounter.getLastPageCounter(), \
353                                    action, jobsize, jobprice, self.preserveinputfile, \
354                                    self.title, self.copies, self.options, clienthost, \
355                                    self.jobSizeBytes, self.checksum)
[1820]356            self.printMoreInfo(user, printer, _("Job added to history."))
[1271]357           
[1372]358            # exports some new environment variables
[1517]359            os.environ["PYKOTAJOBSIZE"] = str(jobsize)
360            os.environ["PYKOTAJOBPRICE"] = str(jobprice)
[1372]361           
[1517]362            # then re-export user information with new value
[1372]363            self.exportUserInfo(userpquota)
364           
[1923]365            # handle ending banner pages after accounting ends
366            if accountbanner in ["STARTING", "NONE"] :
[2066]367                if (action == 'DENY') and (userpquota.WarnCount >= self.config.getMaxDenyBanners(printer.Name)) :
[2054]368                    self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn")
369                else :
370                    if action == 'DENY' :
[2066]371                        self.logdebug("Incrementing the number of deny banners for user %s on printer %s" % (user.Name, printer.Name))
372                        userpquota.incDenyBannerCounter() # increments the warning counter
[2054]373                        self.exportUserInfo(userpquota)
374                    banner = self.endingBanner(printer.Name)
375                    if banner :
376                        self.logdebug("Printing ending banner after accounting ends.")
377                        self.handleData(banner)
378                       
[1372]379            # Launches the post hook
380            self.posthook(userpquota)
381           
[1271]382        return retcode   
[1478]383               
[1458]384    def unregisterFileNo(self, pollobj, fileno) :               
385        """Removes a file handle from the polling object."""
386        try :
387            pollobj.unregister(fileno)
388        except KeyError :   
[1584]389            self.printInfo(_("File number %s unregistered twice from polling object, ignored.") % fileno, "warn")
[1494]390        except :   
391            self.logdebug("Error while unregistering file number %s from polling object." % fileno)
[1458]392        else :   
393            self.logdebug("File number %s unregistered from polling object." % fileno)
394           
[1495]395    def formatFileEvent(self, fd, mask) :       
[1478]396        """Formats file debug info."""
[1495]397        maskval = []
398        if mask & select.POLLIN :
399            maskval.append("POLLIN")
400        if mask & select.POLLOUT :
401            maskval.append("POLLOUT")
402        if mask & select.POLLPRI :
403            maskval.append("POLLPRI")
404        if mask & select.POLLERR :
405            maskval.append("POLLERR")
406        if mask & select.POLLHUP :
407            maskval.append("POLLHUP")
408        if mask & select.POLLNVAL :
409            maskval.append("POLLNVAL")
410        return "%s (%s)" % (fd, " | ".join(maskval))
[1478]411       
[1918]412    def handleData(self, filehandle=None) :
[1271]413        """Pass the job's data to the real backend."""
[1222]414        # Find the real backend pathname   
[1271]415        realbackend = os.path.join(os.path.split(sys.argv[0])[0], self.originalbackend)
[1222]416       
417        # And launch it
[1923]418        if filehandle is None :
419            arguments = sys.argv
420        else :   
421            # Here we absolutely WANT to remove any filename from the command line !
422            arguments = [ "Fake this because we are printing a banner" ] + sys.argv[1:6]
[2006]423           
424        self.regainPriv()   
425       
[1924]426        self.logdebug("Starting real backend %s with args %s" % (realbackend, " ".join(['"%s"' % a for a in ([os.environ["DEVICE_URI"]] + arguments[1:])])))
427        subprocess = PyKotaPopen4([realbackend] + arguments[1:], bufsize=0, arg0=os.environ["DEVICE_URI"])
[1222]428       
429        # Save file descriptors, we will need them later.
430        stderrfno = sys.stderr.fileno()
431        fromcfno = subprocess.fromchild.fileno()
[1494]432        tocfno = subprocess.tochild.fileno()
[1222]433       
434        # We will have to be careful when dealing with I/O
435        # So we use a poll object to know when to read or write
436        pollster = select.poll()
437        pollster.register(fromcfno, select.POLLIN | select.POLLPRI)
[1494]438        pollster.register(stderrfno, select.POLLOUT)
439        pollster.register(tocfno, select.POLLOUT)
[1222]440       
[1494]441        # Initialize our buffers
442        indata = ""
443        outdata = ""
444        endinput = endoutput = 0
445        inputclosed = outputclosed = 0
[1897]446        totaltochild = totalfromcups = 0
447        totalfromchild = totaltocups = 0
[1494]448       
[1918]449        if filehandle is None:
450            if self.preserveinputfile is None :
451               # this is not a real file, we read the job's data
452                # from our temporary file which is a copy of stdin
453                infno = self.jobdatastream.fileno()
454                self.jobdatastream.seek(0)
455                pollster.register(infno, select.POLLIN | select.POLLPRI)
456            else :   
457                # job's data is in a file, no need to pass the data
458                # to the real backend
459                self.logdebug("Job's data is in %s" % self.preserveinputfile)
460                infno = None
461                endinput = 1
462        else:
463            self.logdebug("Printing data passed from filehandle")
464            indata = filehandle.read()
[1494]465            infno = None
466            endinput = 1
[1918]467            filehandle.close()
[1494]468       
[1606]469        self.logdebug("Entering streams polling loop...")
[1495]470        MEGABYTE = 1024*1024
[1494]471        killed = 0
472        status = -1
[1495]473        while (status == -1) and (not killed) and not (inputclosed and outputclosed) :
[1494]474            # First check if original backend is still alive
475            status = subprocess.poll()
476           
477            # Now if we got SIGTERM, we have
478            # to kill -TERM the original backend
479            if self.gotSigTerm and not killed :
480                try :
[1222]481                    os.kill(subprocess.pid, signal.SIGTERM)
[1495]482                except OSError, msg : # ignore but logs if process was already killed.
[1514]483                    self.logdebug("Error while sending signal to pid %s : %s" % (subprocess.pid, msg))
[1495]484                else :   
[1606]485                    self.printInfo(_("SIGTERM was sent to real backend %s (pid: %s)") % (realbackend, subprocess.pid))
[1291]486                    killed = 1
[1494]487           
488            # In any case, deal with any remaining I/O
[1498]489            try :
490                availablefds = pollster.poll(5000)
491            except select.error, msg :   
492                self.logdebug("Interrupted poll : %s" % msg)
493                availablefds = []
[1495]494            if not availablefds :
[1606]495                self.logdebug("Nothing to do, sleeping a bit...")
[1495]496                time.sleep(0.01) # give some time to the system
497            else :
498                for (fd, mask) in availablefds :
499                    # self.logdebug(self.formatFileEvent(fd, mask))
500                    try :
501                        if mask & select.POLLOUT :
502                            # We can write
503                            if fd == tocfno :
504                                if indata :
505                                    try :
[1897]506                                        nbwritten = os.write(fd, indata)   
[1515]507                                    except (OSError, IOError), msg :   
[1495]508                                        self.logdebug("Error while writing to real backend's stdin %s : %s" % (fd, msg))
509                                    else :   
[1897]510                                        if len(indata) != nbwritten :
511                                            self.logdebug("Short write to real backend's input !")
512                                        totaltochild += nbwritten   
513                                        self.logdebug("%s bytes sent to real backend so far..." % totaltochild)
514                                        indata = indata[nbwritten:]
[1498]515                                else :       
[1606]516                                    self.logdebug("No data to send to real backend yet, sleeping a bit...")
[1498]517                                    time.sleep(0.01)
518                                   
[1495]519                                if endinput :   
520                                    self.unregisterFileNo(pollster, tocfno)       
[1606]521                                    self.logdebug("Closing real backend's stdin.")
[1495]522                                    os.close(tocfno)
523                                    inputclosed = 1
524                            elif fd == stderrfno :
525                                if outdata :
526                                    try :
[1897]527                                        nbwritten = os.write(fd, outdata)
[1515]528                                    except (OSError, IOError), msg :   
[1495]529                                        self.logdebug("Error while writing to CUPS back channel (stderr) %s : %s" % (fd, msg))
530                                    else :
[1897]531                                        if len(outdata) != nbwritten :
532                                            self.logdebug("Short write to stderr (CUPS) !")
533                                        totaltocups += nbwritten   
534                                        self.logdebug("%s bytes sent back to CUPS so far..." % totaltocups)
535                                        outdata = outdata[nbwritten:]
[1498]536                                else :       
537                                    # self.logdebug("No data to send back to CUPS yet, sleeping a bit...") # Uncommenting this fills your logs
538                                    time.sleep(0.01) # Give some time to the system, stderr is ALWAYS writeable it seems.
539                                   
[1495]540                                if endoutput :   
541                                    self.unregisterFileNo(pollster, stderrfno)       
542                                    outputclosed = 1
[1498]543                            else :   
544                                self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
545                                time.sleep(0.01)
546                               
[1495]547                        if mask & (select.POLLIN | select.POLLPRI) :     
548                            # We have something to read
549                            try :
550                                data = os.read(fd, MEGABYTE)
551                            except (IOError, OSError), msg :   
552                                self.logdebug("Error while reading file %s : %s" % (fd, msg))
553                            else :
554                                if fd == infno :
555                                    if not data :    # If yes, then no more input data
556                                        self.unregisterFileNo(pollster, infno)
[1606]557                                        self.logdebug("Input data ends.")
[1495]558                                        endinput = 1 # this happens with real files.
[1498]559                                    else :   
560                                        indata += data
[1897]561                                        totalfromcups += len(data)
562                                        self.logdebug("%s bytes read from CUPS so far..." % totalfromcups)
[1495]563                                elif fd == fromcfno :
[1498]564                                    if not data :
[1606]565                                        self.logdebug("No back channel data to read from real backend yet, sleeping a bit...")
[1498]566                                        time.sleep(0.01)
567                                    else :
568                                        outdata += data
[1897]569                                        totalfromchild += len(data)
570                                        self.logdebug("%s bytes read from real backend so far..." % totalfromchild)
[1498]571                                else :   
572                                    self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
573                                    time.sleep(0.01)
574                                   
[1495]575                        if mask & (select.POLLHUP | select.POLLERR) :
576                            # Treat POLLERR as an EOF.
577                            # Some standard I/O stream has no more datas
578                            self.unregisterFileNo(pollster, fd)
[1494]579                            if fd == infno :
[1495]580                                # Here we are in the case where the input file is stdin.
581                                # which has no more data to be read.
[1606]582                                self.logdebug("Input data ends.")
[1495]583                                endinput = 1
584                            elif fd == fromcfno :   
585                                # We are no more interested in this file descriptor       
[1606]586                                self.logdebug("Closing real backend's stdout+stderr.")
[1495]587                                os.close(fromcfno)
588                                endoutput = 1
[1498]589                            else :   
590                                self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
591                                time.sleep(0.01)
[1495]592                               
593                        if mask & select.POLLNVAL :       
[1606]594                            self.logdebug("File %s was closed. Unregistering from polling object." % fd)
[1495]595                            self.unregisterFileNo(pollster, fd)
596                    except IOError, msg :           
597                        self.logdebug("Got an IOError : %s" % msg) # we got signalled during an I/O
[1191]598               
[1494]599        # We must close the real backend's input stream
600        if killed and not inputclosed :
[1606]601            self.logdebug("Forcing close of real backend's stdin.")
[1494]602            os.close(tocfno)
603       
[1606]604        self.logdebug("Exiting streams polling loop...")
[1492]605       
[1897]606        self.logdebug("input data's final length : %s" % len(indata))
607        self.logdebug("back-channel data's final length : %s" % len(outdata))
608       
609        self.logdebug("Total bytes read from CUPS (job's datas) : %s" % totalfromcups)
610        self.logdebug("Total bytes sent to real backend (job's datas) : %s" % totaltochild)
611       
612        self.logdebug("Total bytes read from real backend (back-channel datas) : %s" % totalfromchild)
613        self.logdebug("Total bytes sent back to CUPS (back-channel datas) : %s" % totaltocups)
614       
[1494]615        # Check exit code of original CUPS backend.   
616        if status == -1 :
617            # we exited the loop before the real backend exited
618            # now we have to wait for it to finish and get its status
[1606]619            self.logdebug("Waiting for real backend to exit...")
[1494]620            try :
621                status = subprocess.wait()
[2054]622            except OSError : # already dead : TODO : detect when abnormal
[1494]623                status = 0
[1222]624        if os.WIFEXITED(status) :
625            retcode = os.WEXITSTATUS(status)
[1291]626        elif not killed :   
[1606]627            self.sendBackChannelData(_("CUPS backend %s died abnormally.") % realbackend, "error")
[1222]628            retcode = -1
[1291]629        else :   
630            retcode = self.removeJob()
[2006]631           
632        self.dropPriv()   
633       
[1271]634        return retcode   
[1222]635   
[1177]636if __name__ == "__main__" :   
637    # This is a CUPS backend, we should act and die like a CUPS backend
[1542]638    retcode = 0
[1177]639    if len(sys.argv) == 1 :
[1178]640        # we will execute each existing backend in device enumeration mode
641        # and generate their PyKota accounting counterpart
642        (directory, myname) = os.path.split(sys.argv[0])
643        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)] :
644            answer = os.popen(backend, "r")
645            try :
646                devices = [line.strip() for line in answer.readlines()]
647            except :   
648                devices = []
649            status = answer.close()
650            if status is None :
651                for d in devices :
[1180]652                    # each line is of the form : 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
653                    # so we have to decompose it carefully
[1178]654                    fdevice = cStringIO.StringIO("%s" % d)
655                    tokenizer = shlex.shlex(fdevice)
656                    tokenizer.wordchars = tokenizer.wordchars + r".:,?!~/\_$*-+={}[]()#"
657                    arguments = []
658                    while 1 :
659                        token = tokenizer.get_token()
660                        if token :
661                            arguments.append(token)
662                        else :
663                            break
664                    fdevice.close()
[1180]665                    try :
666                        (devicetype, device, name, fullname) = arguments
667                    except ValueError :   
668                        pass    # ignore this 'bizarre' device
669                    else :   
670                        if name.startswith('"') and name.endswith('"') :
671                            name = name[1:-1]
672                        if fullname.startswith('"') and fullname.endswith('"') :
673                            fullname = fullname[1:-1]
[1191]674                        print '%s cupspykota:%s "PyKota+%s" "PyKota managed %s"' % (devicetype, device, name, fullname)
[1542]675        retcode = 0               
[1177]676    elif len(sys.argv) not in (6, 7) :   
677        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n" % sys.argv[0])
678        retcode = 1
679    else :   
680        try :
[1542]681            try :
682                # Initializes the backend
683                kotabackend = PyKotaBackend()   
684            except SystemExit :   
685                retcode = -1
686            except :   
687                crashed("cupspykota backend initialization failed")
688                retcode = 1
689            else :   
690                retcode = kotabackend.mainWork()
691                kotabackend.storage.close()
692                kotabackend.closeJobDataStream()   
[1513]693        except :
[1517]694            try :
695                kotabackend.crashed("cupspykota backend failed")
696            except :   
[1542]697                crashed("cupspykota backend failed")
698            retcode = 1   
[1177]699       
700    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.