root / pykota / trunk / bin / cupspykota @ 2258

Revision 2254, 36.3 kB (checked in by jerome, 19 years ago)

Integrated the major rewrite of ipp.py into PyKota.
Now the job-originating-hostname is correctly retrieved
in all cases I've seen.

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