root / pykota / trunk / bin / cupspykota @ 1819

Revision 1819, 30.9 kB (checked in by jalet, 20 years ago)

Added a space to the pattern to differentiate jobs which id begins with
the same digits, like jobs 87 and 879 for example : if printed by same
user on same printer, but from a different host this could have broken
the result. (In reality this couldn't happen because 879 would be the
last line to match anyway because of job ordering, but we never know
if the page_log file gets corrupt somewhat)

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