root / pykota / trunk / bin / cupspykota @ 1546

Revision 1546, 29.6 kB (checked in by jalet, 20 years ago)

Now all tracebacks include PyKota's version number

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