root / pykota / trunk / bin / cupspykota @ 2398

Revision 2398, 39.8 kB (checked in by jerome, 19 years ago)

Fix for people who tell Samba to add the correct job-originating-host-name attribute
to the job ticket : we will always use the latest value seen if there's more than one.

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