root / pykota / trunk / bin / cupspykota @ 1704

Revision 1704, 30.3 kB (checked in by jalet, 20 years ago)

Fix for autodetection of SC_OPEN_MAX

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