root / pykota / trunk / bin / cupspykota @ 2313

Revision 2313, 37.1 kB (checked in by jerome, 19 years ago)

Improved IPP parser.
Severity : Jamuel, you don't even need to give it a look ;-)

  • 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                if denyduplicates == 1 :
254                    self.printMoreInfo(user, printer, _("Job is a duplicate. Printing is denied."), "warn")
255                    action = "DENY"
256                else :   
257                    self.logdebug("Launching subprocess [%s] to see if dupes should be allowed or not." % denyduplicates)
258                    fanswer = os.popen(denyduplicates, "r")
259                    action = fanswer.read().strip().upper()
260                    fanswer.close()
261                    if action == "DENY" :     
262                        self.printMoreInfo(user, printer, _("Job is a duplicate. Printing is denied by subprocess."), "warn")
263                    else :   
264                        self.printMoreInfo(user, printer, _("Job is a duplicate. Printing is allowed by subprocess."), "warn")
265                        action = "ALLOW" # just to be sure, in the case the external command returns something else...
266            else :   
267                # checks the user's quota
268                action = self.warnUserPQuota(userpquota)
269           
270            # exports some new environment variables
271            os.environ["PYKOTAACTION"] = action
272           
273            # launches the pre hook
274            self.prehook(userpquota)
275
276            # saves the size of banners which have to be accounted for
277            # this is needed in the case of software accounting
278            bannersize = 0
279           
280            # handle starting banner pages before accounting
281            accountbanner = self.config.getAccountBanner(printer.Name)
282            if accountbanner in ["ENDING", "NONE"] :
283                if (action == 'DENY') and (userpquota.WarnCount >= self.config.getMaxDenyBanners(printer.Name)) :
284                    self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn")
285                else :
286                    if action == 'DENY' :
287                        self.logdebug("Incrementing the number of deny banners for user %s on printer %s" % (user.Name, printer.Name))
288                        userpquota.incDenyBannerCounter() # increments the warning counter
289                        self.exportUserInfo(userpquota)
290                    banner = self.startingBanner(printer.Name)
291                    if banner :
292                        self.logdebug("Printing starting banner before accounting begins.")
293                        self.handleData(banner)
294 
295            self.printMoreInfo(user, printer, _("Job accounting begins."))
296            self.accounter.beginJob(printer)
297           
298            # handle starting banner pages during accounting
299            if accountbanner in ["STARTING", "BOTH"] :
300                if (action == 'DENY') and (userpquota.WarnCount >= self.config.getMaxDenyBanners(printer.Name)) :
301                    self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn")
302                else :
303                    if action == 'DENY' :
304                        self.logdebug("Incrementing the number of deny banners for user %s on printer %s" % (user.Name, printer.Name))
305                        userpquota.incDenyBannerCounter() # increments the warning counter
306                        self.exportUserInfo(userpquota)
307                    banner = self.startingBanner(printer.Name)
308                    if banner :
309                        self.logdebug("Printing starting banner during accounting.")
310                        self.handleData(banner)
311                        if self.accounter.isSoftware :
312                            bannersize += 1 # TODO : fix this by passing the banner's content through PDLAnalyzer
313        else :   
314            action = "ALLOW"
315            os.environ["PYKOTAACTION"] = action
316           
317        # pass the job's data to the real backend   
318        if action in ["ALLOW", "WARN"] :
319            if self.gotSigTerm :
320                retcode = self.removeJob()
321            else :   
322                retcode = self.handleData()       
323        else :       
324            retcode = self.removeJob()
325       
326        if policy == "OK" :       
327            # indicate phase change
328            os.environ["PYKOTAPHASE"] = "AFTER"
329           
330            # handle ending banner pages during accounting
331            if accountbanner in ["ENDING", "BOTH"] :
332                if (action == 'DENY') and (userpquota.WarnCount >= self.config.getMaxDenyBanners(printer.Name)) :
333                    self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn")
334                else :
335                    if action == 'DENY' :
336                        self.logdebug("Incrementing the number of deny banners for user %s on printer %s" % (user.Name, printer.Name))
337                        userpquota.incDenyBannerCounter() # increments the warning counter
338                        self.exportUserInfo(userpquota)
339                    banner = self.endingBanner(printer.Name)
340                    if banner :
341                        self.logdebug("Printing ending banner during accounting.")
342                        self.handleData(banner)
343                        if self.accounter.isSoftware :
344                            bannersize += 1 # TODO : fix this by passing the banner's content through PDLAnalyzer
345 
346            # stops accounting.
347            self.accounter.endJob(printer)
348            self.printMoreInfo(user, printer, _("Job accounting ends."))
349               
350            # retrieve the job size   
351            if action == "DENY" :
352                jobsize = 0
353                self.printMoreInfo(user, printer, _("Job size forced to 0 because printing is denied."))
354            else :   
355                userpquota.resetDenyBannerCounter()
356                jobsize = self.accounter.getJobSize(printer)
357                if self.softwareJobSize and (jobsize != self.softwareJobSize) :
358                    self.printInfo(_("Beware : computed job size (%s) != precomputed job size (%s)") % (jobsize, self.softwareJobSize), "error")
359                    (limit, replacement) = self.config.getTrustJobSize(printer.Name)
360                    if limit is None :
361                        self.printInfo(_("The job size will be trusted anyway according to the 'trustjobsize' directive"), "warn")
362                    else :
363                        if jobsize <= limit :
364                            self.printInfo(_("The job size will be trusted because it is inferior to the 'trustjobsize' directive's limit %s") % limit, "warn")
365                        else :
366                            self.printInfo(_("The job size will be modified according to the 'trustjobsize' directive : %s") % replacement, "warn")
367                            if replacement == "PRECOMPUTED" :
368                                jobsize = self.softwareJobSize
369                            else :   
370                                jobsize = replacement
371                jobsize += bannersize   
372            self.printMoreInfo(user, printer, _("Job size : %i") % jobsize)
373           
374            # update the quota for the current user on this printer
375            self.printInfo(_("Updating user %s's quota on printer %s") % (user.Name, printer.Name))
376            jobprice = userpquota.increasePagesUsage(jobsize)
377           
378            # adds the current job to history   
379            printer.addJobToHistory(self.jobid, user, self.accounter.getLastPageCounter(), \
380                                    action, jobsize, jobprice, self.preserveinputfile, \
381                                    self.title, self.copies, self.options, clienthost, \
382                                    self.jobSizeBytes, self.checksum, None, billingcode)
383            self.printMoreInfo(user, printer, _("Job added to history."))
384           
385            # exports some new environment variables
386            os.environ["PYKOTAJOBSIZE"] = str(jobsize)
387            os.environ["PYKOTAJOBPRICE"] = str(jobprice)
388           
389            # then re-export user information with new value
390            self.exportUserInfo(userpquota)
391           
392            # handle ending banner pages after accounting ends
393            if accountbanner in ["STARTING", "NONE"] :
394                if (action == 'DENY') and (userpquota.WarnCount >= self.config.getMaxDenyBanners(printer.Name)) :
395                    self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn")
396                else :
397                    if action == 'DENY' :
398                        self.logdebug("Incrementing the number of deny banners for user %s on printer %s" % (user.Name, printer.Name))
399                        userpquota.incDenyBannerCounter() # increments the warning counter
400                        self.exportUserInfo(userpquota)
401                    banner = self.endingBanner(printer.Name)
402                    if banner :
403                        self.logdebug("Printing ending banner after accounting ends.")
404                        self.handleData(banner)
405                       
406            # Launches the post hook
407            self.posthook(userpquota)
408           
409        return retcode   
410               
411    def unregisterFileNo(self, pollobj, fileno) :               
412        """Removes a file handle from the polling object."""
413        try :
414            pollobj.unregister(fileno)
415        except KeyError :   
416            self.printInfo(_("File number %s unregistered twice from polling object, ignored.") % fileno, "warn")
417        except :   
418            self.logdebug("Error while unregistering file number %s from polling object." % fileno)
419        else :   
420            self.logdebug("File number %s unregistered from polling object." % fileno)
421           
422    def formatFileEvent(self, fd, mask) :       
423        """Formats file debug info."""
424        maskval = []
425        if mask & select.POLLIN :
426            maskval.append("POLLIN")
427        if mask & select.POLLOUT :
428            maskval.append("POLLOUT")
429        if mask & select.POLLPRI :
430            maskval.append("POLLPRI")
431        if mask & select.POLLERR :
432            maskval.append("POLLERR")
433        if mask & select.POLLHUP :
434            maskval.append("POLLHUP")
435        if mask & select.POLLNVAL :
436            maskval.append("POLLNVAL")
437        return "%s (%s)" % (fd, " | ".join(maskval))
438       
439    def handleData(self, filehandle=None) :
440        """Pass the job's data to the real backend."""
441        # Find the real backend pathname   
442        realbackend = os.path.join(os.path.split(sys.argv[0])[0], self.originalbackend)
443       
444        # And launch it
445        if filehandle is None :
446            arguments = sys.argv
447        else :   
448            # Here we absolutely WANT to remove any filename from the command line !
449            arguments = [ "Fake this because we are printing a banner" ] + sys.argv[1:6]
450           
451        self.regainPriv()   
452       
453        self.logdebug("Starting real backend %s with args %s" % (realbackend, " ".join(['"%s"' % a for a in ([os.environ["DEVICE_URI"]] + arguments[1:])])))
454        subprocess = PyKotaPopen4([realbackend] + arguments[1:], bufsize=0, arg0=os.environ["DEVICE_URI"])
455       
456        # Save file descriptors, we will need them later.
457        stderrfno = sys.stderr.fileno()
458        fromcfno = subprocess.fromchild.fileno()
459        tocfno = subprocess.tochild.fileno()
460       
461        # We will have to be careful when dealing with I/O
462        # So we use a poll object to know when to read or write
463        pollster = select.poll()
464        pollster.register(fromcfno, select.POLLIN | select.POLLPRI)
465        pollster.register(stderrfno, select.POLLOUT)
466        pollster.register(tocfno, select.POLLOUT)
467       
468        # Initialize our buffers
469        indata = ""
470        outdata = ""
471        endinput = endoutput = 0
472        inputclosed = outputclosed = 0
473        totaltochild = totalfromcups = 0
474        totalfromchild = totaltocups = 0
475       
476        if filehandle is None:
477            if self.preserveinputfile is None :
478               # this is not a real file, we read the job's data
479                # from our temporary file which is a copy of stdin
480                infno = self.jobdatastream.fileno()
481                self.jobdatastream.seek(0)
482                pollster.register(infno, select.POLLIN | select.POLLPRI)
483            else :   
484                # job's data is in a file, no need to pass the data
485                # to the real backend
486                self.logdebug("Job's data is in %s" % self.preserveinputfile)
487                infno = None
488                endinput = 1
489        else:
490            self.logdebug("Printing data passed from filehandle")
491            indata = filehandle.read()
492            infno = None
493            endinput = 1
494            filehandle.close()
495       
496        self.logdebug("Entering streams polling loop...")
497        MEGABYTE = 1024*1024
498        killed = 0
499        status = -1
500        while (status == -1) and (not killed) and not (inputclosed and outputclosed) :
501            # First check if original backend is still alive
502            status = subprocess.poll()
503           
504            # Now if we got SIGTERM, we have
505            # to kill -TERM the original backend
506            if self.gotSigTerm and not killed :
507                try :
508                    os.kill(subprocess.pid, signal.SIGTERM)
509                except OSError, msg : # ignore but logs if process was already killed.
510                    self.logdebug("Error while sending signal to pid %s : %s" % (subprocess.pid, msg))
511                else :   
512                    self.printInfo(_("SIGTERM was sent to real backend %s (pid: %s)") % (realbackend, subprocess.pid))
513                    killed = 1
514           
515            # In any case, deal with any remaining I/O
516            try :
517                availablefds = pollster.poll(5000)
518            except select.error, msg :   
519                self.logdebug("Interrupted poll : %s" % msg)
520                availablefds = []
521            if not availablefds :
522                self.logdebug("Nothing to do, sleeping a bit...")
523                time.sleep(0.01) # give some time to the system
524            else :
525                for (fd, mask) in availablefds :
526                    # self.logdebug(self.formatFileEvent(fd, mask))
527                    try :
528                        if mask & select.POLLOUT :
529                            # We can write
530                            if fd == tocfno :
531                                if indata :
532                                    try :
533                                        nbwritten = os.write(fd, indata)   
534                                    except (OSError, IOError), msg :   
535                                        self.logdebug("Error while writing to real backend's stdin %s : %s" % (fd, msg))
536                                    else :   
537                                        if len(indata) != nbwritten :
538                                            self.logdebug("Short write to real backend's input !")
539                                        totaltochild += nbwritten   
540                                        self.logdebug("%s bytes sent to real backend so far..." % totaltochild)
541                                        indata = indata[nbwritten:]
542                                else :       
543                                    self.logdebug("No data to send to real backend yet, sleeping a bit...")
544                                    time.sleep(0.01)
545                                   
546                                if endinput :   
547                                    self.unregisterFileNo(pollster, tocfno)       
548                                    self.logdebug("Closing real backend's stdin.")
549                                    os.close(tocfno)
550                                    inputclosed = 1
551                            elif fd == stderrfno :
552                                if outdata :
553                                    try :
554                                        nbwritten = os.write(fd, outdata)
555                                    except (OSError, IOError), msg :   
556                                        self.logdebug("Error while writing to CUPS back channel (stderr) %s : %s" % (fd, msg))
557                                    else :
558                                        if len(outdata) != nbwritten :
559                                            self.logdebug("Short write to stderr (CUPS) !")
560                                        totaltocups += nbwritten   
561                                        self.logdebug("%s bytes sent back to CUPS so far..." % totaltocups)
562                                        outdata = outdata[nbwritten:]
563                                else :       
564                                    # self.logdebug("No data to send back to CUPS yet, sleeping a bit...") # Uncommenting this fills your logs
565                                    time.sleep(0.01) # Give some time to the system, stderr is ALWAYS writeable it seems.
566                                   
567                                if endoutput :   
568                                    self.unregisterFileNo(pollster, stderrfno)       
569                                    outputclosed = 1
570                            else :   
571                                self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
572                                time.sleep(0.01)
573                               
574                        if mask & (select.POLLIN | select.POLLPRI) :     
575                            # We have something to read
576                            try :
577                                data = os.read(fd, MEGABYTE)
578                            except (IOError, OSError), msg :   
579                                self.logdebug("Error while reading file %s : %s" % (fd, msg))
580                            else :
581                                if fd == infno :
582                                    if not data :    # If yes, then no more input data
583                                        self.unregisterFileNo(pollster, infno)
584                                        self.logdebug("Input data ends.")
585                                        endinput = 1 # this happens with real files.
586                                    else :   
587                                        indata += data
588                                        totalfromcups += len(data)
589                                        self.logdebug("%s bytes read from CUPS so far..." % totalfromcups)
590                                elif fd == fromcfno :
591                                    if not data :
592                                        self.logdebug("No back channel data to read from real backend yet, sleeping a bit...")
593                                        time.sleep(0.01)
594                                    else :
595                                        outdata += data
596                                        totalfromchild += len(data)
597                                        self.logdebug("%s bytes read from real backend so far..." % totalfromchild)
598                                else :   
599                                    self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
600                                    time.sleep(0.01)
601                                   
602                        if mask & (select.POLLHUP | select.POLLERR) :
603                            # Treat POLLERR as an EOF.
604                            # Some standard I/O stream has no more datas
605                            self.unregisterFileNo(pollster, fd)
606                            if fd == infno :
607                                # Here we are in the case where the input file is stdin.
608                                # which has no more data to be read.
609                                self.logdebug("Input data ends.")
610                                endinput = 1
611                            elif fd == fromcfno :   
612                                # We are no more interested in this file descriptor       
613                                self.logdebug("Closing real backend's stdout+stderr.")
614                                os.close(fromcfno)
615                                endoutput = 1
616                            else :   
617                                self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
618                                time.sleep(0.01)
619                               
620                        if mask & select.POLLNVAL :       
621                            self.logdebug("File %s was closed. Unregistering from polling object." % fd)
622                            self.unregisterFileNo(pollster, fd)
623                    except IOError, msg :           
624                        self.logdebug("Got an IOError : %s" % msg) # we got signalled during an I/O
625               
626        # We must close the real backend's input stream
627        if killed and not inputclosed :
628            self.logdebug("Forcing close of real backend's stdin.")
629            os.close(tocfno)
630       
631        self.logdebug("Exiting streams polling loop...")
632       
633        self.logdebug("input data's final length : %s" % len(indata))
634        self.logdebug("back-channel data's final length : %s" % len(outdata))
635       
636        self.logdebug("Total bytes read from CUPS (job's datas) : %s" % totalfromcups)
637        self.logdebug("Total bytes sent to real backend (job's datas) : %s" % totaltochild)
638       
639        self.logdebug("Total bytes read from real backend (back-channel datas) : %s" % totalfromchild)
640        self.logdebug("Total bytes sent back to CUPS (back-channel datas) : %s" % totaltocups)
641       
642        # Check exit code of original CUPS backend.   
643        if status == -1 :
644            # we exited the loop before the real backend exited
645            # now we have to wait for it to finish and get its status
646            self.logdebug("Waiting for real backend to exit...")
647            try :
648                status = subprocess.wait()
649            except OSError : # already dead : TODO : detect when abnormal
650                status = 0
651        if os.WIFEXITED(status) :
652            retcode = os.WEXITSTATUS(status)
653        elif not killed :   
654            self.sendBackChannelData(_("CUPS backend %s died abnormally.") % realbackend, "error")
655            retcode = -1
656        else :   
657            retcode = self.removeJob()
658           
659        self.dropPriv()   
660       
661        return retcode   
662   
663if __name__ == "__main__" :   
664    # This is a CUPS backend, we should act and die like a CUPS backend
665    retcode = 0
666    if len(sys.argv) == 1 :
667        (directory, myname) = os.path.split(sys.argv[0])
668        tmpdir = tempfile.gettempdir()
669        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
670        if os.path.exists(lockfilename) :
671            # there's already a lockfile, see if still used
672            lockfile = open(lockfilename, "r")
673            pid = int(lockfile.read())
674            lockfile.close()
675            try :
676                # see if the pid contained in the lock file is still running
677                os.kill(pid, 0)
678            except OSError, e :   
679                if e.errno != errno.EPERM :
680                    # process doesn't exist anymore, remove the lock
681                    os.remove(lockfilename)
682           
683        if not os.path.exists(lockfilename) :
684            lockfile = open(lockfilename, "w")
685            lockfile.write("%i" % os.getpid())
686            lockfile.close()
687            # we will execute each existing backend in device enumeration mode
688            # and generate their PyKota accounting counterpart
689            allbackends = [ os.path.join(directory, b) \
690                                for b in os.listdir(directory) 
691                                    if os.access(os.path.join(directory, b), os.X_OK) \
692                                        and (b != myname)] 
693            for backend in allbackends :                           
694                answer = os.popen(backend, "r")
695                try :
696                    devices = [line.strip() for line in answer.readlines()]
697                except :   
698                    devices = []
699                status = answer.close()
700                if status is None :
701                    for d in devices :
702                        # each line is of the form : 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
703                        # so we have to decompose it carefully
704                        fdevice = cStringIO.StringIO("%s" % d)
705                        tokenizer = shlex.shlex(fdevice)
706                        tokenizer.wordchars = tokenizer.wordchars + r".:,?!~/\_$*-+={}[]()#"
707                        arguments = []
708                        while 1 :
709                            token = tokenizer.get_token()
710                            if token :
711                                arguments.append(token)
712                            else :
713                                break
714                        fdevice.close()
715                        try :
716                            (devicetype, device, name, fullname) = arguments
717                        except ValueError :   
718                            pass    # ignore this 'bizarre' device
719                        else :   
720                            if name.startswith('"') and name.endswith('"') :
721                                name = name[1:-1]
722                            if fullname.startswith('"') and fullname.endswith('"') :
723                                fullname = fullname[1:-1]
724                            print '%s cupspykota:%s "PyKota+%s" "PyKota managed %s"' % (devicetype, device, name, fullname)
725            os.remove(lockfilename)
726        retcode = 0               
727    elif len(sys.argv) not in (6, 7) :   
728        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n" % sys.argv[0])
729        retcode = 1
730    else :   
731        try :
732            # Initializes the backend
733            kotabackend = PyKotaBackend()   
734            kotabackend.deferredInit()
735            retcode = kotabackend.mainWork()
736            kotabackend.storage.close()
737            kotabackend.closeJobDataStream()   
738        except SystemExit :   
739            retcode = -1
740        except :
741            try :
742                kotabackend.crashed("cupspykota backend failed")
743            except :   
744                crashed("cupspykota backend failed")
745            retcode = 1   
746       
747    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.