root / pykota / trunk / bin / cupspykota @ 1632

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

Hardware accounting for LPRng should be OK now. UNTESTED.

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