root / pykota / trunk / bin / cupspykota @ 2395

Revision 2395, 39.5 kB (checked in by jerome, 19 years ago)

Moved some code around so that when the job ticket can be overwritten, the
client hostname is known. This allows easier client/server user interactivity :-)
Severity : high, if you downloaded the code 5 minutes ago :-)

  • 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           
499        self.regainPriv()   
500       
501        self.logdebug("Starting real backend %s with args %s" % (realbackend, " ".join(['"%s"' % a for a in ([os.environ["DEVICE_URI"]] + arguments[1:])])))
502        subprocess = PyKotaPopen4([realbackend] + arguments[1:], bufsize=0, arg0=os.environ["DEVICE_URI"])
503       
504        # Save file descriptors, we will need them later.
505        stderrfno = sys.stderr.fileno()
506        fromcfno = subprocess.fromchild.fileno()
507        tocfno = subprocess.tochild.fileno()
508       
509        # We will have to be careful when dealing with I/O
510        # So we use a poll object to know when to read or write
511        pollster = select.poll()
512        pollster.register(fromcfno, select.POLLIN | select.POLLPRI)
513        pollster.register(stderrfno, select.POLLOUT)
514        pollster.register(tocfno, select.POLLOUT)
515       
516        # Initialize our buffers
517        indata = ""
518        outdata = ""
519        endinput = endoutput = 0
520        inputclosed = outputclosed = 0
521        totaltochild = totalfromcups = 0
522        totalfromchild = totaltocups = 0
523       
524        if filehandle is None:
525            if self.preserveinputfile is None :
526               # this is not a real file, we read the job's data
527                # from our temporary file which is a copy of stdin
528                infno = self.jobdatastream.fileno()
529                self.jobdatastream.seek(0)
530                pollster.register(infno, select.POLLIN | select.POLLPRI)
531            else :   
532                # job's data is in a file, no need to pass the data
533                # to the real backend
534                self.logdebug("Job's data is in %s" % self.preserveinputfile)
535                infno = None
536                endinput = 1
537        else:
538            self.logdebug("Printing data passed from filehandle")
539            indata = filehandle.read()
540            infno = None
541            endinput = 1
542            filehandle.close()
543       
544        self.logdebug("Entering streams polling loop...")
545        MEGABYTE = 1024*1024
546        killed = 0
547        status = -1
548        while (status == -1) and (not killed) and not (inputclosed and outputclosed) :
549            # First check if original backend is still alive
550            status = subprocess.poll()
551           
552            # Now if we got SIGTERM, we have
553            # to kill -TERM the original backend
554            if self.gotSigTerm and not killed :
555                try :
556                    os.kill(subprocess.pid, signal.SIGTERM)
557                except OSError, msg : # ignore but logs if process was already killed.
558                    self.logdebug("Error while sending signal to pid %s : %s" % (subprocess.pid, msg))
559                else :   
560                    self.printInfo(_("SIGTERM was sent to real backend %s (pid: %s)") % (realbackend, subprocess.pid))
561                    killed = 1
562           
563            # In any case, deal with any remaining I/O
564            try :
565                availablefds = pollster.poll(5000)
566            except select.error, msg :   
567                self.logdebug("Interrupted poll : %s" % msg)
568                availablefds = []
569            if not availablefds :
570                self.logdebug("Nothing to do, sleeping a bit...")
571                time.sleep(0.01) # give some time to the system
572            else :
573                for (fd, mask) in availablefds :
574                    # self.logdebug(self.formatFileEvent(fd, mask))
575                    try :
576                        if mask & select.POLLOUT :
577                            # We can write
578                            if fd == tocfno :
579                                if indata :
580                                    try :
581                                        nbwritten = os.write(fd, indata)   
582                                    except (OSError, IOError), msg :   
583                                        self.logdebug("Error while writing to real backend's stdin %s : %s" % (fd, msg))
584                                    else :   
585                                        if len(indata) != nbwritten :
586                                            self.logdebug("Short write to real backend's input !")
587                                        totaltochild += nbwritten   
588                                        self.logdebug("%s bytes sent to real backend so far..." % totaltochild)
589                                        indata = indata[nbwritten:]
590                                else :       
591                                    self.logdebug("No data to send to real backend yet, sleeping a bit...")
592                                    time.sleep(0.01)
593                                   
594                                if endinput :   
595                                    self.unregisterFileNo(pollster, tocfno)       
596                                    self.logdebug("Closing real backend's stdin.")
597                                    os.close(tocfno)
598                                    inputclosed = 1
599                            elif fd == stderrfno :
600                                if outdata :
601                                    try :
602                                        nbwritten = os.write(fd, outdata)
603                                    except (OSError, IOError), msg :   
604                                        self.logdebug("Error while writing to CUPS back channel (stderr) %s : %s" % (fd, msg))
605                                    else :
606                                        if len(outdata) != nbwritten :
607                                            self.logdebug("Short write to stderr (CUPS) !")
608                                        totaltocups += nbwritten   
609                                        self.logdebug("%s bytes sent back to CUPS so far..." % totaltocups)
610                                        outdata = outdata[nbwritten:]
611                                else :       
612                                    # self.logdebug("No data to send back to CUPS yet, sleeping a bit...") # Uncommenting this fills your logs
613                                    time.sleep(0.01) # Give some time to the system, stderr is ALWAYS writeable it seems.
614                                   
615                                if endoutput :   
616                                    self.unregisterFileNo(pollster, stderrfno)       
617                                    outputclosed = 1
618                            else :   
619                                self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
620                                time.sleep(0.01)
621                               
622                        if mask & (select.POLLIN | select.POLLPRI) :     
623                            # We have something to read
624                            try :
625                                data = os.read(fd, MEGABYTE)
626                            except (IOError, OSError), msg :   
627                                self.logdebug("Error while reading file %s : %s" % (fd, msg))
628                            else :
629                                if fd == infno :
630                                    if not data :    # If yes, then no more input data
631                                        self.unregisterFileNo(pollster, infno)
632                                        self.logdebug("Input data ends.")
633                                        endinput = 1 # this happens with real files.
634                                    else :   
635                                        indata += data
636                                        totalfromcups += len(data)
637                                        self.logdebug("%s bytes read from CUPS so far..." % totalfromcups)
638                                elif fd == fromcfno :
639                                    if not data :
640                                        self.logdebug("No back channel data to read from real backend yet, sleeping a bit...")
641                                        time.sleep(0.01)
642                                    else :
643                                        outdata += data
644                                        totalfromchild += len(data)
645                                        self.logdebug("%s bytes read from real backend so far..." % totalfromchild)
646                                else :   
647                                    self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
648                                    time.sleep(0.01)
649                                   
650                        if mask & (select.POLLHUP | select.POLLERR) :
651                            # Treat POLLERR as an EOF.
652                            # Some standard I/O stream has no more datas
653                            self.unregisterFileNo(pollster, fd)
654                            if fd == infno :
655                                # Here we are in the case where the input file is stdin.
656                                # which has no more data to be read.
657                                self.logdebug("Input data ends.")
658                                endinput = 1
659                            elif fd == fromcfno :   
660                                # We are no more interested in this file descriptor       
661                                self.logdebug("Closing real backend's stdout+stderr.")
662                                os.close(fromcfno)
663                                endoutput = 1
664                            else :   
665                                self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
666                                time.sleep(0.01)
667                               
668                        if mask & select.POLLNVAL :       
669                            self.logdebug("File %s was closed. Unregistering from polling object." % fd)
670                            self.unregisterFileNo(pollster, fd)
671                    except IOError, msg :           
672                        self.logdebug("Got an IOError : %s" % msg) # we got signalled during an I/O
673               
674        # We must close the real backend's input stream
675        if killed and not inputclosed :
676            self.logdebug("Forcing close of real backend's stdin.")
677            os.close(tocfno)
678       
679        self.logdebug("Exiting streams polling loop...")
680       
681        self.logdebug("input data's final length : %s" % len(indata))
682        self.logdebug("back-channel data's final length : %s" % len(outdata))
683       
684        self.logdebug("Total bytes read from CUPS (job's datas) : %s" % totalfromcups)
685        self.logdebug("Total bytes sent to real backend (job's datas) : %s" % totaltochild)
686       
687        self.logdebug("Total bytes read from real backend (back-channel datas) : %s" % totalfromchild)
688        self.logdebug("Total bytes sent back to CUPS (back-channel datas) : %s" % totaltocups)
689       
690        # Check exit code of original CUPS backend.   
691        if status == -1 :
692            # we exited the loop before the real backend exited
693            # now we have to wait for it to finish and get its status
694            self.logdebug("Waiting for real backend to exit...")
695            try :
696                status = subprocess.wait()
697            except OSError : # already dead : TODO : detect when abnormal
698                status = 0
699        if os.WIFEXITED(status) :
700            retcode = os.WEXITSTATUS(status)
701        elif not killed :   
702            self.sendBackChannelData(_("CUPS backend %s died abnormally.") % realbackend, "error")
703            retcode = -1
704        else :   
705            retcode = self.removeJob()
706           
707        self.dropPriv()   
708       
709        return retcode   
710   
711if __name__ == "__main__" :   
712    # This is a CUPS backend, we should act and die like a CUPS backend
713    retcode = 0
714    if len(sys.argv) == 1 :
715        (directory, myname) = os.path.split(sys.argv[0])
716        tmpdir = tempfile.gettempdir()
717        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
718        if os.path.exists(lockfilename) :
719            # there's already a lockfile, see if still used
720            lockfile = open(lockfilename, "r")
721            pid = int(lockfile.read())
722            lockfile.close()
723            try :
724                # see if the pid contained in the lock file is still running
725                os.kill(pid, 0)
726            except OSError, e :   
727                if e.errno != errno.EPERM :
728                    # process doesn't exist anymore, remove the lock
729                    os.remove(lockfilename)
730           
731        if not os.path.exists(lockfilename) :
732            lockfile = open(lockfilename, "w")
733            lockfile.write("%i" % os.getpid())
734            lockfile.close()
735            # we will execute each existing backend in device enumeration mode
736            # and generate their PyKota accounting counterpart
737            allbackends = [ os.path.join(directory, b) \
738                                for b in os.listdir(directory) 
739                                    if os.access(os.path.join(directory, b), os.X_OK) \
740                                        and (b != myname)] 
741            for backend in allbackends :                           
742                answer = os.popen(backend, "r")
743                try :
744                    devices = [line.strip() for line in answer.readlines()]
745                except :   
746                    devices = []
747                status = answer.close()
748                if status is None :
749                    for d in devices :
750                        # each line is of the form : 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
751                        # so we have to decompose it carefully
752                        fdevice = cStringIO.StringIO("%s" % d)
753                        tokenizer = shlex.shlex(fdevice)
754                        tokenizer.wordchars = tokenizer.wordchars + r".:,?!~/\_$*-+={}[]()#"
755                        arguments = []
756                        while 1 :
757                            token = tokenizer.get_token()
758                            if token :
759                                arguments.append(token)
760                            else :
761                                break
762                        fdevice.close()
763                        try :
764                            (devicetype, device, name, fullname) = arguments
765                        except ValueError :   
766                            pass    # ignore this 'bizarre' device
767                        else :   
768                            if name.startswith('"') and name.endswith('"') :
769                                name = name[1:-1]
770                            if fullname.startswith('"') and fullname.endswith('"') :
771                                fullname = fullname[1:-1]
772                            print '%s cupspykota:%s "PyKota+%s" "PyKota managed %s"' % (devicetype, device, name, fullname)
773            os.remove(lockfilename)
774        retcode = 0               
775    elif len(sys.argv) not in (6, 7) :   
776        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n" % sys.argv[0])
777        retcode = 1
778    else :   
779        try :
780            # Initializes the backend
781            kotabackend = PyKotaBackend()   
782            kotabackend.deferredInit()
783            retcode = kotabackend.mainWork()
784            kotabackend.storage.close()
785            kotabackend.closeJobDataStream()   
786        except SystemExit :   
787            retcode = -1
788        except :
789            try :
790                kotabackend.crashed("cupspykota backend failed")
791            except :   
792                crashed("cupspykota backend failed")
793            retcode = 1   
794       
795    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.