root / tea4cups / trunk / tea4cups @ 636

Revision 636, 43.5 kB (checked in by jerome, 19 years ago)

Integrated Peter Stuge's patch to allow the creation of pipes between pre and post hooks

  • 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.12alpha2_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[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 ("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        self.pipe = os.pipe()
585        for branchtype in ["prehook", "tee", "posthook"] :
586            if branchtype == "posthook" :
587                os.close(self.pipe[1])
588            branches = self.enumBranches(self.PrinterName, branchtype)
589            status = self.runCommands(branchtype, branches, serialize)
590            if status :
591                if branchtype != "posthook" :
592                    exitcode = status
593                else :   
594                    # we just ignore error in posthooks
595                    self.logInfo("An error occured during the execution of posthooks.", "warn")
596            if (branchtype == "prehook") and self.isCancelled :
597                break # We don't want to execute tees or posthooks in this case
598        signal.signal(signal.SIGTERM, signal.SIG_IGN)
599        try :
600            os.close(self.pipe[1])
601        except OSError :
602            pass
603        os.close(self.pipe[0])
604        if not exitcode :
605            self.logInfo("OK")
606        else :   
607            self.logInfo("An error occured, please check CUPS' error_log file.")
608        return exitcode
609       
610    def pipedSystem(self, cmd, stdin=0, stdout=1) :
611        """Launches a command making a pipe available to it."""
612        # Code contributed by Peter Stuge on May 23rd 2005
613        pid = os.fork()
614        if pid == 0 :
615            os.dup2(stdin, 0)
616            os.dup2(stdout, 1)
617            os.execl("/bin/sh", "sh", "-c", cmd)
618            os.exit(1)
619        return os.waitpid(pid, 0)[1]
620       
621    def runCommands(self, btype, branches, serialize) :   
622        """Runs the commands for a particular branch type."""
623        exitcode = 0 
624        btype = btype.lower()
625        btypetitle = btype.title()
626        branchlist = branches.keys()   
627        branchlist.sort()
628        if serialize :
629            self.logDebug("Begin serialized %ss" % btypetitle)
630            if (btype == "tee") and self.RealBackend :
631                self.logDebug("Launching original backend %s for printer %s" % (self.RealBackend, self.PrinterName))
632                retcode = self.runOriginalBackend()
633                if os.WIFEXITED(retcode) :
634                    retcode = os.WEXITSTATUS(retcode)
635                os.environ["TEASTATUS"] = str(retcode)
636                exitcode = retcode
637            for branch in branchlist :
638                command = branches[branch]
639                if self.gotSigTerm :
640                    break
641                self.logDebug("Launching %s : %s" % (branch, command))
642                if btype != "posthook" :
643                    retcode = self.pipedSystem(command, 0, self.pipe[1])
644                else :
645                    retcode = self.pipedSystem(command, self.pipe[0])
646                self.logDebug("Exit code for %s %s on printer %s is %s" % (btype, branch, self.PrinterName, retcode))
647                if os.WIFEXITED(retcode) :
648                    retcode = os.WEXITSTATUS(retcode)
649                if retcode :   
650                    if (btype == "prehook") and (retcode == 255) : # -1
651                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
652                        self.isCancelled = 1
653                    else :   
654                        self.logInfo("%s %s on printer %s didn't exit successfully." % (btypetitle, branch, self.PrinterName), "error")
655                        exitcode = 1
656            self.logDebug("End serialized %ss" % btypetitle)
657        else :       
658            self.logDebug("Begin forked %ss" % btypetitle)
659            pids = {}
660            if (btype == "tee") and self.RealBackend :
661                branches["Original backend"] = None     # Fakes a tee to launch one more child
662                branchlist = ["Original backend"] + branchlist
663            for branch in branchlist :
664                command = branches[branch]
665                if self.gotSigTerm :
666                    break
667                pid = os.fork()
668                if pid :
669                    pids[branch] = pid
670                else :   
671                    if branch == "Original backend" :
672                        self.logDebug("Launching original backend %s for printer %s" % (self.RealBackend, self.PrinterName))
673                        sys.exit(self.runOriginalBackend())
674                    else :
675                        self.logDebug("Launching %s : %s" % (branch, command))
676                        retcode = os.system(command)
677                        if os.WIFEXITED(retcode) :
678                            retcode = os.WEXITSTATUS(retcode)
679                        else :   
680                            retcode = -1
681                        sys.exit(retcode)
682            for (branch, pid) in pids.items() :
683                (childpid, retcode) = os.waitpid(pid, 0)
684                self.logDebug("Exit code for %s %s (PID %s) on printer %s is %s" % (btype, branch, childpid, self.PrinterName, retcode))
685                if os.WIFEXITED(retcode) :
686                    retcode = os.WEXITSTATUS(retcode)
687                if retcode :   
688                    if (btype == "prehook") and (retcode == 255) : # -1
689                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
690                        self.isCancelled = 1
691                    else :   
692                        self.logInfo("%s %s (PID %s) on printer %s didn't exit successfully." % (btypetitle, branch, childpid, self.PrinterName), "error")
693                        exitcode = 1
694                if branch == "Original backend" :   
695                    os.environ["TEASTATUS"] = str(retcode)
696            self.logDebug("End forked %ss" % btypetitle)
697        return exitcode
698       
699    def unregisterFileNo(self, pollobj, fileno) :               
700        """Removes a file handle from the polling object."""
701        try :
702            pollobj.unregister(fileno)
703        except KeyError :   
704            self.logInfo("File number %s unregistered twice from polling object, ignored." % fileno, "warn")
705        except :   
706            self.logDebug("Error while unregistering file number %s from polling object." % fileno)
707        else :   
708            self.logDebug("File number %s unregistered from polling object." % fileno)
709           
710    def formatFileEvent(self, fd, mask) :       
711        """Formats file debug info."""
712        maskval = []
713        if mask & select.POLLIN :
714            maskval.append("POLLIN")
715        if mask & select.POLLOUT :
716            maskval.append("POLLOUT")
717        if mask & select.POLLPRI :
718            maskval.append("POLLPRI")
719        if mask & select.POLLERR :
720            maskval.append("POLLERR")
721        if mask & select.POLLHUP :
722            maskval.append("POLLHUP")
723        if mask & select.POLLNVAL :
724            maskval.append("POLLNVAL")
725        return "%s (%s)" % (fd, " | ".join(maskval))
726       
727    def runOriginalBackend(self) :   
728        """Launches the original backend."""
729        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
730        arguments = sys.argv
731        self.logDebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in ([os.environ["DEVICE_URI"]] + arguments[1:])])))
732        subprocess = Popen4ForCUPS([originalbackend] + arguments[1:], bufsize=0, arg0=os.environ["DEVICE_URI"])
733       
734        # Save file descriptors, we will need them later.
735        stderrfno = sys.stderr.fileno()
736        fromcfno = subprocess.fromchild.fileno()
737        tocfno = subprocess.tochild.fileno()
738       
739        # We will have to be careful when dealing with I/O
740        # So we use a poll object to know when to read or write
741        pollster = select.poll()
742        pollster.register(fromcfno, select.POLLIN | select.POLLPRI)
743        pollster.register(stderrfno, select.POLLOUT)
744        pollster.register(tocfno, select.POLLOUT)
745       
746        # Initialize our buffers
747        indata = ""
748        outdata = ""
749        endinput = endoutput = 0
750        inputclosed = outputclosed = 0
751        totaltochild = totalfromcups = 0
752        totalfromchild = totaltocups = 0
753       
754        if self.InputFile is None :
755           # this is not a real file, we read the job's data
756            # from our temporary file which is a copy of stdin
757            inf = open(self.DataFile, "rb")
758            infno = inf.fileno()
759            pollster.register(infno, select.POLLIN | select.POLLPRI)
760        else :   
761            # job's data is in a file, no need to pass the data
762            # to the original backend
763            self.logDebug("Job's data is in %s" % self.InputFile)
764            infno = None
765            endinput = 1
766       
767        self.logDebug("Entering streams polling loop...")
768        MEGABYTE = 1024*1024
769        killed = 0
770        status = -1
771        while (status == -1) and (not killed) and not (inputclosed and outputclosed) :
772            # First check if original backend is still alive
773            status = subprocess.poll()
774           
775            # Now if we got SIGTERM, we have
776            # to kill -TERM the original backend
777            if self.gotSigTerm and not killed :
778                try :
779                    os.kill(subprocess.pid, signal.SIGTERM)
780                except OSError, msg : # ignore but logs if process was already killed.
781                    self.logDebug("Error while sending signal to pid %s : %s" % (subprocess.pid, msg))
782                else :   
783                    self.logInfo(_("SIGTERM was sent to original backend %s (PID %s)") % (originalbackend, subprocess.pid))
784                    killed = 1
785           
786            # In any case, deal with any remaining I/O
787            try :
788                availablefds = pollster.poll(5000)
789            except select.error, msg :   
790                self.logDebug("Interrupted poll : %s" % msg)
791                availablefds = []
792            if not availablefds :
793                self.logDebug("Nothing to do, sleeping a bit...")
794                time.sleep(0.01) # give some time to the system
795            else :
796                for (fd, mask) in availablefds :
797                    try :
798                        if mask & select.POLLOUT :
799                            # We can write
800                            if fd == tocfno :
801                                if indata :
802                                    try :
803                                        nbwritten = os.write(fd, indata)   
804                                    except (OSError, IOError), msg :   
805                                        self.logDebug("Error while writing to original backend's stdin %s : %s" % (fd, msg))
806                                    else :   
807                                        if len(indata) != nbwritten :
808                                            self.logDebug("Short write to original backend's input !")
809                                        totaltochild += nbwritten   
810                                        self.logDebug("%s bytes sent to original backend so far..." % totaltochild)
811                                        indata = indata[nbwritten:]
812                                else :       
813                                    self.logDebug("No data to send to original backend yet, sleeping a bit...")
814                                    time.sleep(0.01)
815                                   
816                                if endinput :   
817                                    self.unregisterFileNo(pollster, tocfno)       
818                                    self.logDebug("Closing original backend's stdin.")
819                                    os.close(tocfno)
820                                    inputclosed = 1
821                            elif fd == stderrfno :
822                                if outdata :
823                                    try :
824                                        nbwritten = os.write(fd, outdata)
825                                    except (OSError, IOError), msg :   
826                                        self.logDebug("Error while writing to CUPS back channel (stderr) %s : %s" % (fd, msg))
827                                    else :
828                                        if len(outdata) != nbwritten :
829                                            self.logDebug("Short write to stderr (CUPS) !")
830                                        totaltocups += nbwritten   
831                                        self.logDebug("%s bytes sent back to CUPS so far..." % totaltocups)
832                                        outdata = outdata[nbwritten:]
833                                else :       
834                                    # self.logDebug("No data to send back to CUPS yet, sleeping a bit...") # Uncommenting this fills your logs
835                                    time.sleep(0.01) # Give some time to the system, stderr is ALWAYS writeable it seems.
836                                   
837                                if endoutput :   
838                                    self.unregisterFileNo(pollster, stderrfno)       
839                                    outputclosed = 1
840                            else :   
841                                self.logDebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
842                                time.sleep(0.01)
843                               
844                        if mask & (select.POLLIN | select.POLLPRI) :     
845                            # We have something to read
846                            try :
847                                data = os.read(fd, MEGABYTE)
848                            except (IOError, OSError), msg :   
849                                self.logDebug("Error while reading file %s : %s" % (fd, msg))
850                            else :
851                                if fd == infno :
852                                    if not data :    # If yes, then no more input data
853                                        self.unregisterFileNo(pollster, infno)
854                                        self.logDebug("Input data ends.")
855                                        endinput = 1 # this happens with real files.
856                                    else :   
857                                        indata += data
858                                        totalfromcups += len(data)
859                                        self.logDebug("%s bytes read from CUPS so far..." % totalfromcups)
860                                elif fd == fromcfno :
861                                    if not data :
862                                        self.logDebug("No back channel data to read from original backend yet, sleeping a bit...")
863                                        time.sleep(0.01)
864                                    else :
865                                        outdata += data
866                                        totalfromchild += len(data)
867                                        self.logDebug("%s bytes read from original backend so far..." % totalfromchild)
868                                else :   
869                                    self.logDebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
870                                    time.sleep(0.01)
871                                   
872                        if mask & (select.POLLHUP | select.POLLERR) :
873                            # Treat POLLERR as an EOF.
874                            # Some standard I/O stream has no more datas
875                            self.unregisterFileNo(pollster, fd)
876                            if fd == infno :
877                                # Here we are in the case where the input file is stdin.
878                                # which has no more data to be read.
879                                self.logDebug("Input data ends.")
880                                endinput = 1
881                            elif fd == fromcfno :   
882                                # We are no more interested in this file descriptor       
883                                self.logDebug("Closing original backend's stdout+stderr.")
884                                os.close(fromcfno)
885                                endoutput = 1
886                            else :   
887                                self.logDebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask))
888                                time.sleep(0.01)
889                               
890                        if mask & select.POLLNVAL :       
891                            self.logDebug("File %s was closed. Unregistering from polling object." % fd)
892                            self.unregisterFileNo(pollster, fd)
893                    except IOError, msg :           
894                        self.logDebug("Got an IOError : %s" % msg) # we got signalled during an I/O
895               
896        # We must close the original backend's input stream
897        if killed and not inputclosed :
898            self.logDebug("Forcing close of original backend's stdin.")
899            os.close(tocfno)
900       
901        self.logDebug("Exiting streams polling loop...")
902       
903        self.logDebug("input data's final length : %s" % len(indata))
904        self.logDebug("back-channel data's final length : %s" % len(outdata))
905       
906        self.logDebug("Total bytes read from CUPS (job's datas) : %s" % totalfromcups)
907        self.logDebug("Total bytes sent to original backend (job's datas) : %s" % totaltochild)
908       
909        self.logDebug("Total bytes read from original backend (back-channel datas) : %s" % totalfromchild)
910        self.logDebug("Total bytes sent back to CUPS (back-channel datas) : %s" % totaltocups)
911       
912        # Check exit code of original CUPS backend.   
913        if status == -1 :
914            # we exited the loop before the original backend exited
915            # now we have to wait for it to finish and get its status
916            self.logDebug("Waiting for original backend to exit...")
917            try :
918                status = subprocess.wait()
919            except OSError : # already dead : TODO : detect when abnormal
920                status = 0
921        if os.WIFEXITED(status) :
922            return os.WEXITSTATUS(status)
923        elif not killed :   
924            self.logInfo("CUPS backend %s died abnormally." % originalbackend, "error")
925            return -1
926        else :   
927            return 1
928       
929if __name__ == "__main__" :   
930    # This is a CUPS backend, we should act and die like a CUPS backend
931    wrapper = CupsBackend()
932    if len(sys.argv) == 1 :
933        print "\n".join(wrapper.discoverOtherBackends())
934        sys.exit(0)               
935    elif len(sys.argv) not in (6, 7) :   
936        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
937                              % sys.argv[0])
938        sys.exit(1)
939    else :   
940        try :
941            wrapper.readConfig()
942            wrapper.initBackend()
943            wrapper.saveDatasAndCheckSum()
944            wrapper.exportAttributes()
945            retcode = wrapper.runBranches()
946            wrapper.cleanUp()
947        except SystemExit, e :   
948            retcode = e.code
949        except :   
950            import traceback
951            lines = []
952            for line in traceback.format_exception(*sys.exc_info()) :
953                lines.extend([l for l in line.split("\n") if l])
954            msg = "ERROR: ".join(["%s (PID %s) : %s\n" % (wrapper.MyName, wrapper.pid, l) for l in (["ERROR: Tea4CUPS v%s" % version] + lines)])
955            sys.stderr.write(msg)
956            sys.stderr.flush()
957            retcode = 1
958        sys.exit(retcode)
Note: See TracBrowser for help on using the browser.