root / pykota / trunk / bin / cupspykota @ 2203

Revision 2177, 35.7 kB (checked in by jerome, 20 years ago)

Regaining priviledges wasn't done at the correct time so
parsing cupsd.conf was impossible with some security settings.

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