root / pykota / trunk / bin / cupspykota @ 1855

Revision 1820, 31.2 kB (checked in by jalet, 20 years ago)

Made debugging levels be the same in cupspykota and lprngpykota.
Now outputs more information in informational messages : user, printer, jobid

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