root / pykota / trunk / bin / cupspykota @ 1529

Revision 1520, 29.0 kB (checked in by jalet, 20 years ago)

Now stores the job's size in bytes in the database.
Preliminary work on payments storage : database schemas are OK now,
but no code to store payments yet.
Removed schema picture, not relevant anymore.

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