root / tea4cups / trunk / tea4cups @ 635

Revision 635, 42.6 kB (checked in by jerome, 19 years ago)

Added support for IPP unsupported attributes :-)

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