root / tea4cups / trunk / tea4cups @ 640

Revision 640, 30.9 kB (checked in by stuge, 19 years ago)

Tees have been removed, the original backend is run with fd:s pointing
directly to the proper files rather than having to be read and written
by tea4cups, and a few small bug fixes and cleanups are also included.

Full list of changes:

* Remove the Popen4ForCUPS class
fork() and execve() directly in runOriginalBackend() instead.

* Added exception handling to logInfo()
When CUPS cancels a job before tea4cups is completely done stderr is
closed and writing to it would raise an IOError causing tea4cups to
abort without a chance to clean up.


* Rewrote most of runBranches() to remove tees and clean up a bit
Use retcode for consistency throughout program. Proper error
handling, falling through and doing the right thing whenever
something exits nonzero, removed the shared pipe and use stdin/stdout
instead. Make sure they aren't closed when all the pipes are closed.

* Changed stdioRedirSystem() to return actual exit code of command..
..instead of the encoded value returned by waitpid(), or -1 if the
command didn't exit with an exit code. (E.g. killed by a signal.)
Adjusted all places using the return value accordingly. Also fixed a
bug, if the execl() failed, the child would exit 1. Changed that to
-1 which has special meaning. (prehook cancels the job)

* Fixed indentation mistake and clean a condition in runCommand()
I use two spaces and had an incorrect indentation in the previous
patch. :) Now that tees are removed, == prehook is nicer than !=
posthook.

* Removed tee handling from runCommands() and some cleanup
This includes moving the call to runOriginalBackend() up one level
into runBranches(). Changed to os._exit() for forked hooks as
recommended by docs.python.org. sys.exit() is only supposed to be
used in the parent process. Finally, the child pid for forked hooks
is already known in the waitpid loop (in pid, from the pids dict) so
I removed the variable childpid.

* Removed unregisterFileNo() and formatFileEvents()
No longer used by runOriginalBackend() and not used anywhere else.

* Rewrote runOriginalBackend() per previous no-copy theory
Fork, open dataFile and dup2() if needed, execve original backend.
Log an error and exit -1 if execve() fails. Simplified reaping of the
original backend but it still has to keep track of SIGTERM and pass
it on if received during waitpid(). Log an error message with the
exit code of the original backend if nonzero.

  • 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   
60# Some IPP constants   
61OPERATION_ATTRIBUTES_TAG = 0x01
62JOB_ATTRIBUTES_TAG = 0x02
63END_OF_ATTRIBUTES_TAG = 0x03
64PRINTER_ATTRIBUTES_TAG = 0x04
65UNSUPPORTED_ATTRIBUTES_TAG = 0x05
66
67class IPPMessage :
68    """A class for IPP message files.
69   
70       Usage :
71       
72         fp = open("/var/spool/cups/c00001", "rb")
73         message = IPPMessage(fp.read())
74         fp.close()
75         print "IPP version : %s" % message.version
76         print "IPP operation Id : %s" % message.operation_id
77         print "IPP request Id : %s" % message.request_id
78         for attrtype in ("operation", "job", "printer", "unsupported") :
79             attrdict = getattr(message, "%s_attributes" % attrtype)
80             if attrdict :
81                 print "%s attributes :" % attrtype.title()
82                 for key in attrdict.keys() :
83                     print "  %s : %s" % (key, attrdict[key])
84    """
85    attributes_types = ("operation", "job", "printer", "unsupported")
86    def __init__(self, data, debug=0) :
87        """Initializes and parses IPP Message object.
88       
89           Parameters :
90           
91             data : the IPP Message's content.
92             debug : a boolean value to output debug info on stderr.
93        """
94        self.debug = debug
95        self.data = data
96        for attrtype in self.attributes_types :
97            setattr(self, "%s_attributes" % attrtype, {})
98        self.tags = [ None ] * 256      # by default all tags reserved
99       
100        # Delimiter tags
101        self.tags[OPERATION_ATTRIBUTES_TAG] = "operation-attributes-tag"
102        self.tags[JOB_ATTRIBUTES_TAG] = "job-attributes-tag"
103        self.tags[END_OF_ATTRIBUTES_TAG] = "end-of-attributes-tag"
104        self.tags[PRINTER_ATTRIBUTES_TAG] = "printer-attributes-tag"
105        self.tags[UNSUPPORTED_ATTRIBUTES_TAG] = "unsupported-attributes-tag"
106       
107        # out of band values
108        self.tags[0x10] = "unsupported"
109        self.tags[0x11] = "reserved-for-future-default"
110        self.tags[0x12] = "unknown"
111        self.tags[0x13] = "no-value"
112       
113        # integer values
114        self.tags[0x20] = "generic-integer"
115        self.tags[0x21] = "integer"
116        self.tags[0x22] = "boolean"
117        self.tags[0x23] = "enum"
118       
119        # octetString
120        self.tags[0x30] = "octetString-with-an-unspecified-format"
121        self.tags[0x31] = "dateTime"
122        self.tags[0x32] = "resolution"
123        self.tags[0x33] = "rangeOfInteger"
124        self.tags[0x34] = "reserved-for-collection"
125        self.tags[0x35] = "textWithLanguage"
126        self.tags[0x36] = "nameWithLanguage"
127       
128        # character strings
129        self.tags[0x20] = "generic-character-string"
130        self.tags[0x41] = "textWithoutLanguage"
131        self.tags[0x42] = "nameWithoutLanguage"
132        # self.tags[0x43] = "reserved"
133        self.tags[0x44] = "keyword"
134        self.tags[0x45] = "uri"
135        self.tags[0x46] = "uriScheme"
136        self.tags[0x47] = "charset"
137        self.tags[0x48] = "naturalLanguage"
138        self.tags[0x49] = "mimeMediaType"
139       
140        # now parses the IPP message
141        self.parse()
142       
143    def printInfo(self, msg) :   
144        """Prints a debug message."""
145        if self.debug :
146            sys.stderr.write("%s\n" % msg)
147            sys.stderr.flush()
148           
149    def parseTag(self) :   
150        """Extracts information from an IPP tag."""
151        pos = self.position
152        tagtype = self.tags[ord(self.data[pos])]
153        pos += 1
154        posend = pos2 = pos + 2
155        namelength = unpack(">H", self.data[pos:pos2])[0]
156        if not namelength :
157            name = self._curname
158        else :   
159            posend += namelength
160            self._curname = name = self.data[pos2:posend]
161        pos2 = posend + 2
162        valuelength = unpack(">H", self.data[posend:pos2])[0]
163        posend = pos2 + valuelength
164        value = self.data[pos2:posend]
165        if tagtype in ("integer", "enum") :
166            value = unpack(">I", value)[0]
167        oldval = self._curdict.setdefault(name, [])
168        oldval.append((tagtype, value))
169        self.printInfo("%s(%s) %s" % (name, tagtype, value))
170        return posend - self.position
171       
172    def operation_attributes_tag(self) : 
173        """Indicates that the parser enters into an operation-attributes-tag group."""
174        self.printInfo("Start of operation_attributes_tag")
175        self._curdict = self.operation_attributes
176        return self.parseTag()
177       
178    def job_attributes_tag(self) : 
179        """Indicates that the parser enters into a job-attributes-tag group."""
180        self.printInfo("Start of job_attributes_tag")
181        self._curdict = self.job_attributes
182        return self.parseTag()
183       
184    def printer_attributes_tag(self) : 
185        """Indicates that the parser enters into a printer-attributes-tag group."""
186        self.printInfo("Start of printer_attributes_tag")
187        self._curdict = self.printer_attributes
188        return self.parseTag()
189       
190    def unsupported_attributes_tag(self) : 
191        """Indicates that the parser enters into an unsupported-attributes-tag group."""
192        self.printInfo("Start of unsupported_attributes_tag")
193        self._curdict = self.unsupported_attributes
194        return self.parseTag()
195       
196    def parse(self) :
197        """Parses an IPP Message.
198       
199           NB : Only a subset of RFC2910 is implemented.
200        """
201        self._curname = None
202        self._curdict = None
203        self.version = "%s.%s" % (ord(self.data[0]), ord(self.data[1]))
204        self.operation_id = "0x%04x" % unpack(">H", self.data[2:4])[0]
205        self.request_id = "0x%08x" % unpack(">I", self.data[4:8])[0]
206        self.position = 8
207        try :
208            tag = ord(self.data[self.position])
209            while tag != END_OF_ATTRIBUTES_TAG :
210                self.position += 1
211                name = self.tags[tag]
212                if name is not None :
213                    func = getattr(self, name.replace("-", "_"), None)
214                    if func is not None :
215                        self.position += func()
216                        if ord(self.data[self.position]) > UNSUPPORTED_ATTRIBUTES_TAG :
217                            self.position -= 1
218                            continue
219                tag = ord(self.data[self.position])
220        except IndexError :
221            raise IPPError, "Unexpected end of IPP message."
222           
223        # Now transform all one-element lists into single values
224        for attrtype in self.attributes_types :
225            attrdict = getattr(self, "%s_attributes" % attrtype)
226            for (key, value) in attrdict.items() :
227                if len(value) == 1 :
228                    attrdict[key] = value[0]
229                   
230class FakeConfig :   
231    """Fakes a configuration file parser."""
232    def get(self, section, option, raw=0) :
233        """Fakes the retrieval of an option."""
234        raise ConfigError, "Invalid configuration file : no option %s in section [%s]" % (option, section)
235       
236class CupsBackend :
237    """Base class for tools with no database access."""
238    def __init__(self) :
239        """Initializes the CUPS backend wrapper."""
240        signal.signal(signal.SIGTERM, signal.SIG_IGN)
241        signal.signal(signal.SIGPIPE, signal.SIG_IGN)
242        self.MyName = "Tea4CUPS"
243        self.myname = "tea4cups"
244        self.pid = os.getpid()
245       
246    def readConfig(self) :   
247        """Reads the configuration file."""
248        confdir = os.environ.get("CUPS_SERVERROOT", ".") 
249        self.conffile = os.path.join(confdir, "%s.conf" % self.myname)
250        if os.path.isfile(self.conffile) :
251            self.config = ConfigParser.ConfigParser()
252            self.config.read([self.conffile])
253            self.debug = self.isTrue(self.getGlobalOption("debug", ignore=1))
254        else :   
255            self.config = FakeConfig()
256            self.debug = 1      # no config, so force debug mode !
257           
258    def logInfo(self, message, level="info") :       
259        """Logs a message to CUPS' error_log file."""
260        try :
261            sys.stderr.write("%s: %s v%s (PID %i) : %s\n" % (level.upper(), self.MyName, version, os.getpid(), message))
262            sys.stderr.flush()
263        except IOError :
264            pass
265       
266    def logDebug(self, message) :   
267        """Logs something to debug output if debug is enabled."""
268        if self.debug :
269            self.logInfo(message, level="debug")
270           
271    def isTrue(self, option) :       
272        """Returns 1 if option is set to true, else 0."""
273        if (option is not None) and (option.upper().strip() in ['Y', 'YES', '1', 'ON', 'T', 'TRUE']) :
274            return 1
275        else :   
276            return 0
277                       
278    def getGlobalOption(self, option, ignore=0) :   
279        """Returns an option from the global section, or raises a ConfigError if ignore is not set, else returns None."""
280        try :
281            return self.config.get("global", option, raw=1)
282        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :   
283            if not ignore :
284                raise ConfigError, "Option %s not found in section global of %s" % (option, self.conffile)
285               
286    def getPrintQueueOption(self, printqueuename, option, ignore=0) :   
287        """Returns an option from the printer section, or the global section, or raises a ConfigError."""
288        globaloption = self.getGlobalOption(option, ignore=1)
289        try :
290            return self.config.get(printqueuename, option, raw=1)
291        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :   
292            if globaloption is not None :
293                return globaloption
294            elif not ignore :
295                raise ConfigError, "Option %s not found in section [%s] of %s" % (option, printqueuename, self.conffile)
296               
297    def enumBranches(self, printqueuename, branchtype="tee") :
298        """Returns the list of branchtypes branches for a particular section's."""
299        branchbasename = "%s_" % branchtype.lower()
300        try :
301            globalbranches = [ (k, self.config.get("global", k)) for k in self.config.options("global") if k.startswith(branchbasename) ]
302        except ConfigParser.NoSectionError, msg :   
303            raise ConfigError, "Invalid configuration file : %s" % msg
304        try :
305            sectionbranches = [ (k, self.config.get(printqueuename, k)) for k in self.config.options(printqueuename) if k.startswith(branchbasename) ]
306        except ConfigParser.NoSectionError, msg :   
307            self.logInfo("No section for print queue %s : %s" % (printqueuename, msg))
308            sectionbranches = []
309        branches = {}
310        for (k, v) in globalbranches :
311            value = v.strip()
312            if value :
313                branches[k] = value
314        for (k, v) in sectionbranches :   
315            value = v.strip()
316            if value :
317                branches[k] = value # overwrite any global option or set a new value
318            else :   
319                del branches[k] # empty value disables a global option
320        return branches
321       
322    def discoverOtherBackends(self) :   
323        """Discovers the other CUPS backends.
324       
325           Executes each existing backend in turn in device enumeration mode.
326           Returns the list of available backends.
327        """
328        # Unfortunately this method can't output any debug information
329        # to stdout or stderr, else CUPS considers that the device is
330        # not available.
331        available = []
332        (directory, myname) = os.path.split(sys.argv[0])
333        if not directory :
334            directory = "./"
335        tmpdir = tempfile.gettempdir()
336        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
337        if os.path.exists(lockfilename) :
338            lockfile = open(lockfilename, "r")
339            pid = int(lockfile.read())
340            lockfile.close()
341            try :
342                # see if the pid contained in the lock file is still running
343                os.kill(pid, 0)
344            except OSError, e :   
345                if e.errno != errno.EPERM :
346                    # process doesn't exist anymore
347                    os.remove(lockfilename)
348           
349        if not os.path.exists(lockfilename) :
350            lockfile = open(lockfilename, "w")
351            lockfile.write("%i" % self.pid)
352            lockfile.close()
353            allbackends = [ os.path.join(directory, b) \
354                                for b in os.listdir(directory) 
355                                    if os.access(os.path.join(directory, b), os.X_OK) \
356                                        and (b != myname)] 
357            for backend in allbackends :                           
358                answer = os.popen(backend, "r")
359                try :
360                    devices = [line.strip() for line in answer.readlines()]
361                except :   
362                    devices = []
363                status = answer.close()
364                if status is None :
365                    for d in devices :
366                        # each line is of the form :
367                        # 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
368                        # so we have to decompose it carefully
369                        fdevice = cStringIO.StringIO(d)
370                        tokenizer = shlex.shlex(fdevice)
371                        tokenizer.wordchars = tokenizer.wordchars + \
372                                                        r".:,?!~/\_$*-+={}[]()#"
373                        arguments = []
374                        while 1 :
375                            token = tokenizer.get_token()
376                            if token :
377                                arguments.append(token)
378                            else :
379                                break
380                        fdevice.close()
381                        try :
382                            (devicetype, device, name, fullname) = arguments
383                        except ValueError :   
384                            pass    # ignore this 'bizarre' device
385                        else :   
386                            if name.startswith('"') and name.endswith('"') :
387                                name = name[1:-1]
388                            if fullname.startswith('"') and fullname.endswith('"') :
389                                fullname = fullname[1:-1]
390                            available.append('%s %s:%s "%s+%s" "%s managed %s"' \
391                                                 % (devicetype, self.myname, device, self.MyName, name, self.MyName, fullname))
392            os.remove(lockfilename)
393        available.append('direct %s:// "%s+Nothing" "%s managed Virtual Printer"' \
394                             % (self.myname, self.MyName, self.MyName))
395        return available
396                       
397    def initBackend(self) :   
398        """Initializes the backend's attributes."""
399        # check that the DEVICE_URI environment variable's value is
400        # prefixed with self.myname otherwise don't touch it.
401        # If this is the case, we have to remove the prefix from
402        # the environment before launching the real backend
403        muststartwith = "%s:" % self.myname
404        device_uri = os.environ.get("DEVICE_URI", "")
405        if device_uri.startswith(muststartwith) :
406            fulldevice_uri = device_uri[:]
407            device_uri = fulldevice_uri[len(muststartwith):]
408            for i in range(2) :
409                if device_uri.startswith("/") : 
410                    device_uri = device_uri[1:]
411        try :
412            (backend, destination) = device_uri.split(":", 1) 
413        except ValueError :   
414            if not device_uri :
415                self.logDebug("Not attached to an existing print queue.")
416                backend = ""
417            else :   
418                raise TeeError, "Invalid DEVICE_URI : %s\n" % device_uri
419       
420        self.JobId = sys.argv[1].strip()
421        self.UserName = sys.argv[2].strip() or pwd.getpwuid(os.geteuid())[0] # use CUPS' user when printing test pages from CUPS' web interface
422        self.Title = sys.argv[3].strip()
423        self.Copies = int(sys.argv[4].strip())
424        self.Options = sys.argv[5].strip()
425        if len(sys.argv) == 7 :
426            self.InputFile = sys.argv[6] # read job's datas from file
427        else :   
428            self.InputFile = None        # read job's datas from stdin
429           
430        self.RealBackend = backend
431        self.DeviceURI = device_uri
432        self.PrinterName = os.environ.get("PRINTER", "")
433        self.Directory = self.getPrintQueueOption(self.PrinterName, "directory")
434        self.DataFile = os.path.join(self.Directory, "%s-%s-%s-%s" % (self.myname, self.PrinterName, self.UserName, self.JobId))
435        (ippfilename, ippmessage) = self.parseIPPMessageFile()
436        self.ControlFile = ippfilename
437        (chtype, self.ClientHost) = ippmessage.operation_attributes.get("job-originating-host-name", \
438                                          ippmessage.job_attributes.get("job-originating-host-name", (None, None)))
439        (jbtype, self.JobBilling) = ippmessage.job_attributes.get("job-billing", (None, None))
440           
441    def getCupsConfigDirectives(self, directives=[]) :
442        """Retrieves some CUPS directives from its configuration file.
443       
444           Returns a mapping with lowercased directives as keys and
445           their setting as values.
446        """
447        dirvalues = {} 
448        cupsroot = os.environ.get("CUPS_SERVERROOT", "/etc/cups")
449        cupsdconf = os.path.join(cupsroot, "cupsd.conf")
450        try :
451            conffile = open(cupsdconf, "r")
452        except IOError :   
453            raise TeeError, "Unable to open %s" % cupsdconf
454        else :   
455            for line in conffile.readlines() :
456                linecopy = line.strip().lower()
457                for di in [d.lower() for d in directives] :
458                    if linecopy.startswith("%s " % di) :
459                        try :
460                            val = line.split()[1]
461                        except :   
462                            pass # ignore errors, we take the last value in any case.
463                        else :   
464                            dirvalues[di] = val
465            conffile.close()           
466        return dirvalues       
467           
468    def parseIPPMessageFile(self) :       
469        """Parses the IPP message file and returns a tuple (filename, parsedvalue)."""
470        cupsdconf = self.getCupsConfigDirectives(["RequestRoot"])
471        requestroot = cupsdconf.get("requestroot", "/var/spool/cups")
472        if (len(self.JobId) < 5) and self.JobId.isdigit() :
473            ippmessagefile = "c%05i" % int(self.JobId)
474        else :   
475            ippmessagefile = "c%s" % self.JobId
476        ippmessagefile = os.path.join(requestroot, ippmessagefile)
477        ippmessage = {}
478        try :
479            ippdatafile = open(ippmessagefile)
480        except :   
481            self.logInfo("Unable to open IPP message file %s" % ippmessagefile, "warn")
482        else :   
483            self.logDebug("Parsing of IPP message file %s begins." % ippmessagefile)
484            try :
485                ippmessage = IPPMessage(ippdatafile.read())
486            except IPPError, msg :   
487                self.logInfo("Error while parsing %s : %s" % (ippmessagefile, msg), "warn")
488            else :   
489                self.logDebug("Parsing of IPP message file %s ends." % ippmessagefile)
490            ippdatafile.close()
491        return (ippmessagefile, ippmessage)
492               
493    def exportAttributes(self) :   
494        """Exports our backend's attributes to the environment."""
495        os.environ["DEVICE_URI"] = self.DeviceURI       # WARNING !
496        os.environ["TEAPRINTERNAME"] = self.PrinterName
497        os.environ["TEADIRECTORY"] = self.Directory
498        os.environ["TEADATAFILE"] = self.DataFile
499        os.environ["TEAJOBSIZE"] = str(self.JobSize)
500        os.environ["TEAMD5SUM"] = self.JobMD5Sum
501        os.environ["TEACLIENTHOST"] = self.ClientHost or ""
502        os.environ["TEAJOBID"] = self.JobId
503        os.environ["TEAUSERNAME"] = self.UserName
504        os.environ["TEATITLE"] = self.Title
505        os.environ["TEACOPIES"] = str(self.Copies)
506        os.environ["TEAOPTIONS"] = self.Options
507        os.environ["TEAINPUTFILE"] = self.InputFile or ""
508        os.environ["TEABILLING"] = self.JobBilling or ""
509        os.environ["TEACONTROLFILE"] = self.ControlFile
510       
511    def saveDatasAndCheckSum(self) :
512        """Saves the input datas into a static file."""
513        self.logDebug("Duplicating data stream into %s" % self.DataFile)
514        mustclose = 0
515        if self.InputFile is not None :
516            infile = open(self.InputFile, "rb")
517            mustclose = 1
518        else :   
519            infile = sys.stdin
520        CHUNK = 64*1024         # read 64 Kb at a time
521        dummy = 0
522        sizeread = 0
523        checksum = md5.new()
524        outfile = open(self.DataFile, "wb")   
525        while 1 :
526            data = infile.read(CHUNK) 
527            if not data :
528                break
529            sizeread += len(data)   
530            outfile.write(data)
531            checksum.update(data)   
532            if not (dummy % 32) : # Only display every 2 Mb
533                self.logDebug("%s bytes saved..." % sizeread)
534            dummy += 1   
535        outfile.close()
536        if mustclose :   
537            infile.close()
538        self.JobSize = sizeread   
539        self.JobMD5Sum = checksum.hexdigest()
540        self.logDebug("Job %s is %s bytes long." % (self.JobId, self.JobSize))
541        self.logDebug("Job %s MD5 sum is %s" % (self.JobId, self.JobMD5Sum))
542
543    def cleanUp(self) :
544        """Cleans up the place."""
545        if not self.isTrue(self.getPrintQueueOption(self.PrinterName, "keepfiles", ignore=1)) :
546            os.remove(self.DataFile)
547           
548    def sigtermHandler(self, signum, frame) :
549        """Sets an attribute whenever SIGTERM is received."""
550        self.gotSigTerm = 1
551        self.logInfo("SIGTERM received for Job %s." % self.JobId)
552       
553    def runBranches(self) :         
554        """Launches each hook defined for the current print queue."""
555        self.isCancelled = 0    # did a prehook cancel the print job ?
556        self.gotSigTerm = 0
557        signal.signal(signal.SIGTERM, self.sigtermHandler)
558        serialize = self.isTrue(self.getPrintQueueOption(self.PrinterName, "serialize", ignore=1))
559        self.pipes = { 0: (0,1) }
560        branches = self.enumBranches(self.PrinterName, "prehook")
561        for b in branches :
562            self.pipes[b.split("_", 1)[1]] = os.pipe()
563        retcode = self.runCommands("prehook", branches, serialize)
564        for p in self.pipes.items()[1:] :
565            os.close(p[1][1])
566        if not self.isCancelled and not self.gotSigTerm :
567            if self.RealBackend :
568                retcode = self.runOriginalBackend()
569            if not self.gotSigTerm :
570                os.environ["TEASTATUS"] = str(retcode)
571                branches = self.enumBranches(self.PrinterName, "posthook")
572                if self.runCommands("posthook", branches, serialize) :
573                    self.logInfo("An error occured during the execution of posthooks.", "warn")
574        for p in self.pipes.items()[1:] :
575            os.close(p[1][0])
576        signal.signal(signal.SIGTERM, signal.SIG_IGN)
577        if not retcode :
578            self.logInfo("OK")
579        else :   
580            self.logInfo("An error occured, please check CUPS' error_log file.")
581        return retcode
582       
583    def stdioRedirSystem(self, cmd, stdin=0, stdout=1) :
584        """Launches a command with stdio redirected."""
585        # Code contributed by Peter Stuge on May 23rd and June 7th 2005
586        pid = os.fork()
587        if pid == 0 :
588            if stdin != 0 :
589                os.dup2(stdin, 0)
590                os.close(stdin)
591            if stdout != 1 :
592                os.dup2(stdout, 1)
593                os.close(stdout)
594            try :
595                os.execl("/bin/sh", "sh", "-c", cmd)
596            except OSError, msg :
597                self.logDebug("execl() failed: %s" % msg)
598            os._exit(-1)
599        status = os.waitpid(pid, 0)[1]
600        if os.WIFEXITED(status) :
601            return os.WEXITSTATUS(status)
602        return -1
603       
604    def runCommand(self, branch, command) :
605        """Runs a particular branch command."""
606        # Code contributed by Peter Stuge on June 7th 2005
607        self.logDebug("Launching %s : %s" % (branch, command))
608        btype, bname = branch.split("_", 1)
609        if bname not in self.pipes :
610            bname = 0
611        if btype == "prehook" :
612            return self.stdioRedirSystem(command, 0, self.pipes[bname][1])
613        else :
614            return self.stdioRedirSystem(command, self.pipes[bname][0])
615
616    def runCommands(self, btype, branches, serialize) :   
617        """Runs the commands for a particular branch type."""
618        exitcode = 0 
619        btype = btype.lower()
620        btypetitle = btype.title()
621        branchlist = branches.keys()   
622        branchlist.sort()
623        if serialize :
624            self.logDebug("Begin serialized %ss" % btypetitle)
625            for branch in branchlist :
626                if self.gotSigTerm :
627                    break
628                retcode = self.runCommand(branch, branches[branch])
629                self.logDebug("Exit code for %s %s on printer %s is %s" % (btype, branch, self.PrinterName, retcode))
630                if retcode :   
631                    if (btype == "prehook") and (retcode == 255) : # -1
632                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
633                        self.isCancelled = 1
634                    else :   
635                        self.logInfo("%s %s on printer %s didn't exit successfully." % (btypetitle, branch, self.PrinterName), "error")
636                        exitcode = 1
637            self.logDebug("End serialized %ss" % btypetitle)
638        else :       
639            self.logDebug("Begin forked %ss" % btypetitle)
640            pids = {}
641            for branch in branchlist :
642                if self.gotSigTerm :
643                    break
644                pid = os.fork()
645                if pid :
646                    pids[branch] = pid
647                else :   
648                    os._exit(self.runCommand(branch, branches[branch]))
649            for (branch, pid) in pids.items() :
650                retcode = os.waitpid(pid, 0)[1]
651                if os.WIFEXITED(retcode) :
652                    retcode = os.WEXITSTATUS(retcode)
653                else :
654                    retcode = -1
655                self.logDebug("Exit code for %s %s (PID %s) on printer %s is %s" % (btype, branch, pid, self.PrinterName, retcode))
656                if retcode :   
657                    if (btype == "prehook") and (retcode == 255) : # -1
658                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
659                        self.isCancelled = 1
660                    else :   
661                        self.logInfo("%s %s (PID %s) on printer %s didn't exit successfully." % (btypetitle, branch, pid, self.PrinterName), "error")
662                        exitcode = 1
663            self.logDebug("End forked %ss" % btypetitle)
664        return exitcode
665       
666    def runOriginalBackend(self) :   
667        """Launches the original backend."""
668        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
669        arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:]
670        self.logDebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in arguments])))
671
672        pid = os.fork()
673        if pid == 0 :
674            if self.InputFile is None :
675                f=open(self.dataFile, "rb")
676                os.dup2(f.fileno(), 0)
677                f.close()
678            try :
679                os.execve(originalbackend,arguments,os.environ)
680            except OSError, msg :
681                self.logDebug("execve() failed: %s" % msg)
682            os._exit(-1)
683        killed = 0
684        status = -1
685        while status == -1 :
686            try :
687                status = os.waitpid(pid, 0)[1]
688            except OSError, (err, msg) :
689                if err == 4 and self.gotSigTerm :
690                    os.kill(pid, signal.SIGTERM)
691                    killed = 1
692        if os.WIFEXITED(status) :
693            status = os.WEXITSTATUS(status)
694            if status :
695              self.logInfo("CUPS backend %s returned %d." % (originalbackend, status), "error")
696            return status
697        elif not killed :   
698            self.logInfo("CUPS backend %s died abnormally." % originalbackend, "error")
699            return -1
700        else :   
701            return 1
702       
703if __name__ == "__main__" :   
704    # This is a CUPS backend, we should act and die like a CUPS backend
705    wrapper = CupsBackend()
706    if len(sys.argv) == 1 :
707        print "\n".join(wrapper.discoverOtherBackends())
708        sys.exit(0)               
709    elif len(sys.argv) not in (6, 7) :   
710        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
711                              % sys.argv[0])
712        sys.exit(1)
713    else :   
714        try :
715            wrapper.readConfig()
716            wrapper.initBackend()
717            wrapper.saveDatasAndCheckSum()
718            wrapper.exportAttributes()
719            retcode = wrapper.runBranches()
720            wrapper.cleanUp()
721        except SystemExit, e :   
722            retcode = e.code
723        except :   
724            import traceback
725            lines = []
726            for line in traceback.format_exception(*sys.exc_info()) :
727                lines.extend([l for l in line.split("\n") if l])
728            msg = "ERROR: ".join(["%s (PID %s) : %s\n" % (wrapper.MyName, wrapper.pid, l) for l in (["ERROR: Tea4CUPS v%s" % version] + lines)])
729            sys.stderr.write(msg)
730            sys.stderr.flush()
731            retcode = 1
732        sys.exit(retcode)
Note: See TracBrowser for help on using the browser.