root / pykota / trunk / bin / cupspykota @ 1519

Revision 1519, 28.7 kB (checked in by jalet, 20 years ago)

Now denies empty jobs

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