root / pykota / trunk / bin / cupspykota @ 2147

Revision 2147, 34.5 kB (checked in by jerome, 19 years ago)

Removed all references to $Log$

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