root / pykota / trunk / bin / cupspykota @ 1600

Revision 1600, 30.1 kB (checked in by jalet, 20 years ago)

LPRng support early version

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