root / pykota / trunk / bin / cupspykota @ 1539

Revision 1530, 29.2 kB (checked in by jalet, 20 years ago)

Fixed problem when username was passed in uppercase from Samba and we
tried to find correct line in CUPS page_log to extract the hostname.

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