root / pykota / trunk / bin / cupspykota @ 1562

Revision 1562, 30.2 kB (checked in by jalet, 20 years ago)

Always send some debug info to CUPS' back channel stream (stderr) as
informationnal messages.

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