root / pykota / trunk / bin / cupspykota @ 2366

Revision 2314, 37.5 kB (checked in by jerome, 19 years ago)

Added a comment about dupes.
Severity : improvement to do when all the rest is done :-)

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