root / pykota / trunk / bin / cupspykota @ 1713

Revision 1713, 30.4 kB (checked in by jalet, 20 years ago)

Added fix for incorrect job's size when hardware accounting fails

  • 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 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
22#
23# $Id$
24#
25# $Log$
26# Revision 1.72  2004/09/13 16:02:44  jalet
27# Added fix for incorrect job's size when hardware accounting fails
28#
29# Revision 1.71  2004/09/06 17:05:06  jalet
30# Fix for autodetection of SC_OPEN_MAX
31#
32# Revision 1.70  2004/07/26 09:20:27  jalet
33# Unneeded module
34#
35# Revision 1.69  2004/07/22 22:41:47  jalet
36# Hardware accounting for LPRng should be OK now. UNTESTED.
37#
38# Revision 1.68  2004/07/20 22:19:44  jalet
39# Sanitized a bit + use of gettext
40#
41# Revision 1.67  2004/07/16 12:22:45  jalet
42# LPRng support early version
43#
44# Revision 1.66  2004/07/01 19:56:25  jalet
45# Better dispatching of error messages
46#
47# Revision 1.65  2004/06/22 09:31:17  jalet
48# Always send some debug info to CUPS' back channel stream (stderr) as
49# informationnal messages.
50#
51# Revision 1.64  2004/06/18 13:34:46  jalet
52# Now all tracebacks include PyKota's version number
53#
54# Revision 1.63  2004/06/17 13:26:50  jalet
55# Better exception handling code
56#
57# Revision 1.62  2004/06/16 20:56:34  jalet
58# Smarter initialisation code
59#
60# Revision 1.61  2004/06/08 09:00:04  jalet
61# Fixed problem when username was passed in uppercase from Samba and we
62# tried to find correct line in CUPS page_log to extract the hostname.
63#
64# Revision 1.60  2004/06/03 23:14:08  jalet
65# Now stores the job's size in bytes in the database.
66# Preliminary work on payments storage : database schemas are OK now,
67# but no code to store payments yet.
68# Removed schema picture, not relevant anymore.
69#
70# Revision 1.59  2004/06/03 22:12:53  jalet
71# Now denies empty jobs
72#
73# Revision 1.58  2004/06/03 21:50:33  jalet
74# Improved error logging.
75# crashrecipient directive added.
76# Now exports the job's size in bytes too.
77#
78# Revision 1.57  2004/06/02 22:18:07  jalet
79# I think the bug when cancelling jobs should be fixed right now
80#
81# Revision 1.56  2004/06/02 21:50:56  jalet
82# Moved the sigterm capturing elsewhere
83#
84# Revision 1.55  2004/06/02 14:25:07  jalet
85# Should correctly capture ALL errors now
86#
87# Revision 1.54  2004/05/26 16:44:48  jalet
88# Now logs something when client hostname can't be extracted
89#
90# Revision 1.53  2004/05/26 14:49:35  jalet
91# First try at saving the job-originating-hostname in the database
92#
93# Revision 1.52  2004/05/25 09:15:13  jalet
94# accounter.py : old code deleted
95# the rest : now exports PYKOTAPRECOMPUTEDJOBSIZE and PYKOTAPRECOMPUTEDJOBPRICE
96#
97# Revision 1.51  2004/05/25 08:31:16  jalet
98# Heavy CPU usage seems to be fixed at least !
99#
100# Revision 1.50  2004/05/25 05:17:50  jalet
101# Now precomputes the job's size only if current printer's enforcement
102# is "STRICT"
103#
104# Revision 1.49  2004/05/24 22:45:48  jalet
105# New 'enforcement' directive added
106# Polling loop improvements
107#
108# Revision 1.48  2004/05/24 14:36:24  jalet
109# Revert to old polling loop. Will need optimisations
110#
111# Revision 1.47  2004/05/24 11:59:46  jalet
112# More robust (?) code
113#
114# Revision 1.46  2004/05/21 22:02:51  jalet
115# Preliminary work on pre-accounting
116#
117# Revision 1.45  2004/05/19 07:15:32  jalet
118# Could the 'misterious' bug in my loop be finally fixed ???
119#
120# Revision 1.44  2004/05/18 14:48:47  jalet
121# Big code changes to completely remove the need for "requester" directives,
122# jsut use "hardware(... your previous requester directive's content ...)"
123#
124# Revision 1.43  2004/05/17 11:46:05  jalet
125# First try at cupspykota's main loop rewrite
126#
127# Revision 1.42  2004/05/10 11:22:28  jalet
128# Typo
129#
130# Revision 1.41  2004/05/10 10:07:30  jalet
131# Catches OSError while reading
132#
133# Revision 1.40  2004/05/10 09:29:48  jalet
134# Should be more robust if we receive a SIGTERM during an I/O operation
135#
136# Revision 1.39  2004/05/07 14:44:53  jalet
137# Fix for file handles unregistered twice from the polling object
138#
139# Revision 1.38  2004/04/09 22:24:46  jalet
140# Began work on correct handling of child processes when jobs are cancelled by
141# the user. Especially important when an external requester is running for a
142# long time.
143#
144# Revision 1.37  2004/03/18 19:11:25  jalet
145# Fix for raw jobs in cupspykota
146#
147# Revision 1.36  2004/03/18 14:03:18  jalet
148# Added fsync() calls
149#
150# Revision 1.35  2004/03/16 12:05:01  jalet
151# Small fix for new waitprinter.sh : when job was denied, would wait forever
152# for printer being in printing mode.
153#
154# Revision 1.34  2004/03/15 10:47:56  jalet
155# This time the traceback formatting should be correct !
156#
157# Revision 1.33  2004/03/05 12:46:07  jalet
158# Improve tracebacks
159#
160# Revision 1.32  2004/03/05 12:31:35  jalet
161# Now should output full traceback when crashing
162#
163# Revision 1.31  2004/03/01 14:35:56  jalet
164# PYKOTAPHASE wasn't set soon enough at the start of the job
165#
166# Revision 1.30  2004/03/01 14:34:15  jalet
167# PYKOTAPHASE wasn't set at the right time at the end of data transmission
168# to underlying layer (real backend)
169#
170# Revision 1.29  2004/03/01 11:23:25  jalet
171# Pre and Post hooks to external commands are available in the cupspykota
172# backend. Forthe pykota filter they will be implemented real soon now.
173#
174# Revision 1.28  2004/02/26 14:18:07  jalet
175# Should fix the remaining bugs wrt printers groups and users groups.
176#
177# Revision 1.27  2004/02/04 23:41:27  jalet
178# Should fix the incorrect "backend died abnormally" problem.
179#
180# Revision 1.26  2004/01/30 16:35:03  jalet
181# Fixes stupid software accounting bug in CUPS backend
182#
183# Revision 1.25  2004/01/16 17:51:46  jalet
184# Fuck Fuck Fuck !!!
185#
186# Revision 1.24  2004/01/14 15:52:01  jalet
187# Small fix for job cancelling code.
188#
189# Revision 1.23  2004/01/13 10:48:28  jalet
190# Small streams polling loop modification.
191#
192# Revision 1.22  2004/01/12 22:43:40  jalet
193# New formula to compute a job's price
194#
195# Revision 1.21  2004/01/12 18:17:36  jalet
196# Denied jobs weren't stored into the history anymore, this is now fixed.
197#
198# Revision 1.20  2004/01/11 23:22:42  jalet
199# Major code refactoring, it's way cleaner, and now allows automated addition
200# of printers on first print.
201#
202# Revision 1.19  2004/01/08 14:10:32  jalet
203# Copyright year changed.
204#
205# Revision 1.18  2004/01/07 16:16:32  jalet
206# Better debugging information
207#
208# Revision 1.17  2003/12/27 16:49:25  uid67467
209# Should be ok now.
210#
211# Revision 1.17  2003/12/06 08:54:29  jalet
212# Code simplifications.
213# Added many debugging messages.
214#
215# Revision 1.16  2003/11/26 20:43:29  jalet
216# Inadvertantly introduced a bug, which is fixed.
217#
218# Revision 1.15  2003/11/26 19:17:35  jalet
219# Printing on a printer not present in the Quota Storage now results
220# in the job being stopped or cancelled depending on the system.
221#
222# Revision 1.14  2003/11/25 13:25:45  jalet
223# Boolean problem with old Python, replaced with 0
224#
225# Revision 1.13  2003/11/23 19:01:35  jalet
226# Job price added to history
227#
228# Revision 1.12  2003/11/21 14:28:43  jalet
229# More complete job history.
230#
231# Revision 1.11  2003/11/19 23:19:35  jalet
232# Code refactoring work.
233# Explicit redirection to /dev/null has to be set in external policy now, just
234# like in external mailto.
235#
236# Revision 1.10  2003/11/18 17:54:24  jalet
237# SIGTERMs are now transmitted to original backends.
238#
239# Revision 1.9  2003/11/18 14:11:07  jalet
240# Small fix for bizarre urls
241#
242# Revision 1.8  2003/11/15 14:26:44  jalet
243# General improvements to the documentation.
244# Email address changed in sample configuration file, because
245# I receive low quota messages almost every day...
246#
247# Revision 1.7  2003/11/14 22:05:12  jalet
248# New CUPS backend fully functionnal.
249# Old CUPS configuration method is now officially deprecated.
250#
251# Revision 1.6  2003/11/14 20:13:11  jalet
252# We exit the loop too soon.
253#
254# Revision 1.5  2003/11/14 18:31:27  jalet
255# Not perfect, but seems to work with the poll() loop.
256#
257# Revision 1.4  2003/11/14 17:04:15  jalet
258# More (untested) work on the CUPS backend.
259#
260# Revision 1.3  2003/11/12 23:27:44  jalet
261# More work on new backend. This commit may be unstable.
262#
263# Revision 1.2  2003/11/12 09:33:34  jalet
264# New CUPS backend supports device enumeration
265#
266# Revision 1.1  2003/11/08 16:05:31  jalet
267# CUPS backend added for people to experiment.
268#
269#
270#
271
272import sys
273import os
274import popen2
275import cStringIO
276import shlex
277import select
278import signal
279import time
280
281from pykota.tool import PyKotaFilterOrBackend, PyKotaToolError, crashed
282from pykota.config import PyKotaConfigError
283from pykota.storage import PyKotaStorageError
284from pykota.accounter import PyKotaAccounterError
285   
286class PyKotaPopen4(popen2.Popen4) :
287    """Our own class to execute real backends.
288   
289       Their first argument is different from their path so using
290       native popen2.Popen3 would not be feasible.
291    """
292    def __init__(self, cmd, bufsize=-1, arg0=None) :
293        self.arg0 = arg0
294        popen2.Popen4.__init__(self, cmd, bufsize)
295       
296    def _run_child(self, cmd):
297        try :
298            MAXFD = os.sysconf("SC_OPEN_MAX")
299        except (AttributeError, ValueError) :   
300            MAXFD = 256
301        for i in range(3, MAXFD) : 
302            try:
303                os.close(i)
304            except OSError:
305                pass
306        try:
307            os.execvpe(cmd[0], [self.arg0 or cmd[0]] + cmd[1:], os.environ)
308        finally:
309            os._exit(1)
310   
311class PyKotaBackend(PyKotaFilterOrBackend) :       
312    """A class for the pykota backend."""
313    def acceptJob(self) :       
314        """Returns the appropriate exit code to tell CUPS all is OK."""
315        return 0
316           
317    def removeJob(self) :           
318        """Returns the appropriate exit code to let CUPS think all is OK.
319       
320           Returning 0 (success) prevents CUPS from stopping the print queue.
321        """   
322        return 0
323       
324    def getPageLogLocation(self) :
325        """Retrieves CUPS' page_log file location."""
326        location = None
327        cupsroot = os.environ.get("CUPS_SERVERROOT", "/etc/cups")
328        cupsdconf = os.path.join(cupsroot, "cupsd.conf")
329        try :
330            conffile = open(cupsdconf, "r")
331        except IOError :   
332            self.logdebug("Unable to open %s" % cupsdconf)
333            return # file doesn't exist or can't be read
334        else :   
335            for line in conffile.readlines() :
336                linecopy = line.strip().lower()
337                if linecopy.startswith("pagelog ") :
338                    try :
339                        location = line.split()[1]
340                    except :   
341                        pass # ignore errors, we take the last value in any case.
342            conffile.close()           
343            return location           
344           
345    def getJobOriginatingHostname(self, printername, username, jobid) :
346        """Retrieves the job-originating-hostname from the CUPS page_log file if possible."""
347        pagelogpath = self.getPageLogLocation() or "/var/log/cups/page_log"
348        try :
349            pagelog = open(pagelogpath, "r")
350        except IOError :   
351            self.logdebug("Unable to open %s" % pagelogpath)
352            return # no page log or can't read it, originating hostname unknown yet
353        else :   
354            # TODO : read backward so we could take first value seen
355            # TODO : here we read forward so we must take the last value seen
356            prefix = ("%s %s %s" % (printername, username, jobid)).lower()
357            matchingline = None
358            while 1 :
359                line = pagelog.readline()
360                if not line :
361                    break
362                else :
363                    line = line.strip()
364                    if line.lower().startswith(prefix) :   
365                        matchingline = line # no break, because we read forward
366            pagelog.close()       
367            if matchingline is None :
368                self.logdebug("No matching line found in %s" % pagelogpath)
369                return # correct line not found, job-originating-hostname unknown
370            else :   
371                return matchingline.split()[-1]
372               
373    def doWork(self, policy, printer, user, userpquota) :   
374        """Most of the work is done here."""
375        # Two different values possible for policy here :
376        # ALLOW means : Either printer, user or user print quota doesn't exist,
377        #               but the job should be allowed anyway.
378        # OK means : Both printer, user and user print quota exist, job should
379        #            be allowed if current user is allowed to print on this printer
380        if policy == "OK" :
381            # exports user information with initial values
382            self.exportUserInfo(userpquota)
383           
384            # tries to extract job-originating-hostname
385            clienthost = self.getJobOriginatingHostname(printer.Name, user.Name, self.jobid)
386            self.logdebug("Client Hostname : %s" % (clienthost or "Unknown"))   
387            os.environ["PYKOTAJOBORIGINATINGHOSTNAME"] = str(clienthost or "")
388           
389            # enters first phase
390            os.environ["PYKOTAPHASE"] = "BEFORE"
391           
392            # do we want strict or laxist quota enforcement ?
393            if self.config.getPrinterEnforcement(printer.Name) == "STRICT" :
394                self.softwareJobSize = self.precomputeJobSize()
395                self.softwareJobPrice = userpquota.computeJobPrice(self.softwareJobSize)
396                self.logdebug("Precomputed job's size is %s pages, price is %s units" % (self.softwareJobSize, self.softwareJobPrice))
397            os.environ["PYKOTAPRECOMPUTEDJOBSIZE"] = str(self.softwareJobSize)
398            os.environ["PYKOTAPRECOMPUTEDJOBPRICE"] = str(self.softwareJobPrice)
399           
400            # if no data to pass to real backend, probably a filter
401            # higher in the chain failed because of a misconfiguration.
402            # we deny the job in this case (nothing to print anyway)
403            if not self.jobSizeBytes :
404                self.printInfo(_("Job contains no data. Printing is denied."), "warn")
405                action = "DENY"
406            else :   
407                # checks the user's quota
408                action = self.warnUserPQuota(userpquota)
409           
410            # exports some new environment variables
411            os.environ["PYKOTAACTION"] = action
412           
413            # launches the pre hook
414            self.prehook(userpquota)
415           
416            self.printInfo(_("Job accounting begins."))
417            self.accounter.beginJob(printer)
418        else :   
419            action = "ALLOW"
420            os.environ["PYKOTAACTION"] = action
421           
422        # pass the job's data to the real backend   
423        if action in ["ALLOW", "WARN"] :
424            if self.gotSigTerm :
425                retcode = self.removeJob()
426            else :   
427                retcode = self.handleData()       
428        else :       
429            retcode = self.removeJob()
430       
431        if policy == "OK" :       
432            # indicate phase change
433            os.environ["PYKOTAPHASE"] = "AFTER"
434           
435            # stops accounting.
436            self.accounter.endJob(printer)
437            self.printInfo(_("Job accounting ends."))
438               
439            # retrieve the job size   
440            if action == "DENY" :
441                jobsize = 0
442                self.printInfo(_("Job size forced to 0 because printing is denied."))
443            else :   
444                jobsize = self.accounter.getJobSize(printer)
445            self.printInfo(_("Job size : %i") % jobsize)
446           
447            # update the quota for the current user on this printer
448            self.printInfo(_("Updating user %s's quota on printer %s") % (user.Name, printer.Name))
449            jobprice = userpquota.increasePagesUsage(jobsize)
450           
451            # adds the current job to history   
452            printer.addJobToHistory(self.jobid, user, self.accounter.getLastPageCounter(), action, jobsize, jobprice, self.preserveinputfile, self.title, self.copies, self.options, clienthost, self.jobSizeBytes)
453            self.printInfo(_("Job added to history."))
454           
455            # exports some new environment variables
456            os.environ["PYKOTAJOBSIZE"] = str(jobsize)
457            os.environ["PYKOTAJOBPRICE"] = str(jobprice)
458           
459            # then re-export user information with new value
460            self.exportUserInfo(userpquota)
461           
462            # Launches the post hook
463            self.posthook(userpquota)
464           
465        return retcode   
466               
467    def unregisterFileNo(self, pollobj, fileno) :               
468        """Removes a file handle from the polling object."""
469        try :
470            pollobj.unregister(fileno)
471        except KeyError :   
472            self.printInfo(_("File number %s unregistered twice from polling object, ignored.") % fileno, "warn")
473        except :   
474            self.logdebug("Error while unregistering file number %s from polling object." % fileno)
475        else :   
476            self.logdebug("File number %s unregistered from polling object." % fileno)
477           
478    def formatFileEvent(self, fd, mask) :       
479        """Formats file debug info."""
480        maskval = []
481        if mask & select.POLLIN :
482            maskval.append("POLLIN")
483        if mask & select.POLLOUT :
484            maskval.append("POLLOUT")
485        if mask & select.POLLPRI :
486            maskval.append("POLLPRI")
487        if mask & select.POLLERR :
488            maskval.append("POLLERR")
489        if mask & select.POLLHUP :
490            maskval.append("POLLHUP")
491        if mask & select.POLLNVAL :
492            maskval.append("POLLNVAL")
493        return "%s (%s)" % (fd, " | ".join(maskval))
494       
495    def handleData(self) :                   
496        """Pass the job's data to the real backend."""
497        # Find the real backend pathname   
498        realbackend = os.path.join(os.path.split(sys.argv[0])[0], self.originalbackend)
499       
500        # And launch it
501        self.logdebug("Starting real backend %s with args %s" % (realbackend, " ".join(['"%s"' % a for a in ([os.environ["DEVICE_URI"]] + sys.argv[1:])])))
502        subprocess = PyKotaPopen4([realbackend] + sys.argv[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       
522        if self.preserveinputfile is None :
523            # this is not a real file, we read the job's data
524            # from our temporary file which is a copy of stdin
525            infno = self.jobdatastream.fileno()
526            self.jobdatastream.seek(0)
527            pollster.register(infno, select.POLLIN | select.POLLPRI)
528        else :   
529            # job's data is in a file, no need to pass the data
530            # to the real backend
531            self.logdebug("Job's data is in %s" % self.preserveinputfile)
532            infno = None
533            endinput = 1
534       
535        self.logdebug("Entering streams polling loop...")
536        MEGABYTE = 1024*1024
537        killed = 0
538        status = -1
539        while (status == -1) and (not killed) and not (inputclosed and outputclosed) :
540            # First check if original backend is still alive
541            status = subprocess.poll()
542           
543            # Now if we got SIGTERM, we have
544            # to kill -TERM the original backend
545            if self.gotSigTerm and not killed :
546                try :
547                    os.kill(subprocess.pid, signal.SIGTERM)
548                except OSError, msg : # ignore but logs if process was already killed.
549                    self.logdebug("Error while sending signal to pid %s : %s" % (subprocess.pid, msg))
550                else :   
551                    self.printInfo(_("SIGTERM was sent to real backend %s (pid: %s)") % (realbackend, subprocess.pid))
552                    killed = 1
553           
554            # In any case, deal with any remaining I/O
555            try :
556                availablefds = pollster.poll(5000)
557            except select.error, msg :   
558                self.logdebug("Interrupted poll : %s" % msg)
559                availablefds = []
560            if not availablefds :
561                self.logdebug("Nothing to do, sleeping a bit...")
562                time.sleep(0.01) # give some time to the system
563            else :
564                for (fd, mask) in availablefds :
565                    # self.logdebug(self.formatFileEvent(fd, mask))
566                    try :
567                        if mask & select.POLLOUT :
568                            # We can write
569                            if fd == tocfno :
570                                if indata :
571                                    try :
572                                        os.write(fd, indata)   
573                                    except (OSError, IOError), msg :   
574                                        self.logdebug("Error while writing to real backend's stdin %s : %s" % (fd, msg))
575                                    else :   
576                                        indata = ""
577                                else :       
578                                    self.logdebug("No data to send to real backend yet, sleeping a bit...")
579                                    time.sleep(0.01)
580                                   
581                                if endinput :   
582                                    self.unregisterFileNo(pollster, tocfno)       
583                                    self.logdebug("Closing real backend's stdin.")
584                                    os.close(tocfno)
585                                    inputclosed = 1
586                            elif fd == stderrfno :
587                                if outdata :
588                                    try :
589                                        os.write(fd, outdata)
590                                    except (OSError, IOError), msg :   
591                                        self.logdebug("Error while writing to CUPS back channel (stderr) %s : %s" % (fd, msg))
592                                    else :
593                                        outdata = ""
594                                else :       
595                                    # self.logdebug("No data to send back to CUPS yet, sleeping a bit...") # Uncommenting this fills your logs
596                                    time.sleep(0.01) # Give some time to the system, stderr is ALWAYS writeable it seems.
597                                   
598                                if endoutput :   
599                                    self.unregisterFileNo(pollster, stderrfno)       
600                                    outputclosed = 1
601                            else :   
602                                self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
603                                time.sleep(0.01)
604                               
605                        if mask & (select.POLLIN | select.POLLPRI) :     
606                            # We have something to read
607                            try :
608                                data = os.read(fd, MEGABYTE)
609                            except (IOError, OSError), msg :   
610                                self.logdebug("Error while reading file %s : %s" % (fd, msg))
611                            else :
612                                if fd == infno :
613                                    if not data :    # If yes, then no more input data
614                                        self.unregisterFileNo(pollster, infno)
615                                        self.logdebug("Input data ends.")
616                                        endinput = 1 # this happens with real files.
617                                    else :   
618                                        indata += data
619                                elif fd == fromcfno :
620                                    if not data :
621                                        self.logdebug("No back channel data to read from real backend yet, sleeping a bit...")
622                                        time.sleep(0.01)
623                                    else :
624                                        outdata += data
625                                else :   
626                                    self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
627                                    time.sleep(0.01)
628                                   
629                        if mask & (select.POLLHUP | select.POLLERR) :
630                            # Treat POLLERR as an EOF.
631                            # Some standard I/O stream has no more datas
632                            self.unregisterFileNo(pollster, fd)
633                            if fd == infno :
634                                # Here we are in the case where the input file is stdin.
635                                # which has no more data to be read.
636                                self.logdebug("Input data ends.")
637                                endinput = 1
638                            elif fd == fromcfno :   
639                                # We are no more interested in this file descriptor       
640                                self.logdebug("Closing real backend's stdout+stderr.")
641                                os.close(fromcfno)
642                                endoutput = 1
643                            else :   
644                                self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
645                                time.sleep(0.01)
646                               
647                        if mask & select.POLLNVAL :       
648                            self.logdebug("File %s was closed. Unregistering from polling object." % fd)
649                            self.unregisterFileNo(pollster, fd)
650                    except IOError, msg :           
651                        self.logdebug("Got an IOError : %s" % msg) # we got signalled during an I/O
652               
653        # We must close the real backend's input stream
654        if killed and not inputclosed :
655            self.logdebug("Forcing close of real backend's stdin.")
656            os.close(tocfno)
657       
658        self.logdebug("Exiting streams polling loop...")
659       
660        # Check exit code of original CUPS backend.   
661        if status == -1 :
662            # we exited the loop before the real backend exited
663            # now we have to wait for it to finish and get its status
664            self.logdebug("Waiting for real backend to exit...")
665            try :
666                status = subprocess.wait()
667            except OSError : # already dead   
668                status = 0
669        if os.WIFEXITED(status) :
670            retcode = os.WEXITSTATUS(status)
671        elif not killed :   
672            self.sendBackChannelData(_("CUPS backend %s died abnormally.") % realbackend, "error")
673            retcode = -1
674        else :   
675            retcode = self.removeJob()
676        return retcode   
677   
678if __name__ == "__main__" :   
679    # This is a CUPS backend, we should act and die like a CUPS backend
680    retcode = 0
681    if len(sys.argv) == 1 :
682        # we will execute each existing backend in device enumeration mode
683        # and generate their PyKota accounting counterpart
684        (directory, myname) = os.path.split(sys.argv[0])
685        for backend in [os.path.join(directory, b) for b in os.listdir(directory) if os.path.isfile(os.path.join(directory, b)) and (b != myname)] :
686            answer = os.popen(backend, "r")
687            try :
688                devices = [line.strip() for line in answer.readlines()]
689            except :   
690                devices = []
691            status = answer.close()
692            if status is None :
693                for d in devices :
694                    # each line is of the form : 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
695                    # so we have to decompose it carefully
696                    fdevice = cStringIO.StringIO("%s" % d)
697                    tokenizer = shlex.shlex(fdevice)
698                    tokenizer.wordchars = tokenizer.wordchars + r".:,?!~/\_$*-+={}[]()#"
699                    arguments = []
700                    while 1 :
701                        token = tokenizer.get_token()
702                        if token :
703                            arguments.append(token)
704                        else :
705                            break
706                    fdevice.close()
707                    try :
708                        (devicetype, device, name, fullname) = arguments
709                    except ValueError :   
710                        pass    # ignore this 'bizarre' device
711                    else :   
712                        if name.startswith('"') and name.endswith('"') :
713                            name = name[1:-1]
714                        if fullname.startswith('"') and fullname.endswith('"') :
715                            fullname = fullname[1:-1]
716                        print '%s cupspykota:%s "PyKota+%s" "PyKota managed %s"' % (devicetype, device, name, fullname)
717        retcode = 0               
718    elif len(sys.argv) not in (6, 7) :   
719        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n" % sys.argv[0])
720        retcode = 1
721    else :   
722        try :
723            try :
724                # Initializes the backend
725                kotabackend = PyKotaBackend()   
726            except SystemExit :   
727                retcode = -1
728            except :   
729                crashed("cupspykota backend initialization failed")
730                retcode = 1
731            else :   
732                retcode = kotabackend.mainWork()
733                kotabackend.storage.close()
734                kotabackend.closeJobDataStream()   
735        except :
736            try :
737                kotabackend.crashed("cupspykota backend failed")
738            except :   
739                crashed("cupspykota backend failed")
740            retcode = 1   
741       
742    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.