root / pykota / trunk / bin / cupspykota @ 2308

Revision 2308, 37.1 kB (checked in by jerome, 19 years ago)

Extended syntax for the denyduplicates directive : it now
allows any external command to be launched, which will decide
if the dupes should be allowed or not.

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