root / pykota / trunk / bin / cupspykota @ 1606

Revision 1606, 30.0 kB (checked in by jalet, 20 years ago)

Sanitized a bit + use of gettext

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