root / pykota / trunk / bin / cupspykota @ 1584

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

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