root / tea4cups / trunk / tea4cups @ 639

Revision 639, 44.1 kB (checked in by jerome, 19 years ago)

Integrated Peter Stuge's second patch

  • 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# (c) 2005 Peter Stuge <stuge-tea4cups@cdy.org>
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation; either version 2 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
20# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
21#
22# $Id$
23#
24#
25
26import sys
27import os
28import pwd
29import popen2
30import errno
31import md5
32import cStringIO
33import shlex
34import tempfile
35import ConfigParser
36import select
37import signal
38import time
39from struct import unpack
40
41version = "2.12alpha3_unofficial"
42
43class TeeError(Exception):
44    """Base exception for Tea4CUPS related stuff."""
45    def __init__(self, message = ""):
46        self.message = message
47        Exception.__init__(self, message)
48    def __repr__(self):
49        return self.message
50    __str__ = __repr__
51   
52class ConfigError(TeeError) :   
53    """Configuration related exceptions."""
54    pass 
55   
56class IPPError(TeeError) :   
57    """IPP related exceptions."""
58    pass 
59   
60class Popen4ForCUPS(popen2.Popen4) :
61    """Our own class to execute real backends.
62   
63       Their first argument is different from their path so using
64       native popen2.Popen3 would not be feasible.
65    """
66    def __init__(self, cmd, bufsize=-1, arg0=None) :
67        self.arg0 = arg0
68        popen2.Popen4.__init__(self, cmd, bufsize)
69       
70    def _run_child(self, cmd):
71        try :
72            MAXFD = os.sysconf("SC_OPEN_MAX")
73        except (AttributeError, ValueError) :   
74            MAXFD = 256
75        for i in range(3, MAXFD) : 
76            try:
77                os.close(i)
78            except OSError:
79                pass
80        try:
81            os.execvpe(cmd[0], [self.arg0 or cmd[0]] + cmd[1:], os.environ)
82        finally:
83            os._exit(1)
84   
85# Some IPP constants   
86OPERATION_ATTRIBUTES_TAG = 0x01
87JOB_ATTRIBUTES_TAG = 0x02
88END_OF_ATTRIBUTES_TAG = 0x03
89PRINTER_ATTRIBUTES_TAG = 0x04
90UNSUPPORTED_ATTRIBUTES_TAG = 0x05
91
92class IPPMessage :
93    """A class for IPP message files.
94   
95       Usage :
96       
97         fp = open("/var/spool/cups/c00001", "rb")
98         message = IPPMessage(fp.read())
99         fp.close()
100         print "IPP version : %s" % message.version
101         print "IPP operation Id : %s" % message.operation_id
102         print "IPP request Id : %s" % message.request_id
103         for attrtype in ("operation", "job", "printer", "unsupported") :
104             attrdict = getattr(message, "%s_attributes" % attrtype)
105             if attrdict :
106                 print "%s attributes :" % attrtype.title()
107                 for key in attrdict.keys() :
108                     print "  %s : %s" % (key, attrdict[key])
109    """
110    attributes_types = ("operation", "job", "printer", "unsupported")
111    def __init__(self, data, debug=0) :
112        """Initializes and parses IPP Message object.
113       
114           Parameters :
115           
116             data : the IPP Message's content.
117             debug : a boolean value to output debug info on stderr.
118        """
119        self.debug = debug
120        self.data = data
121        for attrtype in self.attributes_types :
122            setattr(self, "%s_attributes" % attrtype, {})
123        self.tags = [ None ] * 256      # by default all tags reserved
124       
125        # Delimiter tags
126        self.tags[OPERATION_ATTRIBUTES_TAG] = "operation-attributes-tag"
127        self.tags[JOB_ATTRIBUTES_TAG] = "job-attributes-tag"
128        self.tags[END_OF_ATTRIBUTES_TAG] = "end-of-attributes-tag"
129        self.tags[PRINTER_ATTRIBUTES_TAG] = "printer-attributes-tag"
130        self.tags[UNSUPPORTED_ATTRIBUTES_TAG] = "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 self.attributes_types :
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, self.config.get("global", k)) for k in self.config.options("global") if k.startswith(branchbasename) ]
324        except ConfigParser.NoSectionError, msg :   
325            raise ConfigError, "Invalid configuration file : %s" % msg
326        try :
327            sectionbranches = [ (k, self.config.get(printqueuename, k)) for k in self.config.options(printqueuename) if k.startswith(branchbasename) ]
328        except ConfigParser.NoSectionError, msg :   
329            self.logInfo("No section for print queue %s : %s" % (printqueuename, msg))
330            sectionbranches = []
331        branches = {}
332        for (k, v) in globalbranches :
333            value = v.strip()
334            if value :
335                branches[k] = value
336        for (k, v) in sectionbranches :   
337            value = v.strip()
338            if value :
339                branches[k] = value # overwrite any global option or set a new value
340            else :   
341                del branches[k] # empty value disables a global option
342        return branches
343       
344    def discoverOtherBackends(self) :   
345        """Discovers the other CUPS backends.
346       
347           Executes each existing backend in turn in device enumeration mode.
348           Returns the list of available backends.
349        """
350        # Unfortunately this method can't output any debug information
351        # to stdout or stderr, else CUPS considers that the device is
352        # not available.
353        available = []
354        (directory, myname) = os.path.split(sys.argv[0])
355        if not directory :
356            directory = "./"
357        tmpdir = tempfile.gettempdir()
358        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
359        if os.path.exists(lockfilename) :
360            lockfile = open(lockfilename, "r")
361            pid = int(lockfile.read())
362            lockfile.close()
363            try :
364                # see if the pid contained in the lock file is still running
365                os.kill(pid, 0)
366            except OSError, e :   
367                if e.errno != errno.EPERM :
368                    # process doesn't exist anymore
369                    os.remove(lockfilename)
370           
371        if not os.path.exists(lockfilename) :
372            lockfile = open(lockfilename, "w")
373            lockfile.write("%i" % self.pid)
374            lockfile.close()
375            allbackends = [ os.path.join(directory, b) \
376                                for b in os.listdir(directory) 
377                                    if os.access(os.path.join(directory, b), os.X_OK) \
378                                        and (b != myname)] 
379            for backend in allbackends :                           
380                answer = os.popen(backend, "r")
381                try :
382                    devices = [line.strip() for line in answer.readlines()]
383                except :   
384                    devices = []
385                status = answer.close()
386                if status is None :
387                    for d in devices :
388                        # each line is of the form :
389                        # 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
390                        # so we have to decompose it carefully
391                        fdevice = cStringIO.StringIO(d)
392                        tokenizer = shlex.shlex(fdevice)
393                        tokenizer.wordchars = tokenizer.wordchars + \
394                                                        r".:,?!~/\_$*-+={}[]()#"
395                        arguments = []
396                        while 1 :
397                            token = tokenizer.get_token()
398                            if token :
399                                arguments.append(token)
400                            else :
401                                break
402                        fdevice.close()
403                        try :
404                            (devicetype, device, name, fullname) = arguments
405                        except ValueError :   
406                            pass    # ignore this 'bizarre' device
407                        else :   
408                            if name.startswith('"') and name.endswith('"') :
409                                name = name[1:-1]
410                            if fullname.startswith('"') and fullname.endswith('"') :
411                                fullname = fullname[1:-1]
412                            available.append('%s %s:%s "%s+%s" "%s managed %s"' \
413                                                 % (devicetype, self.myname, device, self.MyName, name, self.MyName, fullname))
414            os.remove(lockfilename)
415        available.append('direct %s:// "%s+Nothing" "%s managed Virtual Printer"' \
416                             % (self.myname, self.MyName, self.MyName))
417        return available
418                       
419    def initBackend(self) :   
420        """Initializes the backend's attributes."""
421        # check that the DEVICE_URI environment variable's value is
422        # prefixed with self.myname otherwise don't touch it.
423        # If this is the case, we have to remove the prefix from
424        # the environment before launching the real backend
425        muststartwith = "%s:" % self.myname
426        device_uri = os.environ.get("DEVICE_URI", "")
427        if device_uri.startswith(muststartwith) :
428            fulldevice_uri = device_uri[:]
429            device_uri = fulldevice_uri[len(muststartwith):]
430            for i in range(2) :
431                if device_uri.startswith("/") : 
432                    device_uri = device_uri[1:]
433        try :
434            (backend, destination) = device_uri.split(":", 1) 
435        except ValueError :   
436            if not device_uri :
437                self.logDebug("Not attached to an existing print queue.")
438                backend = ""
439            else :   
440                raise TeeError, "Invalid DEVICE_URI : %s\n" % device_uri
441       
442        self.JobId = sys.argv[1].strip()
443        self.UserName = sys.argv[2].strip() or pwd.getpwuid(os.geteuid())[0] # use CUPS' user when printing test pages from CUPS' web interface
444        self.Title = sys.argv[3].strip()
445        self.Copies = int(sys.argv[4].strip())
446        self.Options = sys.argv[5].strip()
447        if len(sys.argv) == 7 :
448            self.InputFile = sys.argv[6] # read job's datas from file
449        else :   
450            self.InputFile = None        # read job's datas from stdin
451           
452        self.RealBackend = backend
453        self.DeviceURI = device_uri
454        self.PrinterName = os.environ.get("PRINTER", "")
455        self.Directory = self.getPrintQueueOption(self.PrinterName, "directory")
456        self.DataFile = os.path.join(self.Directory, "%s-%s-%s-%s" % (self.myname, self.PrinterName, self.UserName, self.JobId))
457        (ippfilename, ippmessage) = self.parseIPPMessageFile()
458        self.ControlFile = ippfilename
459        (chtype, self.ClientHost) = ippmessage.operation_attributes.get("job-originating-host-name", \
460                                          ippmessage.job_attributes.get("job-originating-host-name", (None, None)))
461        (jbtype, self.JobBilling) = ippmessage.job_attributes.get("job-billing", (None, None))
462           
463    def getCupsConfigDirectives(self, directives=[]) :
464        """Retrieves some CUPS directives from its configuration file.
465       
466           Returns a mapping with lowercased directives as keys and
467           their setting as values.
468        """
469        dirvalues = {} 
470        cupsroot = os.environ.get("CUPS_SERVERROOT", "/etc/cups")
471        cupsdconf = os.path.join(cupsroot, "cupsd.conf")
472        try :
473            conffile = open(cupsdconf, "r")
474        except IOError :   
475            raise TeeError, "Unable to open %s" % cupsdconf
476        else :   
477            for line in conffile.readlines() :
478                linecopy = line.strip().lower()
479                for di in [d.lower() for d in directives] :
480                    if linecopy.startswith("%s " % di) :
481                        try :
482                            val = line.split()[1]
483                        except :   
484                            pass # ignore errors, we take the last value in any case.
485                        else :   
486                            dirvalues[di] = val
487            conffile.close()           
488        return dirvalues       
489           
490    def parseIPPMessageFile(self) :       
491        """Parses the IPP message file and returns a tuple (filename, parsedvalue)."""
492        cupsdconf = self.getCupsConfigDirectives(["RequestRoot"])
493        requestroot = cupsdconf.get("requestroot", "/var/spool/cups")
494        if (len(self.JobId) < 5) and self.JobId.isdigit() :
495            ippmessagefile = "c%05i" % int(self.JobId)
496        else :   
497            ippmessagefile = "c%s" % self.JobId
498        ippmessagefile = os.path.join(requestroot, ippmessagefile)
499        ippmessage = {}
500        try :
501            ippdatafile = open(ippmessagefile)
502        except :   
503            self.logInfo("Unable to open IPP message file %s" % ippmessagefile, "warn")
504        else :   
505            self.logDebug("Parsing of IPP message file %s begins." % ippmessagefile)
506            try :
507                ippmessage = IPPMessage(ippdatafile.read())
508            except IPPError, msg :   
509                self.logInfo("Error while parsing %s : %s" % (ippmessagefile, msg), "warn")
510            else :   
511                self.logDebug("Parsing of IPP message file %s ends." % ippmessagefile)
512            ippdatafile.close()
513        return (ippmessagefile, ippmessage)
514               
515    def exportAttributes(self) :   
516        """Exports our backend's attributes to the environment."""
517        os.environ["DEVICE_URI"] = self.DeviceURI       # WARNING !
518        os.environ["TEAPRINTERNAME"] = self.PrinterName
519        os.environ["TEADIRECTORY"] = self.Directory
520        os.environ["TEADATAFILE"] = self.DataFile
521        os.environ["TEAJOBSIZE"] = str(self.JobSize)
522        os.environ["TEAMD5SUM"] = self.JobMD5Sum
523        os.environ["TEACLIENTHOST"] = self.ClientHost or ""
524        os.environ["TEAJOBID"] = self.JobId
525        os.environ["TEAUSERNAME"] = self.UserName
526        os.environ["TEATITLE"] = self.Title
527        os.environ["TEACOPIES"] = str(self.Copies)
528        os.environ["TEAOPTIONS"] = self.Options
529        os.environ["TEAINPUTFILE"] = self.InputFile or ""
530        os.environ["TEABILLING"] = self.JobBilling or ""
531        os.environ["TEACONTROLFILE"] = self.ControlFile
532       
533    def saveDatasAndCheckSum(self) :
534        """Saves the input datas into a static file."""
535        self.logDebug("Duplicating data stream into %s" % self.DataFile)
536        mustclose = 0
537        if self.InputFile is not None :
538            infile = open(self.InputFile, "rb")
539            mustclose = 1
540        else :   
541            infile = sys.stdin
542        CHUNK = 64*1024         # read 64 Kb at a time
543        dummy = 0
544        sizeread = 0
545        checksum = md5.new()
546        outfile = open(self.DataFile, "wb")   
547        while 1 :
548            data = infile.read(CHUNK) 
549            if not data :
550                break
551            sizeread += len(data)   
552            outfile.write(data)
553            checksum.update(data)   
554            if not (dummy % 32) : # Only display every 2 Mb
555                self.logDebug("%s bytes saved..." % sizeread)
556            dummy += 1   
557        outfile.close()
558        if mustclose :   
559            infile.close()
560        self.JobSize = sizeread   
561        self.JobMD5Sum = checksum.hexdigest()
562        self.logDebug("Job %s is %s bytes long." % (self.JobId, self.JobSize))
563        self.logDebug("Job %s MD5 sum is %s" % (self.JobId, self.JobMD5Sum))
564
565    def cleanUp(self) :
566        """Cleans up the place."""
567        if not self.isTrue(self.getPrintQueueOption(self.PrinterName, "keepfiles", ignore=1)) :
568            os.remove(self.DataFile)
569           
570    def sigtermHandler(self, signum, frame) :
571        """Sets an attribute whenever SIGTERM is received."""
572        self.gotSigTerm = 1
573        self.logInfo("SIGTERM received for Job %s." % self.JobId)
574       
575    def runBranches(self) :         
576        """Launches each hook or tee defined for the current print queue."""
577        exitcode = 0
578        self.isCancelled = 0    # did a prehook cancel the print job ?
579        self.gotSigTerm = 0
580        signal.signal(signal.SIGTERM, self.sigtermHandler)
581        serialize = self.isTrue(self.getPrintQueueOption(self.PrinterName, "serialize", ignore=1))
582        self.pipes = { 0: os.pipe() }
583        for branchtype in ["prehook", "tee", "posthook"] :
584            if branchtype == "posthook" :
585                os.close(self.pipes[0][1])
586            branches = self.enumBranches(self.PrinterName, branchtype)
587            for b in branches :
588                bname = b.split("_", 1)[1]
589                if branchtype != "posthook" and bname not in self.pipes :
590                    self.pipes[bname] = os.pipe()
591                else :
592                    if bname in self.pipes :
593                        os.close(self.pipes[bname][1])
594            status = self.runCommands(branchtype, branches, serialize)
595            if status :
596                if branchtype != "posthook" :
597                    exitcode = status
598                else :   
599                    # we just ignore error in posthooks
600                    self.logInfo("An error occured during the execution of posthooks.", "warn")
601            if (branchtype == "prehook") and self.isCancelled :
602                break # We don't want to execute tees or posthooks in this case
603        signal.signal(signal.SIGTERM, signal.SIG_IGN)
604        for p in self.pipes :
605            os.close(self.pipes[p][0])
606            try :
607                os.close(self.pipes[p][1])
608            except OSError :
609                pass
610        if not exitcode :
611            self.logInfo("OK")
612        else :   
613            self.logInfo("An error occured, please check CUPS' error_log file.")
614        return exitcode
615       
616    def stdioRedirSystem(self, cmd, stdin=0, stdout=1) :
617        """Launches a command with stdio redirected."""
618        # Code contributed by Peter Stuge on May 23rd and June 7th 2005
619        pid = os.fork()
620        if pid == 0 :
621            if stdin != 0 :
622                os.dup2(stdin, 0)
623                os.close(stdin)
624            if stdout != 1 :
625                os.dup2(stdout, 1)
626                os.close(stdout)
627            try :
628                os.execl("/bin/sh", "sh", "-c", cmd)
629            except OSError , msg :
630                self.logDebug("execl() failed: %s" % msg)
631            os._exit(1)
632        return os.waitpid(pid, 0)[1]
633       
634    def runCommand(self, branch, command) :
635        """Runs a particular branch command."""
636        # Code contributed by Peter Stuge on June 7th 2005
637        self.logDebug("Launching %s : %s" % (branch, command))
638        btype, bname = branch.split("_", 1)
639        if bname not in self.pipes :
640          bname = 0
641        if btype != "posthook" :
642            return self.stdioRedirSystem(command, 0, self.pipes[bname][1])
643        else :
644            return self.stdioRedirSystem(command, self.pipes[bname][0])
645
646    def runCommands(self, btype, branches, serialize) :   
647        """Runs the commands for a particular branch type."""
648        exitcode = 0 
649        btype = btype.lower()
650        btypetitle = btype.title()
651        branchlist = branches.keys()   
652        branchlist.sort()
653        if serialize :
654            self.logDebug("Begin serialized %ss" % btypetitle)
655            if (btype == "tee") and self.RealBackend :
656                self.logDebug("Launching original backend %s for printer %s" % (self.RealBackend, self.PrinterName))
657                retcode = self.runOriginalBackend()
658                if os.WIFEXITED(retcode) :
659                    retcode = os.WEXITSTATUS(retcode)
660                os.environ["TEASTATUS"] = str(retcode)
661                exitcode = retcode
662            for branch in branchlist :
663                command = branches[branch]
664                if self.gotSigTerm :
665                    break
666                retcode = self.runCommand(branch, command)
667                self.logDebug("Exit code for %s %s on printer %s is %s" % (btype, branch, self.PrinterName, retcode))
668                if os.WIFEXITED(retcode) :
669                    retcode = os.WEXITSTATUS(retcode)
670                if retcode :   
671                    if (btype == "prehook") and (retcode == 255) : # -1
672                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
673                        self.isCancelled = 1
674                    else :   
675                        self.logInfo("%s %s on printer %s didn't exit successfully." % (btypetitle, branch, self.PrinterName), "error")
676                        exitcode = 1
677            self.logDebug("End serialized %ss" % btypetitle)
678        else :       
679            self.logDebug("Begin forked %ss" % btypetitle)
680            pids = {}
681            if (btype == "tee") and self.RealBackend :
682                branches["Original backend"] = None     # Fakes a tee to launch one more child
683                branchlist = ["Original backend"] + branchlist
684            for branch in branchlist :
685                command = branches[branch]
686                if self.gotSigTerm :
687                    break
688                pid = os.fork()
689                if pid :
690                    pids[branch] = pid
691                else :   
692                    if branch == "Original backend" :
693                        self.logDebug("Launching original backend %s for printer %s" % (self.RealBackend, self.PrinterName))
694                        sys.exit(self.runOriginalBackend())
695                    else :
696                        retcode = self.runCommand(branch, command)
697                        if os.WIFEXITED(retcode) :
698                            retcode = os.WEXITSTATUS(retcode)
699                        else :   
700                            retcode = -1
701                        sys.exit(retcode)
702            for (branch, pid) in pids.items() :
703                (childpid, retcode) = os.waitpid(pid, 0)
704                self.logDebug("Exit code for %s %s (PID %s) on printer %s is %s" % (btype, branch, childpid, self.PrinterName, retcode))
705                if os.WIFEXITED(retcode) :
706                    retcode = os.WEXITSTATUS(retcode)
707                if retcode :   
708                    if (btype == "prehook") and (retcode == 255) : # -1
709                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
710                        self.isCancelled = 1
711                    else :   
712                        self.logInfo("%s %s (PID %s) on printer %s didn't exit successfully." % (btypetitle, branch, childpid, self.PrinterName), "error")
713                        exitcode = 1
714                if branch == "Original backend" :   
715                    os.environ["TEASTATUS"] = str(retcode)
716            self.logDebug("End forked %ss" % btypetitle)
717        return exitcode
718       
719    def unregisterFileNo(self, pollobj, fileno) :               
720        """Removes a file handle from the polling object."""
721        try :
722            pollobj.unregister(fileno)
723        except KeyError :   
724            self.logInfo("File number %s unregistered twice from polling object, ignored." % fileno, "warn")
725        except :   
726            self.logDebug("Error while unregistering file number %s from polling object." % fileno)
727        else :   
728            self.logDebug("File number %s unregistered from polling object." % fileno)
729           
730    def formatFileEvent(self, fd, mask) :       
731        """Formats file debug info."""
732        maskval = []
733        if mask & select.POLLIN :
734            maskval.append("POLLIN")
735        if mask & select.POLLOUT :
736            maskval.append("POLLOUT")
737        if mask & select.POLLPRI :
738            maskval.append("POLLPRI")
739        if mask & select.POLLERR :
740            maskval.append("POLLERR")
741        if mask & select.POLLHUP :
742            maskval.append("POLLHUP")
743        if mask & select.POLLNVAL :
744            maskval.append("POLLNVAL")
745        return "%s (%s)" % (fd, " | ".join(maskval))
746       
747    def runOriginalBackend(self) :   
748        """Launches the original backend."""
749        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
750        arguments = sys.argv
751        self.logDebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in ([os.environ["DEVICE_URI"]] + arguments[1:])])))
752        subprocess = Popen4ForCUPS([originalbackend] + arguments[1:], bufsize=0, arg0=os.environ["DEVICE_URI"])
753       
754        # Save file descriptors, we will need them later.
755        stderrfno = sys.stderr.fileno()
756        fromcfno = subprocess.fromchild.fileno()
757        tocfno = subprocess.tochild.fileno()
758       
759        # We will have to be careful when dealing with I/O
760        # So we use a poll object to know when to read or write
761        pollster = select.poll()
762        pollster.register(fromcfno, select.POLLIN | select.POLLPRI)
763        pollster.register(stderrfno, select.POLLOUT)
764        pollster.register(tocfno, select.POLLOUT)
765       
766        # Initialize our buffers
767        indata = ""
768        outdata = ""
769        endinput = endoutput = 0
770        inputclosed = outputclosed = 0
771        totaltochild = totalfromcups = 0
772        totalfromchild = totaltocups = 0
773       
774        if self.InputFile is None :
775           # this is not a real file, we read the job's data
776            # from our temporary file which is a copy of stdin
777            inf = open(self.DataFile, "rb")
778            infno = inf.fileno()
779            pollster.register(infno, select.POLLIN | select.POLLPRI)
780        else :   
781            # job's data is in a file, no need to pass the data
782            # to the original backend
783            self.logDebug("Job's data is in %s" % self.InputFile)
784            infno = None
785            endinput = 1
786       
787        self.logDebug("Entering streams polling loop...")
788        MEGABYTE = 1024*1024
789        killed = 0
790        status = -1
791        while (status == -1) and (not killed) and not (inputclosed and outputclosed) :
792            # First check if original backend is still alive
793            status = subprocess.poll()
794           
795            # Now if we got SIGTERM, we have
796            # to kill -TERM the original backend
797            if self.gotSigTerm and not killed :
798                try :
799                    os.kill(subprocess.pid, signal.SIGTERM)
800                except OSError, msg : # ignore but logs if process was already killed.
801                    self.logDebug("Error while sending signal to pid %s : %s" % (subprocess.pid, msg))
802                else :   
803                    self.logInfo(_("SIGTERM was sent to original backend %s (PID %s)") % (originalbackend, subprocess.pid))
804                    killed = 1
805           
806            # In any case, deal with any remaining I/O
807            try :
808                availablefds = pollster.poll(5000)
809            except select.error, msg :   
810                self.logDebug("Interrupted poll : %s" % msg)
811                availablefds = []
812            if not availablefds :
813                self.logDebug("Nothing to do, sleeping a bit...")
814                time.sleep(0.01) # give some time to the system
815            else :
816                for (fd, mask) in availablefds :
817                    try :
818                        if mask & select.POLLOUT :
819                            # We can write
820                            if fd == tocfno :
821                                if indata :
822                                    try :
823                                        nbwritten = os.write(fd, indata)   
824                                    except (OSError, IOError), msg :   
825                                        self.logDebug("Error while writing to original backend's stdin %s : %s" % (fd, msg))
826                                    else :   
827                                        if len(indata) != nbwritten :
828                                            self.logDebug("Short write to original backend's input !")
829                                        totaltochild += nbwritten   
830                                        self.logDebug("%s bytes sent to original backend so far..." % totaltochild)
831                                        indata = indata[nbwritten:]
832                                else :       
833                                    self.logDebug("No data to send to original backend yet, sleeping a bit...")
834                                    time.sleep(0.01)
835                                   
836                                if endinput :   
837                                    self.unregisterFileNo(pollster, tocfno)       
838                                    self.logDebug("Closing original backend's stdin.")
839                                    os.close(tocfno)
840                                    inputclosed = 1
841                            elif fd == stderrfno :
842                                if outdata :
843                                    try :
844                                        nbwritten = os.write(fd, outdata)
845                                    except (OSError, IOError), msg :   
846                                        self.logDebug("Error while writing to CUPS back channel (stderr) %s : %s" % (fd, msg))
847                                    else :
848                                        if len(outdata) != nbwritten :
849                                            self.logDebug("Short write to stderr (CUPS) !")
850                                        totaltocups += nbwritten   
851                                        self.logDebug("%s bytes sent back to CUPS so far..." % totaltocups)
852                                        outdata = outdata[nbwritten:]
853                                else :       
854                                    # self.logDebug("No data to send back to CUPS yet, sleeping a bit...") # Uncommenting this fills your logs
855                                    time.sleep(0.01) # Give some time to the system, stderr is ALWAYS writeable it seems.
856                                   
857                                if endoutput :   
858                                    self.unregisterFileNo(pollster, stderrfno)       
859                                    outputclosed = 1
860                            else :   
861                                self.logDebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
862                                time.sleep(0.01)
863                               
864                        if mask & (select.POLLIN | select.POLLPRI) :     
865                            # We have something to read
866                            try :
867                                data = os.read(fd, MEGABYTE)
868                            except (IOError, OSError), msg :   
869                                self.logDebug("Error while reading file %s : %s" % (fd, msg))
870                            else :
871                                if fd == infno :
872                                    if not data :    # If yes, then no more input data
873                                        self.unregisterFileNo(pollster, infno)
874                                        self.logDebug("Input data ends.")
875                                        endinput = 1 # this happens with real files.
876                                    else :   
877                                        indata += data
878                                        totalfromcups += len(data)
879                                        self.logDebug("%s bytes read from CUPS so far..." % totalfromcups)
880                                elif fd == fromcfno :
881                                    if not data :
882                                        self.logDebug("No back channel data to read from original backend yet, sleeping a bit...")
883                                        time.sleep(0.01)
884                                    else :
885                                        outdata += data
886                                        totalfromchild += len(data)
887                                        self.logDebug("%s bytes read from original backend so far..." % totalfromchild)
888                                else :   
889                                    self.logDebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
890                                    time.sleep(0.01)
891                                   
892                        if mask & (select.POLLHUP | select.POLLERR) :
893                            # Treat POLLERR as an EOF.
894                            # Some standard I/O stream has no more datas
895                            self.unregisterFileNo(pollster, fd)
896                            if fd == infno :
897                                # Here we are in the case where the input file is stdin.
898                                # which has no more data to be read.
899                                self.logDebug("Input data ends.")
900                                endinput = 1
901                            elif fd == fromcfno :   
902                                # We are no more interested in this file descriptor       
903                                self.logDebug("Closing original backend's stdout+stderr.")
904                                os.close(fromcfno)
905                                endoutput = 1
906                            else :   
907                                self.logDebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
908                                time.sleep(0.01)
909                               
910                        if mask & select.POLLNVAL :       
911                            self.logDebug("File %s was closed. Unregistering from polling object." % fd)
912                            self.unregisterFileNo(pollster, fd)
913                    except IOError, msg :           
914                        self.logDebug("Got an IOError : %s" % msg) # we got signalled during an I/O
915               
916        # We must close the original backend's input stream
917        if killed and not inputclosed :
918            self.logDebug("Forcing close of original backend's stdin.")
919            os.close(tocfno)
920       
921        self.logDebug("Exiting streams polling loop...")
922       
923        self.logDebug("input data's final length : %s" % len(indata))
924        self.logDebug("back-channel data's final length : %s" % len(outdata))
925       
926        self.logDebug("Total bytes read from CUPS (job's datas) : %s" % totalfromcups)
927        self.logDebug("Total bytes sent to original backend (job's datas) : %s" % totaltochild)
928       
929        self.logDebug("Total bytes read from original backend (back-channel datas) : %s" % totalfromchild)
930        self.logDebug("Total bytes sent back to CUPS (back-channel datas) : %s" % totaltocups)
931       
932        # Check exit code of original CUPS backend.   
933        if status == -1 :
934            # we exited the loop before the original backend exited
935            # now we have to wait for it to finish and get its status
936            self.logDebug("Waiting for original backend to exit...")
937            try :
938                status = subprocess.wait()
939            except OSError : # already dead : TODO : detect when abnormal
940                status = 0
941        if os.WIFEXITED(status) :
942            return os.WEXITSTATUS(status)
943        elif not killed :   
944            self.logInfo("CUPS backend %s died abnormally." % originalbackend, "error")
945            return -1
946        else :   
947            return 1
948       
949if __name__ == "__main__" :   
950    # This is a CUPS backend, we should act and die like a CUPS backend
951    wrapper = CupsBackend()
952    if len(sys.argv) == 1 :
953        print "\n".join(wrapper.discoverOtherBackends())
954        sys.exit(0)               
955    elif len(sys.argv) not in (6, 7) :   
956        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
957                              % sys.argv[0])
958        sys.exit(1)
959    else :   
960        try :
961            wrapper.readConfig()
962            wrapper.initBackend()
963            wrapper.saveDatasAndCheckSum()
964            wrapper.exportAttributes()
965            retcode = wrapper.runBranches()
966            wrapper.cleanUp()
967        except SystemExit, e :   
968            retcode = e.code
969        except :   
970            import traceback
971            lines = []
972            for line in traceback.format_exception(*sys.exc_info()) :
973                lines.extend([l for l in line.split("\n") if l])
974            msg = "ERROR: ".join(["%s (PID %s) : %s\n" % (wrapper.MyName, wrapper.pid, l) for l in (["ERROR: Tea4CUPS v%s" % version] + lines)])
975            sys.stderr.write(msg)
976            sys.stderr.flush()
977            retcode = 1
978        sys.exit(retcode)
Note: See TracBrowser for help on using the browser.