root / pykota / trunk / bin / cupspykota @ 1542

Revision 1542, 29.8 kB (checked in by jalet, 20 years ago)

Better exception handling code

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