root / pykota / tags / 1.22 / bin / cupspykota @ 2240

Revision 2240, 35.9 kB (checked in by jerome, 19 years ago)

Backported fix

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