root / pykota / trunk / bin / cupspykota @ 1643

Revision 1643, 30.2 kB (checked in by jalet, 20 years ago)

Unneeded module

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