root / pykota / trunk / bin / cupspykota @ 2396

Revision 2396, 39.6 kB (checked in by jerome, 19 years ago)

Fixed username in the case it was overwritten because of the overwrite_jobticket
directive.

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