root / pykota / trunk / bin / cupspykota @ 2042

Revision 2028, 38.5 kB (checked in by jalet, 20 years ago)

Modified copyright years

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