root / pykota / trunk / bin / cupspykota @ 2388

Revision 2388, 38.9 kB (checked in by jerome, 19 years ago)

Fixed an LDAP filtering problem when several billing codes were passed on pkbcodes' command line.
The unknown_billingcode directive now works as expected.
The billing code's page counter and balance are updated when printing.
Severity : If you need full management of billing codes, this is for you.

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