root / pykota / trunk / bin / cupspykota @ 1541

Revision 1541, 29.4 kB (checked in by jalet, 20 years ago)

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