root / tea4cups / trunk / tea4cups @ 638

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

Backported PyKota's fix for unsupported IPP attributes

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