root / pykota / trunk / bin / cupspykota @ 2396

Revision 2396, 39.6 kB (checked in by jerome, 19 years ago)

Fixed username in the case it was overwritten because of the overwrite_jobticket
directive.

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