root / pykota / trunk / bin / cupspykota @ 2006

Revision 2006, 38.2 kB (checked in by jalet, 19 years ago)

Implemented the dropping of priviledges. Beware, beware...

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