root / pykota / trunk / bin / cupspykota @ 2394

Revision 2393, 39.2 kB (checked in by jerome, 19 years ago)

Added the overwrite_jobticket directive.
Severity : How Powerful !!! :-)

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