root / tea4cups / trunk / tea4cups @ 633

Revision 633, 41.6 kB (checked in by jerome, 19 years ago)

Docstrings

  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Rev
Line 
1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
4# Tea4CUPS : Tee for CUPS
5#
6# (c) 2005 Jerome Alet <alet@librelogiciel.com>
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
20#
21# $Id$
22#
23#
24
25import sys
26import os
27import pwd
28import popen2
29import errno
30import md5
31import cStringIO
32import shlex
33import tempfile
34import ConfigParser
35import select
36import signal
37import time
38from struct import unpack
39
40version = "2.12alpha_unofficial"
41
42class TeeError(Exception):
43    """Base exception for Tea4CUPS related stuff."""
44    def __init__(self, message = ""):
45        self.message = message
46        Exception.__init__(self, message)
47    def __repr__(self):
48        return self.message
49    __str__ = __repr__
50   
51class ConfigError(TeeError) :   
52    """Configuration related exceptions."""
53    pass 
54   
55class IPPError(TeeError) :   
56    """IPP related exceptions."""
57    pass 
58   
59class Popen4ForCUPS(popen2.Popen4) :
60    """Our own class to execute real backends.
61   
62       Their first argument is different from their path so using
63       native popen2.Popen3 would not be feasible.
64    """
65    def __init__(self, cmd, bufsize=-1, arg0=None) :
66        self.arg0 = arg0
67        popen2.Popen4.__init__(self, cmd, bufsize)
68       
69    def _run_child(self, cmd):
70        try :
71            MAXFD = os.sysconf("SC_OPEN_MAX")
72        except (AttributeError, ValueError) :   
73            MAXFD = 256
74        for i in range(3, MAXFD) : 
75            try:
76                os.close(i)
77            except OSError:
78                pass
79        try:
80            os.execvpe(cmd[0], [self.arg0 or cmd[0]] + cmd[1:], os.environ)
81        finally:
82            os._exit(1)
83   
84# Some IPP constants   
85OPERATION_ATTRIBUTES_TAG = 0x01
86JOB_ATTRIBUTES_TAG = 0x02
87END_OF_ATTRIBUTES_TAG = 0x03
88PRINTER_ATTRIBUTES_TAG = 0x04
89UNSUPPORTED_ATTRIBUTES_TAG = 0x05
90
91class IPPMessage :
92    """A class for IPP message files."""
93    def __init__(self, data, debug=0) :
94        """Initializes an IPP Message object."""
95        self.debug = debug
96        self.data = data
97        self.operation_attributes = {}
98        self.job_attributes = {}
99        self.printer_attributes = {}
100        self.tags = [ None ] * 256      # by default all tags reserved
101       
102        # Delimiter tags
103        self.tags[0x01] = "operation-attributes-tag"
104        self.tags[0x02] = "job-attributes-tag"
105        self.tags[0x03] = "end-of-attributes-tag"
106        self.tags[0x04] = "printer-attributes-tag"
107        self.tags[0x05] = "unsupported-attributes-tag"
108       
109        # out of band values
110        self.tags[0x10] = "unsupported"
111        self.tags[0x11] = "reserved-for-future-default"
112        self.tags[0x12] = "unknown"
113        self.tags[0x13] = "no-value"
114       
115        # integer values
116        self.tags[0x20] = "generic-integer"
117        self.tags[0x21] = "integer"
118        self.tags[0x22] = "boolean"
119        self.tags[0x23] = "enum"
120       
121        # octetString
122        self.tags[0x30] = "octetString-with-an-unspecified-format"
123        self.tags[0x31] = "dateTime"
124        self.tags[0x32] = "resolution"
125        self.tags[0x33] = "rangeOfInteger"
126        self.tags[0x34] = "reserved-for-collection"
127        self.tags[0x35] = "textWithLanguage"
128        self.tags[0x36] = "nameWithLanguage"
129       
130        # character strings
131        self.tags[0x20] = "generic-character-string"
132        self.tags[0x41] = "textWithoutLanguage"
133        self.tags[0x42] = "nameWithoutLanguage"
134        # self.tags[0x43] = "reserved"
135        self.tags[0x44] = "keyword"
136        self.tags[0x45] = "uri"
137        self.tags[0x46] = "uriScheme"
138        self.tags[0x47] = "charset"
139        self.tags[0x48] = "naturalLanguage"
140        self.tags[0x49] = "mimeMediaType"
141       
142        # now parses the IPP message
143        self.parse()
144       
145    def printInfo(self, msg) :   
146        """Prints a debug message."""
147        if self.debug :
148            sys.stderr.write("%s\n" % msg)
149            sys.stderr.flush()
150           
151    def parseTag(self) :   
152        """Extracts information from an IPP tag."""
153        pos = self.position
154        tagtype = self.tags[ord(self.data[pos])]
155        pos += 1
156        posend = pos2 = pos + 2
157        namelength = unpack(">H", self.data[pos:pos2])[0]
158        if not namelength :
159            name = self._curname
160        else :   
161            posend += namelength
162            self._curname = name = self.data[pos2:posend]
163        pos2 = posend + 2
164        valuelength = unpack(">H", self.data[posend:pos2])[0]
165        posend = pos2 + valuelength
166        value = self.data[pos2:posend]
167        if tagtype in ("integer", "enum") :
168            value = unpack(">I", value)
169        oldval = self._curdict.setdefault(name, [])
170        oldval.append((tagtype, value))
171        self.printInfo("%s(%s) %s" % (name, tagtype, value))
172        return posend - self.position
173       
174    def operation_attributes_tag(self) : 
175        """Indicates that the parser enters into an operation-attributes-tag group."""
176        self.printInfo("Start of operation_attributes_tag")
177        self._curdict = self.operation_attributes
178        return self.parseTag()
179       
180    def job_attributes_tag(self) : 
181        """Indicates that the parser enters into an operation-attributes-tag group."""
182        self.printInfo("Start of job_attributes_tag")
183        self._curdict = self.job_attributes
184        return self.parseTag()
185       
186    def printer_attributes_tag(self) : 
187        """Indicates that the parser enters into an operation-attributes-tag group."""
188        self.printInfo("Start of printer_attributes_tag")
189        self._curdict = self.printer_attributes
190        return self.parseTag()
191           
192    def parse(self) :
193        """Parses an IPP Message.
194       
195           Implemented with RFC2910 as a reference.
196           NB : only a subset is actually implemented, but CUPS generated
197           message files are completely decoded.
198        """
199        self._curname = None
200        self._curdict = None
201        self.version = "%s.%s" % (ord(self.data[0]), ord(self.data[1]))
202        self.operation_id = "0x%04x" % unpack(">H", self.data[2:4])[0]
203        self.request_id = "0x%08x" % unpack(">I", self.data[4:8])[0]
204        self.position = 8
205        try :
206            tag = ord(self.data[self.position])
207            while tag != END_OF_ATTRIBUTES_TAG :
208                self.position += 1
209                name = self.tags[tag]
210                if name is not None :
211                    func = getattr(self, name.replace("-", "_"), None)
212                    if func is not None :
213                        self.position += func()
214                        if ord(self.data[self.position]) > UNSUPPORTED_ATTRIBUTES_TAG :
215                            self.position -= 1
216                            continue
217                tag = ord(self.data[self.position])
218        except IndexError :
219            raise IPPError, "Unexpected end of IPP message."
220           
221        # Now transform all one-element lists into single values
222        for attrtype in ("operation", "job", "printer") :
223            attrdict = getattr(self, "%s_attributes" % attrtype)
224            for (key, value) in attrdict.items() :
225                if len(value) == 1 :
226                    attrdict[key] = value[0]
227           
228class FakeConfig :   
229    """Fakes a configuration file parser."""
230    def get(self, section, option, raw=0) :
231        """Fakes the retrieval of an option."""
232        raise ConfigError, "Invalid configuration file : no option %s in section [%s]" % (option, section)
233       
234class CupsBackend :
235    """Base class for tools with no database access."""
236    def __init__(self) :
237        """Initializes the CUPS backend wrapper."""
238        signal.signal(signal.SIGTERM, signal.SIG_IGN)
239        signal.signal(signal.SIGPIPE, signal.SIG_IGN)
240        self.MyName = "Tea4CUPS"
241        self.myname = "tea4cups"
242        self.pid = os.getpid()
243       
244    def readConfig(self) :   
245        """Reads the configuration file."""
246        confdir = os.environ.get("CUPS_SERVERROOT", ".") 
247        self.conffile = os.path.join(confdir, "%s.conf" % self.myname)
248        if os.path.isfile(self.conffile) :
249            self.config = ConfigParser.ConfigParser()
250            self.config.read([self.conffile])
251            self.debug = self.isTrue(self.getGlobalOption("debug", ignore=1))
252        else :   
253            self.config = FakeConfig()
254            self.debug = 1      # no config, so force debug mode !
255           
256    def logInfo(self, message, level="info") :       
257        """Logs a message to CUPS' error_log file."""
258        sys.stderr.write("%s: %s v%s (PID %i) : %s\n" % (level.upper(), self.MyName, version, os.getpid(), message))
259        sys.stderr.flush()
260       
261    def logDebug(self, message) :   
262        """Logs something to debug output if debug is enabled."""
263        if self.debug :
264            self.logInfo(message, level="debug")
265       
266    def isTrue(self, option) :       
267        """Returns 1 if option is set to true, else 0."""
268        if (option is not None) and (option.upper().strip() in ['Y', 'YES', '1', 'ON', 'T', 'TRUE']) :
269            return 1
270        else :   
271            return 0
272                       
273    def getGlobalOption(self, option, ignore=0) :   
274        """Returns an option from the global section, or raises a ConfigError if ignore is not set, else returns None."""
275        try :
276            return self.config.get("global", option, raw=1)
277        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :   
278            if not ignore :
279                raise ConfigError, "Option %s not found in section global of %s" % (option, self.conffile)
280               
281    def getPrintQueueOption(self, printqueuename, option, ignore=0) :   
282        """Returns an option from the printer section, or the global section, or raises a ConfigError."""
283        globaloption = self.getGlobalOption(option, ignore=1)
284        try :
285            return self.config.get(printqueuename, option, raw=1)
286        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :   
287            if globaloption is not None :
288                return globaloption
289            elif not ignore :
290                raise ConfigError, "Option %s not found in section [%s] of %s" % (option, printqueuename, self.conffile)
291               
292    def enumBranches(self, printqueuename, branchtype="tee") :
293        """Returns the list of branchtypes branches for a particular section's."""
294        branchbasename = "%s_" % branchtype.lower()
295        try :
296            # globalbranches = [ (k, v) for (k, v) in self.config.items("global") if k.startswith(branchbasename) ]
297            globalbranches = [ (k, self.config.get("global", k)) for k in self.config.options("global") if k.startswith(branchbasename) ]
298        except ConfigParser.NoSectionError, msg :   
299            raise ConfigError, "Invalid configuration file : %s" % msg
300        try :
301            # sectionbranches = [ (k, v) for (k, v) in self.config.items(printqueuename) if k.startswith(branchbasename) ]
302            sectionbranches = [ (k, self.config.get(printqueuename, k)) for k in self.config.options(printqueuename) if k.startswith(branchbasename) ]
303        except ConfigParser.NoSectionError, msg :   
304            self.logInfo("No section for print queue %s : %s" % (printqueuename, msg))
305            sectionbranches = []
306        branches = {}
307        for (k, v) in globalbranches :
308            value = v.strip()
309            if value :
310                branches[k] = value
311        for (k, v) in sectionbranches :   
312            value = v.strip()
313            if value :
314                branches[k] = value # overwrite any global option or set a new value
315            else :   
316                del branches[k] # empty value disables a global option
317        return branches
318       
319    def discoverOtherBackends(self) :   
320        """Discovers the other CUPS backends.
321       
322           Executes each existing backend in turn in device enumeration mode.
323           Returns the list of available backends.
324        """
325        # Unfortunately this method can't output any debug information
326        # to stdout or stderr, else CUPS considers that the device is
327        # not available.
328        available = []
329        (directory, myname) = os.path.split(sys.argv[0])
330        if not directory :
331            directory = "./"
332        tmpdir = tempfile.gettempdir()
333        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
334        if os.path.exists(lockfilename) :
335            lockfile = open(lockfilename, "r")
336            pid = int(lockfile.read())
337            lockfile.close()
338            try :
339                # see if the pid contained in the lock file is still running
340                os.kill(pid, 0)
341            except OSError, e :   
342                if e.errno != errno.EPERM :
343                    # process doesn't exist anymore
344                    os.remove(lockfilename)
345           
346        if not os.path.exists(lockfilename) :
347            lockfile = open(lockfilename, "w")
348            lockfile.write("%i" % self.pid)
349            lockfile.close()
350            allbackends = [ os.path.join(directory, b) \
351                                for b in os.listdir(directory) 
352                                    if os.access(os.path.join(directory, b), os.X_OK) \
353                                        and (b != myname)] 
354            for backend in allbackends :                           
355                answer = os.popen(backend, "r")
356                try :
357                    devices = [line.strip() for line in answer.readlines()]
358                except :   
359                    devices = []
360                status = answer.close()
361                if status is None :
362                    for d in devices :
363                        # each line is of the form :
364                        # 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
365                        # so we have to decompose it carefully
366                        fdevice = cStringIO.StringIO(d)
367                        tokenizer = shlex.shlex(fdevice)
368                        tokenizer.wordchars = tokenizer.wordchars + \
369                                                        r".:,?!~/\_$*-+={}[]()#"
370                        arguments = []
371                        while 1 :
372                            token = tokenizer.get_token()
373                            if token :
374                                arguments.append(token)
375                            else :
376                                break
377                        fdevice.close()
378                        try :
379                            (devicetype, device, name, fullname) = arguments
380                        except ValueError :   
381                            pass    # ignore this 'bizarre' device
382                        else :   
383                            if name.startswith('"') and name.endswith('"') :
384                                name = name[1:-1]
385                            if fullname.startswith('"') and fullname.endswith('"') :
386                                fullname = fullname[1:-1]
387                            available.append('%s %s:%s "%s+%s" "%s managed %s"' \
388                                                 % (devicetype, self.myname, device, self.MyName, name, self.MyName, fullname))
389            os.remove(lockfilename)
390        available.append('direct %s:// "%s+Nothing" "%s managed Virtual Printer"' \
391                             % (self.myname, self.MyName, self.MyName))
392        return available
393                       
394    def initBackend(self) :   
395        """Initializes the backend's attributes."""
396        # check that the DEVICE_URI environment variable's value is
397        # prefixed with self.myname otherwise don't touch it.
398        # If this is the case, we have to remove the prefix from
399        # the environment before launching the real backend
400        muststartwith = "%s:" % self.myname
401        device_uri = os.environ.get("DEVICE_URI", "")
402        if device_uri.startswith(muststartwith) :
403            fulldevice_uri = device_uri[:]
404            device_uri = fulldevice_uri[len(muststartwith):]
405            for i in range(2) :
406                if device_uri.startswith("/") : 
407                    device_uri = device_uri[1:]
408        try :
409            (backend, destination) = device_uri.split(":", 1) 
410        except ValueError :   
411            if not device_uri :
412                self.logDebug("Not attached to an existing print queue.")
413                backend = ""
414            else :   
415                raise TeeError, "Invalid DEVICE_URI : %s\n" % device_uri
416       
417        self.JobId = sys.argv[1].strip()
418        self.UserName = sys.argv[2].strip() or pwd.getpwuid(os.geteuid())[0] # use CUPS' user when printing test pages from CUPS' web interface
419        self.Title = sys.argv[3].strip()
420        self.Copies = int(sys.argv[4].strip())
421        self.Options = sys.argv[5].strip()
422        if len(sys.argv) == 7 :
423            self.InputFile = sys.argv[6] # read job's datas from file
424        else :   
425            self.InputFile = None        # read job's datas from stdin
426           
427        self.RealBackend = backend
428        self.DeviceURI = device_uri
429        self.PrinterName = os.environ.get("PRINTER", "")
430        self.Directory = self.getPrintQueueOption(self.PrinterName, "directory")
431        self.DataFile = os.path.join(self.Directory, "%s-%s-%s-%s" % (self.myname, self.PrinterName, self.UserName, self.JobId))
432        (ippfilename, ippmessage) = self.parseIPPMessageFile()
433        self.ControlFile = ippfilename
434        (chtype, self.ClientHost) = ippmessage.operation_attributes.get("job-originating-host-name", \
435                                          ippmessage.job_attributes.get("job-originating-host-name", (None, None)))
436        (jbtype, self.JobBilling) = ippmessage.job_attributes.get("job-billing", (None, None))
437           
438    def getCupsConfigDirectives(self, directives=[]) :
439        """Retrieves some CUPS directives from its configuration file.
440       
441           Returns a mapping with lowercased directives as keys and
442           their setting as values.
443        """
444        dirvalues = {} 
445        cupsroot = os.environ.get("CUPS_SERVERROOT", "/etc/cups")
446        cupsdconf = os.path.join(cupsroot, "cupsd.conf")
447        try :
448            conffile = open(cupsdconf, "r")
449        except IOError :   
450            raise TeeError, "Unable to open %s" % cupsdconf
451        else :   
452            for line in conffile.readlines() :
453                linecopy = line.strip().lower()
454                for di in [d.lower() for d in directives] :
455                    if linecopy.startswith("%s " % di) :
456                        try :
457                            val = line.split()[1]
458                        except :   
459                            pass # ignore errors, we take the last value in any case.
460                        else :   
461                            dirvalues[di] = val
462            conffile.close()           
463        return dirvalues       
464           
465    def parseIPPMessageFile(self) :       
466        """Parses the IPP message file and returns a tuple (filename, parsedvalue)."""
467        cupsdconf = self.getCupsConfigDirectives(["RequestRoot"])
468        requestroot = cupsdconf.get("requestroot", "/var/spool/cups")
469        if (len(self.JobId) < 5) and self.JobId.isdigit() :
470            ippmessagefile = "c%05i" % int(self.JobId)
471        else :   
472            ippmessagefile = "c%s" % self.JobId
473        ippmessagefile = os.path.join(requestroot, ippmessagefile)
474        ippmessage = {}
475        try :
476            ippdatafile = open(ippmessagefile)
477        except :   
478            self.logInfo("Unable to open IPP message file %s" % ippmessagefile, "warn")
479        else :   
480            self.logDebug("Parsing of IPP message file %s begins." % ippmessagefile)
481            try :
482                ippmessage = IPPMessage(ippdatafile.read())
483            except IPPError, msg :   
484                self.logInfo("Error while parsing %s : %s" % (ippmessagefile, msg), "warn")
485            else :   
486                self.logDebug("Parsing of IPP message file %s ends." % ippmessagefile)
487            ippdatafile.close()
488        return (ippmessagefile, ippmessage)
489               
490    def exportAttributes(self) :   
491        """Exports our backend's attributes to the environment."""
492        os.environ["DEVICE_URI"] = self.DeviceURI       # WARNING !
493        os.environ["TEAPRINTERNAME"] = self.PrinterName
494        os.environ["TEADIRECTORY"] = self.Directory
495        os.environ["TEADATAFILE"] = self.DataFile
496        os.environ["TEAJOBSIZE"] = str(self.JobSize)
497        os.environ["TEAMD5SUM"] = self.JobMD5Sum
498        os.environ["TEACLIENTHOST"] = self.ClientHost or ""
499        os.environ["TEAJOBID"] = self.JobId
500        os.environ["TEAUSERNAME"] = self.UserName
501        os.environ["TEATITLE"] = self.Title
502        os.environ["TEACOPIES"] = str(self.Copies)
503        os.environ["TEAOPTIONS"] = self.Options
504        os.environ["TEAINPUTFILE"] = self.InputFile or ""
505        os.environ["TEABILLING"] = self.JobBilling or ""
506        os.environ["TEACONTROLFILE"] = self.ControlFile
507       
508    def saveDatasAndCheckSum(self) :
509        """Saves the input datas into a static file."""
510        self.logDebug("Duplicating data stream into %s" % self.DataFile)
511        mustclose = 0
512        if self.InputFile is not None :
513            infile = open(self.InputFile, "rb")
514            mustclose = 1
515        else :   
516            infile = sys.stdin
517        CHUNK = 64*1024         # read 64 Kb at a time
518        dummy = 0
519        sizeread = 0
520        checksum = md5.new()
521        outfile = open(self.DataFile, "wb")   
522        while 1 :
523            data = infile.read(CHUNK) 
524            if not data :
525                break
526            sizeread += len(data)   
527            outfile.write(data)
528            checksum.update(data)   
529            if not (dummy % 32) : # Only display every 2 Mb
530                self.logDebug("%s bytes saved..." % sizeread)
531            dummy += 1   
532        outfile.close()
533        if mustclose :   
534            infile.close()
535        self.JobSize = sizeread   
536        self.JobMD5Sum = checksum.hexdigest()
537        self.logDebug("Job %s is %s bytes long." % (self.JobId, self.JobSize))
538        self.logDebug("Job %s MD5 sum is %s" % (self.JobId, self.JobMD5Sum))
539
540    def cleanUp(self) :
541        """Cleans up the place."""
542        if not self.isTrue(self.getPrintQueueOption(self.PrinterName, "keepfiles", ignore=1)) :
543            os.remove(self.DataFile)
544           
545    def sigtermHandler(self, signum, frame) :
546        """Sets an attribute whenever SIGTERM is received."""
547        self.gotSigTerm = 1
548        self.logInfo("SIGTERM received for Job %s." % self.JobId)
549       
550    def runBranches(self) :         
551        """Launches each hook or tee defined for the current print queue."""
552        exitcode = 0
553        self.isCancelled = 0    # did a prehook cancel the print job ?
554        self.gotSigTerm = 0
555        signal.signal(signal.SIGTERM, self.sigtermHandler)
556        serialize = self.isTrue(self.getPrintQueueOption(self.PrinterName, "serialize", ignore=1))
557        for branchtype in ["prehook", "tee", "posthook"] :
558            branches = self.enumBranches(self.PrinterName, branchtype)
559            status = self.runCommands(branchtype, branches, serialize)
560            if status :
561                if branchtype != "posthook" :
562                    exitcode = status
563                else :   
564                    # we just ignore error in posthooks
565                    self.logInfo("An error occured during the execution of posthooks.", "warn")
566            if (branchtype == "prehook") and self.isCancelled :
567                break # We don't want to execute tees or posthooks in this case
568        signal.signal(signal.SIGTERM, signal.SIG_IGN)
569        if not exitcode :
570            self.logInfo("OK")
571        else :   
572            self.logInfo("An error occured, please check CUPS' error_log file.")
573        return exitcode
574       
575    def runCommands(self, btype, branches, serialize) :   
576        """Runs the commands for a particular branch type."""
577        exitcode = 0 
578        btype = btype.lower()
579        btypetitle = btype.title()
580        branchlist = branches.keys()   
581        branchlist.sort()
582        if serialize :
583            self.logDebug("Begin serialized %ss" % btypetitle)
584            if (btype == "tee") and self.RealBackend :
585                self.logDebug("Launching original backend %s for printer %s" % (self.RealBackend, self.PrinterName))
586                retcode = self.runOriginalBackend()
587                if os.WIFEXITED(retcode) :
588                    retcode = os.WEXITSTATUS(retcode)
589                os.environ["TEASTATUS"] = str(retcode)
590                exitcode = retcode
591            for branch in branchlist :
592                command = branches[branch]
593                if self.gotSigTerm :
594                    break
595                self.logDebug("Launching %s : %s" % (branch, command))
596                retcode = os.system(command)
597                self.logDebug("Exit code for %s %s on printer %s is %s" % (btype, branch, self.PrinterName, retcode))
598                if os.WIFEXITED(retcode) :
599                    retcode = os.WEXITSTATUS(retcode)
600                if retcode :   
601                    if (btype == "prehook") and (retcode == 255) : # -1
602                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
603                        self.isCancelled = 1
604                    else :   
605                        self.logInfo("%s %s on printer %s didn't exit successfully." % (btypetitle, branch, self.PrinterName), "error")
606                        exitcode = 1
607            self.logDebug("End serialized %ss" % btypetitle)
608        else :       
609            self.logDebug("Begin forked %ss" % btypetitle)
610            pids = {}
611            if (btype == "tee") and self.RealBackend :
612                branches["Original backend"] = None     # Fakes a tee to launch one more child
613                branchlist = ["Original backend"] + branchlist
614            for branch in branchlist :
615                command = branches[branch]
616                if self.gotSigTerm :
617                    break
618                pid = os.fork()
619                if pid :
620                    pids[branch] = pid
621                else :   
622                    if branch == "Original backend" :
623                        self.logDebug("Launching original backend %s for printer %s" % (self.RealBackend, self.PrinterName))
624                        sys.exit(self.runOriginalBackend())
625                    else :
626                        self.logDebug("Launching %s : %s" % (branch, command))
627                        retcode = os.system(command)
628                        if os.WIFEXITED(retcode) :
629                            retcode = os.WEXITSTATUS(retcode)
630                        else :   
631                            retcode = -1
632                        sys.exit(retcode)
633            for (branch, pid) in pids.items() :
634                (childpid, retcode) = os.waitpid(pid, 0)
635                self.logDebug("Exit code for %s %s (PID %s) on printer %s is %s" % (btype, branch, childpid, self.PrinterName, retcode))
636                if os.WIFEXITED(retcode) :
637                    retcode = os.WEXITSTATUS(retcode)
638                if retcode :   
639                    if (btype == "prehook") and (retcode == 255) : # -1
640                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
641                        self.isCancelled = 1
642                    else :   
643                        self.logInfo("%s %s (PID %s) on printer %s didn't exit successfully." % (btypetitle, branch, childpid, self.PrinterName), "error")
644                        exitcode = 1
645                if branch == "Original backend" :   
646                    os.environ["TEASTATUS"] = str(retcode)
647            self.logDebug("End forked %ss" % btypetitle)
648        return exitcode
649       
650    def unregisterFileNo(self, pollobj, fileno) :               
651        """Removes a file handle from the polling object."""
652        try :
653            pollobj.unregister(fileno)
654        except KeyError :   
655            self.logInfo("File number %s unregistered twice from polling object, ignored." % fileno, "warn")
656        except :   
657            self.logDebug("Error while unregistering file number %s from polling object." % fileno)
658        else :   
659            self.logDebug("File number %s unregistered from polling object." % fileno)
660           
661    def formatFileEvent(self, fd, mask) :       
662        """Formats file debug info."""
663        maskval = []
664        if mask & select.POLLIN :
665            maskval.append("POLLIN")
666        if mask & select.POLLOUT :
667            maskval.append("POLLOUT")
668        if mask & select.POLLPRI :
669            maskval.append("POLLPRI")
670        if mask & select.POLLERR :
671            maskval.append("POLLERR")
672        if mask & select.POLLHUP :
673            maskval.append("POLLHUP")
674        if mask & select.POLLNVAL :
675            maskval.append("POLLNVAL")
676        return "%s (%s)" % (fd, " | ".join(maskval))
677       
678    def runOriginalBackend(self) :   
679        """Launches the original backend."""
680        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
681        arguments = sys.argv
682        self.logDebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in ([os.environ["DEVICE_URI"]] + arguments[1:])])))
683        subprocess = Popen4ForCUPS([originalbackend] + arguments[1:], bufsize=0, arg0=os.environ["DEVICE_URI"])
684       
685        # Save file descriptors, we will need them later.
686        stderrfno = sys.stderr.fileno()
687        fromcfno = subprocess.fromchild.fileno()
688        tocfno = subprocess.tochild.fileno()
689       
690        # We will have to be careful when dealing with I/O
691        # So we use a poll object to know when to read or write
692        pollster = select.poll()
693        pollster.register(fromcfno, select.POLLIN | select.POLLPRI)
694        pollster.register(stderrfno, select.POLLOUT)
695        pollster.register(tocfno, select.POLLOUT)
696       
697        # Initialize our buffers
698        indata = ""
699        outdata = ""
700        endinput = endoutput = 0
701        inputclosed = outputclosed = 0
702        totaltochild = totalfromcups = 0
703        totalfromchild = totaltocups = 0
704       
705        if self.InputFile is None :
706           # this is not a real file, we read the job's data
707            # from our temporary file which is a copy of stdin
708            inf = open(self.DataFile, "rb")
709            infno = inf.fileno()
710            pollster.register(infno, select.POLLIN | select.POLLPRI)
711        else :   
712            # job's data is in a file, no need to pass the data
713            # to the original backend
714            self.logDebug("Job's data is in %s" % self.InputFile)
715            infno = None
716            endinput = 1
717       
718        self.logDebug("Entering streams polling loop...")
719        MEGABYTE = 1024*1024
720        killed = 0
721        status = -1
722        while (status == -1) and (not killed) and not (inputclosed and outputclosed) :
723            # First check if original backend is still alive
724            status = subprocess.poll()
725           
726            # Now if we got SIGTERM, we have
727            # to kill -TERM the original backend
728            if self.gotSigTerm and not killed :
729                try :
730                    os.kill(subprocess.pid, signal.SIGTERM)
731                except OSError, msg : # ignore but logs if process was already killed.
732                    self.logDebug("Error while sending signal to pid %s : %s" % (subprocess.pid, msg))
733                else :   
734                    self.logInfo(_("SIGTERM was sent to original backend %s (PID %s)") % (originalbackend, subprocess.pid))
735                    killed = 1
736           
737            # In any case, deal with any remaining I/O
738            try :
739                availablefds = pollster.poll(5000)
740            except select.error, msg :   
741                self.logDebug("Interrupted poll : %s" % msg)
742                availablefds = []
743            if not availablefds :
744                self.logDebug("Nothing to do, sleeping a bit...")
745                time.sleep(0.01) # give some time to the system
746            else :
747                for (fd, mask) in availablefds :
748                    try :
749                        if mask & select.POLLOUT :
750                            # We can write
751                            if fd == tocfno :
752                                if indata :
753                                    try :
754                                        nbwritten = os.write(fd, indata)   
755                                    except (OSError, IOError), msg :   
756                                        self.logDebug("Error while writing to original backend's stdin %s : %s" % (fd, msg))
757                                    else :   
758                                        if len(indata) != nbwritten :
759                                            self.logDebug("Short write to original backend's input !")
760                                        totaltochild += nbwritten   
761                                        self.logDebug("%s bytes sent to original backend so far..." % totaltochild)
762                                        indata = indata[nbwritten:]
763                                else :       
764                                    self.logDebug("No data to send to original backend yet, sleeping a bit...")
765                                    time.sleep(0.01)
766                                   
767                                if endinput :   
768                                    self.unregisterFileNo(pollster, tocfno)       
769                                    self.logDebug("Closing original backend's stdin.")
770                                    os.close(tocfno)
771                                    inputclosed = 1
772                            elif fd == stderrfno :
773                                if outdata :
774                                    try :
775                                        nbwritten = os.write(fd, outdata)
776                                    except (OSError, IOError), msg :   
777                                        self.logDebug("Error while writing to CUPS back channel (stderr) %s : %s" % (fd, msg))
778                                    else :
779                                        if len(outdata) != nbwritten :
780                                            self.logDebug("Short write to stderr (CUPS) !")
781                                        totaltocups += nbwritten   
782                                        self.logDebug("%s bytes sent back to CUPS so far..." % totaltocups)
783                                        outdata = outdata[nbwritten:]
784                                else :       
785                                    # self.logDebug("No data to send back to CUPS yet, sleeping a bit...") # Uncommenting this fills your logs
786                                    time.sleep(0.01) # Give some time to the system, stderr is ALWAYS writeable it seems.
787                                   
788                                if endoutput :   
789                                    self.unregisterFileNo(pollster, stderrfno)       
790                                    outputclosed = 1
791                            else :   
792                                self.logDebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
793                                time.sleep(0.01)
794                               
795                        if mask & (select.POLLIN | select.POLLPRI) :     
796                            # We have something to read
797                            try :
798                                data = os.read(fd, MEGABYTE)
799                            except (IOError, OSError), msg :   
800                                self.logDebug("Error while reading file %s : %s" % (fd, msg))
801                            else :
802                                if fd == infno :
803                                    if not data :    # If yes, then no more input data
804                                        self.unregisterFileNo(pollster, infno)
805                                        self.logDebug("Input data ends.")
806                                        endinput = 1 # this happens with real files.
807                                    else :   
808                                        indata += data
809                                        totalfromcups += len(data)
810                                        self.logDebug("%s bytes read from CUPS so far..." % totalfromcups)
811                                elif fd == fromcfno :
812                                    if not data :
813                                        self.logDebug("No back channel data to read from original backend yet, sleeping a bit...")
814                                        time.sleep(0.01)
815                                    else :
816                                        outdata += data
817                                        totalfromchild += len(data)
818                                        self.logDebug("%s bytes read from original backend so far..." % totalfromchild)
819                                else :   
820                                    self.logDebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
821                                    time.sleep(0.01)
822                                   
823                        if mask & (select.POLLHUP | select.POLLERR) :
824                            # Treat POLLERR as an EOF.
825                            # Some standard I/O stream has no more datas
826                            self.unregisterFileNo(pollster, fd)
827                            if fd == infno :
828                                # Here we are in the case where the input file is stdin.
829                                # which has no more data to be read.
830                                self.logDebug("Input data ends.")
831                                endinput = 1
832                            elif fd == fromcfno :   
833                                # We are no more interested in this file descriptor       
834                                self.logDebug("Closing original backend's stdout+stderr.")
835                                os.close(fromcfno)
836                                endoutput = 1
837                            else :   
838                                self.logDebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
839                                time.sleep(0.01)
840                               
841                        if mask & select.POLLNVAL :       
842                            self.logDebug("File %s was closed. Unregistering from polling object." % fd)
843                            self.unregisterFileNo(pollster, fd)
844                    except IOError, msg :           
845                        self.logDebug("Got an IOError : %s" % msg) # we got signalled during an I/O
846               
847        # We must close the original backend's input stream
848        if killed and not inputclosed :
849            self.logDebug("Forcing close of original backend's stdin.")
850            os.close(tocfno)
851       
852        self.logDebug("Exiting streams polling loop...")
853       
854        self.logDebug("input data's final length : %s" % len(indata))
855        self.logDebug("back-channel data's final length : %s" % len(outdata))
856       
857        self.logDebug("Total bytes read from CUPS (job's datas) : %s" % totalfromcups)
858        self.logDebug("Total bytes sent to original backend (job's datas) : %s" % totaltochild)
859       
860        self.logDebug("Total bytes read from original backend (back-channel datas) : %s" % totalfromchild)
861        self.logDebug("Total bytes sent back to CUPS (back-channel datas) : %s" % totaltocups)
862       
863        # Check exit code of original CUPS backend.   
864        if status == -1 :
865            # we exited the loop before the original backend exited
866            # now we have to wait for it to finish and get its status
867            self.logDebug("Waiting for original backend to exit...")
868            try :
869                status = subprocess.wait()
870            except OSError : # already dead : TODO : detect when abnormal
871                status = 0
872        if os.WIFEXITED(status) :
873            return os.WEXITSTATUS(status)
874        elif not killed :   
875            self.logInfo("CUPS backend %s died abnormally." % originalbackend, "error")
876            return -1
877        else :   
878            return 1
879       
880if __name__ == "__main__" :   
881    # This is a CUPS backend, we should act and die like a CUPS backend
882    wrapper = CupsBackend()
883    if len(sys.argv) == 1 :
884        print "\n".join(wrapper.discoverOtherBackends())
885        sys.exit(0)               
886    elif len(sys.argv) not in (6, 7) :   
887        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
888                              % sys.argv[0])
889        sys.exit(1)
890    else :   
891        try :
892            wrapper.readConfig()
893            wrapper.initBackend()
894            wrapper.saveDatasAndCheckSum()
895            wrapper.exportAttributes()
896            retcode = wrapper.runBranches()
897            wrapper.cleanUp()
898        except SystemExit, e :   
899            retcode = e.code
900        except :   
901            import traceback
902            lines = []
903            for line in traceback.format_exception(*sys.exc_info()) :
904                lines.extend([l for l in line.split("\n") if l])
905            msg = "ERROR: ".join(["%s (PID %s) : %s\n" % (wrapper.MyName, wrapper.pid, l) for l in (["ERROR: Tea4CUPS v%s" % version] + lines)])
906            sys.stderr.write(msg)
907            sys.stderr.flush()
908            retcode = 1
909        sys.exit(retcode)
Note: See TracBrowser for help on using the browser.