root / tea4cups / trunk / tea4cups @ 676

Revision 676, 38.6 kB (checked in by jerome, 18 years ago)

Added support for the 'retry' directive.
Fixed an IPP parsing problem.
Fixed the filename argument to the original backend : if a tea4cups filter was applied,
and the file was printed in raw mode, the filtered datas were not printed, but the raw
ones were.

  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Rev
RevLine 
[565]1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
[576]4# Tea4CUPS : Tee for CUPS
[565]5#
[674]6# (c) 2005, 2006 Jerome Alet <alet@librelogiciel.com>
[639]7# (c) 2005 Peter Stuge <stuge-tea4cups@cdy.org>
[565]8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation; either version 2 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU General Public License for more details.
[641]17#
[565]18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
[644]20# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
[565]21#
22# $Id$
23#
24#
25
26import sys
27import os
[676]28import time
[630]29import pwd
[568]30import errno
[577]31import md5
[565]32import cStringIO
33import shlex
[568]34import tempfile
[570]35import ConfigParser
[588]36import signal
[653]37from struct import pack, unpack
[565]38
[664]39__version__ = "3.11_unofficial"
[585]40
[570]41class TeeError(Exception):
[576]42    """Base exception for Tea4CUPS related stuff."""
[570]43    def __init__(self, message = ""):
44        self.message = message
45        Exception.__init__(self, message)
46    def __repr__(self):
47        return self.message
48    __str__ = __repr__
[641]49
50class ConfigError(TeeError) :
[570]51    """Configuration related exceptions."""
[641]52    pass
53
54class IPPError(TeeError) :
[581]55    """IPP related exceptions."""
[641]56    pass
57
[646]58class IPPRequest :
59    """A class for IPP requests.
60   
[635]61       Usage :
[646]62       
[635]63         fp = open("/var/spool/cups/c00001", "rb")
[646]64         message = IPPRequest(fp.read())
[635]65         fp.close()
[646]66         message.parse()
67         # print message.dump() # dumps an equivalent to the original IPP message
68         # print str(message)   # returns a string of text with the same content as below
69         print "IPP version : %s.%s" % message.version
70         print "IPP operation Id : 0x%04x" % message.operation_id
71         print "IPP request Id : 0x%08x" % message.request_id
72         for attrtype in message.attributes_types :
[635]73             attrdict = getattr(message, "%s_attributes" % attrtype)
74             if attrdict :
75                 print "%s attributes :" % attrtype.title()
76                 for key in attrdict.keys() :
77                     print "  %s : %s" % (key, attrdict[key])
[646]78         if message.data :           
79             print "IPP datas : ", repr(message.data)           
[635]80    """
[646]81    attributes_types = ("operation", "job", "printer", "unsupported", \
82                                     "subscription", "event_notification")
83    def __init__(self, data="", version=None, operation_id=None, \
84                                              request_id=None, debug=0) :
85        """Initializes an IPP Message object.
86       
[635]87           Parameters :
[646]88           
89             data : the complete IPP Message's content.
[635]90             debug : a boolean value to output debug info on stderr.
91        """
[631]92        self.debug = debug
[646]93        self._data = data
94        self.parsed = 0
95       
96        # Initializes message
97        if version is not None :
98            try :
99                self.version = [int(p) for p in version.split(".")]
100            except AttributeError :
101                if len(version) == 2 : # 2-tuple
102                    self.version = version
103                else :   
104                    try :
105                        self.version = [int(p) for p in str(float(version)).split(".")]
106                    except :
107                        self.version = (1, 1) # default version number
108        self.operation_id = operation_id
109        self.request_id = request_id
110        self.data = ""
111       
112        # Initialize attributes mappings
[638]113        for attrtype in self.attributes_types :
114            setattr(self, "%s_attributes" % attrtype, {})
[646]115           
116        # Initialize tags   
117        self.tags = [ None ] * 256 # by default all tags reserved
118       
[581]119        # Delimiter tags
[646]120        self.tags[0x01] = "operation-attributes-tag"
121        self.tags[0x02] = "job-attributes-tag"
122        self.tags[0x03] = "end-of-attributes-tag"
123        self.tags[0x04] = "printer-attributes-tag"
124        self.tags[0x05] = "unsupported-attributes-tag"
125        self.tags[0x06] = "subscription-attributes-tag"
[676]126        self.tags[0x07] = "event_notification-attributes-tag"
[646]127       
[581]128        # out of band values
129        self.tags[0x10] = "unsupported"
130        self.tags[0x11] = "reserved-for-future-default"
131        self.tags[0x12] = "unknown"
132        self.tags[0x13] = "no-value"
[646]133        self.tags[0x15] = "not-settable"
134        self.tags[0x16] = "delete-attribute"
135        self.tags[0x17] = "admin-define"
136 
[581]137        # integer values
138        self.tags[0x20] = "generic-integer"
139        self.tags[0x21] = "integer"
140        self.tags[0x22] = "boolean"
141        self.tags[0x23] = "enum"
[646]142       
[581]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"
[646]148        self.tags[0x34] = "begCollection" # TODO : find sample files for testing
[581]149        self.tags[0x35] = "textWithLanguage"
150        self.tags[0x36] = "nameWithLanguage"
[646]151        self.tags[0x37] = "endCollection"
152       
[581]153        # character strings
[646]154        self.tags[0x40] = "generic-character-string"
[581]155        self.tags[0x41] = "textWithoutLanguage"
156        self.tags[0x42] = "nameWithoutLanguage"
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"
[646]163        self.tags[0x4a] = "memberAttrName"
164       
165        # Reverse mapping to generate IPP messages
166        self.dictags = {}
167        for i in range(len(self.tags)) :
168            value = self.tags[i]
169            if value is not None :
170                self.dictags[value] = i
171       
[655]172    def logDebug(self, msg) :   
[633]173        """Prints a debug message."""
174        if self.debug :
175            sys.stderr.write("%s\n" % msg)
176            sys.stderr.flush()
[646]177           
178    def __str__(self) :       
179        """Returns the parsed IPP message in a readable form."""
180        if not self.parsed :
181            return ""
182        else :   
[653]183            mybuffer = []
184            mybuffer.append("IPP version : %s.%s" % self.version)
185            mybuffer.append("IPP operation Id : 0x%04x" % self.operation_id)
186            mybuffer.append("IPP request Id : 0x%08x" % self.request_id)
[646]187            for attrtype in self.attributes_types :
188                attrdict = getattr(self, "%s_attributes" % attrtype)
189                if attrdict :
[653]190                    mybuffer.append("%s attributes :" % attrtype.title())
[646]191                    for key in attrdict.keys() :
[653]192                        mybuffer.append("  %s : %s" % (key, attrdict[key]))
[646]193            if self.data :           
[653]194                mybuffer.append("IPP datas : %s" % repr(self.data))
195            return "\n".join(mybuffer)
[646]196       
197    def dump(self) :   
198        """Generates an IPP Message.
199       
200           Returns the message as a string of text.
201        """   
[653]202        mybuffer = []
[646]203        if None not in (self.version, self.operation_id, self.request_id) :
[653]204            mybuffer.append(chr(self.version[0]) + chr(self.version[1]))
205            mybuffer.append(pack(">H", self.operation_id))
206            mybuffer.append(pack(">I", self.request_id))
[646]207            for attrtype in self.attributes_types :
208                tagprinted = 0
209                for (attrname, value) in getattr(self, "%s_attributes" % attrtype).items() :
210                    if not tagprinted :
[653]211                        mybuffer.append(chr(self.dictags["%s-attributes-tag" % attrtype]))
[646]212                        tagprinted = 1
213                    if type(value) != type([]) :
214                        value = [ value ]
215                    for (vtype, val) in value :
[653]216                        mybuffer.append(chr(self.dictags[vtype]))
217                        mybuffer.append(pack(">H", len(attrname)))
218                        mybuffer.append(attrname)
[646]219                        if vtype in ("integer", "enum") :
[653]220                            mybuffer.append(pack(">H", 4))
221                            mybuffer.append(pack(">I", val))
[646]222                        elif vtype == "boolean" :
[653]223                            mybuffer.append(pack(">H", 1))
224                            mybuffer.append(chr(val))
[646]225                        else :   
[653]226                            mybuffer.append(pack(">H", len(val)))
227                            mybuffer.append(val)
228            mybuffer.append(chr(self.dictags["end-of-attributes-tag"]))
229        mybuffer.append(self.data)   
230        return "".join(mybuffer)
[646]231           
232    def parse(self) :
233        """Parses an IPP Request.
234       
235           NB : Only a subset of RFC2910 is implemented.
236        """
237        self._curname = None
238        self._curdict = None
239        self.version = (ord(self._data[0]), ord(self._data[1]))
240        self.operation_id = unpack(">H", self._data[2:4])[0]
241        self.request_id = unpack(">I", self._data[4:8])[0]
242        self.position = 8
243        endofattributes = self.dictags["end-of-attributes-tag"]
244        maxdelimiter = self.dictags["event-notification-attributes-tag"]
245        try :
246            tag = ord(self._data[self.position])
247            while tag != endofattributes :
248                self.position += 1
249                name = self.tags[tag]
250                if name is not None :
251                    func = getattr(self, name.replace("-", "_"), None)
252                    if func is not None :
253                        self.position += func()
254                        if ord(self._data[self.position]) > maxdelimiter :
255                            self.position -= 1
256                            continue
257                tag = ord(self._data[self.position])
258        except IndexError :
259            raise IPPError, "Unexpected end of IPP message."
260           
261        # Now transform all one-element lists into single values
262        for attrtype in self.attributes_types :
263            attrdict = getattr(self, "%s_attributes" % attrtype)
264            for (key, value) in attrdict.items() :
265                if len(value) == 1 :
266                    attrdict[key] = value[0]
267        self.data = self._data[self.position+1:]           
268        self.parsed = 1           
269       
270    def parseTag(self) :   
[581]271        """Extracts information from an IPP tag."""
272        pos = self.position
[646]273        tagtype = self.tags[ord(self._data[pos])]
[581]274        pos += 1
275        posend = pos2 = pos + 2
[646]276        namelength = unpack(">H", self._data[pos:pos2])[0]
[581]277        if not namelength :
[631]278            name = self._curname
[646]279        else :   
[581]280            posend += namelength
[646]281            self._curname = name = self._data[pos2:posend]
[581]282        pos2 = posend + 2
[646]283        valuelength = unpack(">H", self._data[posend:pos2])[0]
[581]284        posend = pos2 + valuelength
[646]285        value = self._data[pos2:posend]
[633]286        if tagtype in ("integer", "enum") :
[634]287            value = unpack(">I", value)[0]
[646]288        elif tagtype == "boolean" :   
289            value = ord(value)
[631]290        oldval = self._curdict.setdefault(name, [])
[633]291        oldval.append((tagtype, value))
[655]292        self.logDebug("%s(%s) : %s" % (name, tagtype, value))
[581]293        return posend - self.position
[646]294       
295    def operation_attributes_tag(self) : 
[581]296        """Indicates that the parser enters into an operation-attributes-tag group."""
[655]297        self.logDebug("Start of operation_attributes_tag")
[631]298        self._curdict = self.operation_attributes
[581]299        return self.parseTag()
[646]300       
301    def job_attributes_tag(self) : 
[635]302        """Indicates that the parser enters into a job-attributes-tag group."""
[655]303        self.logDebug("Start of job_attributes_tag")
[631]304        self._curdict = self.job_attributes
[581]305        return self.parseTag()
[646]306       
307    def printer_attributes_tag(self) : 
[635]308        """Indicates that the parser enters into a printer-attributes-tag group."""
[655]309        self.logDebug("Start of printer_attributes_tag")
[631]310        self._curdict = self.printer_attributes
[581]311        return self.parseTag()
[646]312       
313    def unsupported_attributes_tag(self) : 
[635]314        """Indicates that the parser enters into an unsupported-attributes-tag group."""
[655]315        self.logDebug("Start of unsupported_attributes_tag")
[635]316        self._curdict = self.unsupported_attributes
317        return self.parseTag()
[646]318       
319    def subscription_attributes_tag(self) : 
320        """Indicates that the parser enters into a subscription-attributes-tag group."""
[655]321        self.logDebug("Start of subscription_attributes_tag")
[646]322        self._curdict = self.subscription_attributes
323        return self.parseTag()
324       
325    def event_notification_attributes_tag(self) : 
326        """Indicates that the parser enters into an event-notification-attributes-tag group."""
[655]327        self.logDebug("Start of event_notification_attributes_tag")
[646]328        self._curdict = self.event_notification_attributes
329        return self.parseTag()
[641]330
331class FakeConfig :
[570]332    """Fakes a configuration file parser."""
333    def get(self, section, option, raw=0) :
[626]334        """Fakes the retrieval of an option."""
[570]335        raise ConfigError, "Invalid configuration file : no option %s in section [%s]" % (option, section)
[641]336
[568]337class CupsBackend :
338    """Base class for tools with no database access."""
339    def __init__(self) :
340        """Initializes the CUPS backend wrapper."""
[588]341        signal.signal(signal.SIGTERM, signal.SIG_IGN)
342        signal.signal(signal.SIGPIPE, signal.SIG_IGN)
[577]343        self.MyName = "Tea4CUPS"
344        self.myname = "tea4cups"
345        self.pid = os.getpid()
[641]346
347    def readConfig(self) :
[626]348        """Reads the configuration file."""
[641]349        confdir = os.environ.get("CUPS_SERVERROOT", ".")
[577]350        self.conffile = os.path.join(confdir, "%s.conf" % self.myname)
[570]351        if os.path.isfile(self.conffile) :
352            self.config = ConfigParser.ConfigParser()
353            self.config.read([self.conffile])
[573]354            self.debug = self.isTrue(self.getGlobalOption("debug", ignore=1))
[641]355        else :
[570]356            self.config = FakeConfig()
357            self.debug = 1      # no config, so force debug mode !
[641]358
359    def logInfo(self, message, level="info") :
[574]360        """Logs a message to CUPS' error_log file."""
[640]361        try :
[664]362            sys.stderr.write("%s: %s v%s (PID %i) : %s\n" % (level.upper(), self.MyName, __version__, os.getpid(), message))
[640]363            sys.stderr.flush()
364        except IOError :
365            pass
[641]366
367    def logDebug(self, message) :
[585]368        """Logs something to debug output if debug is enabled."""
369        if self.debug :
370            self.logInfo(message, level="debug")
[641]371
372    def isTrue(self, option) :
[570]373        """Returns 1 if option is set to true, else 0."""
374        if (option is not None) and (option.upper().strip() in ['Y', 'YES', '1', 'ON', 'T', 'TRUE']) :
375            return 1
[641]376        else :
[570]377            return 0
[641]378
379    def getGlobalOption(self, option, ignore=0) :
[570]380        """Returns an option from the global section, or raises a ConfigError if ignore is not set, else returns None."""
381        try :
382            return self.config.get("global", option, raw=1)
[641]383        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :
[577]384            if not ignore :
[570]385                raise ConfigError, "Option %s not found in section global of %s" % (option, self.conffile)
[641]386
387    def getPrintQueueOption(self, printqueuename, option, ignore=0) :
[570]388        """Returns an option from the printer section, or the global section, or raises a ConfigError."""
389        globaloption = self.getGlobalOption(option, ignore=1)
390        try :
[574]391            return self.config.get(printqueuename, option, raw=1)
[641]392        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :
[570]393            if globaloption is not None :
394                return globaloption
[577]395            elif not ignore :
[574]396                raise ConfigError, "Option %s not found in section [%s] of %s" % (option, printqueuename, self.conffile)
[641]397
[597]398    def enumBranches(self, printqueuename, branchtype="tee") :
399        """Returns the list of branchtypes branches for a particular section's."""
400        branchbasename = "%s_" % branchtype.lower()
[573]401        try :
[627]402            globalbranches = [ (k, self.config.get("global", k)) for k in self.config.options("global") if k.startswith(branchbasename) ]
[641]403        except ConfigParser.NoSectionError, msg :
[579]404            raise ConfigError, "Invalid configuration file : %s" % msg
[573]405        try :
[627]406            sectionbranches = [ (k, self.config.get(printqueuename, k)) for k in self.config.options(printqueuename) if k.startswith(branchbasename) ]
[641]407        except ConfigParser.NoSectionError, msg :
[583]408            self.logInfo("No section for print queue %s : %s" % (printqueuename, msg))
[573]409            sectionbranches = []
410        branches = {}
411        for (k, v) in globalbranches :
412            value = v.strip()
413            if value :
414                branches[k] = value
[641]415        for (k, v) in sectionbranches :
[573]416            value = v.strip()
417            if value :
418                branches[k] = value # overwrite any global option or set a new value
[641]419            else :
[573]420                del branches[k] # empty value disables a global option
421        return branches
[641]422
423    def discoverOtherBackends(self) :
[568]424        """Discovers the other CUPS backends.
[641]425
[568]426           Executes each existing backend in turn in device enumeration mode.
427           Returns the list of available backends.
428        """
[569]429        # Unfortunately this method can't output any debug information
430        # to stdout or stderr, else CUPS considers that the device is
431        # not available.
[568]432        available = []
[569]433        (directory, myname) = os.path.split(sys.argv[0])
[588]434        if not directory :
435            directory = "./"
[568]436        tmpdir = tempfile.gettempdir()
[569]437        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
[568]438        if os.path.exists(lockfilename) :
439            lockfile = open(lockfilename, "r")
440            pid = int(lockfile.read())
441            lockfile.close()
442            try :
443                # see if the pid contained in the lock file is still running
444                os.kill(pid, 0)
[641]445            except OSError, e :
[568]446                if e.errno != errno.EPERM :
447                    # process doesn't exist anymore
448                    os.remove(lockfilename)
[641]449
[568]450        if not os.path.exists(lockfilename) :
451            lockfile = open(lockfilename, "w")
[577]452            lockfile.write("%i" % self.pid)
[568]453            lockfile.close()
[569]454            allbackends = [ os.path.join(directory, b) \
[641]455                                for b in os.listdir(directory)
[569]456                                    if os.access(os.path.join(directory, b), os.X_OK) \
[641]457                                        and (b != myname)]
458            for backend in allbackends :
[568]459                answer = os.popen(backend, "r")
460                try :
461                    devices = [line.strip() for line in answer.readlines()]
[641]462                except :
[568]463                    devices = []
464                status = answer.close()
465                if status is None :
466                    for d in devices :
[641]467                        # each line is of the form :
[568]468                        # 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
469                        # so we have to decompose it carefully
470                        fdevice = cStringIO.StringIO(d)
471                        tokenizer = shlex.shlex(fdevice)
472                        tokenizer.wordchars = tokenizer.wordchars + \
473                                                        r".:,?!~/\_$*-+={}[]()#"
474                        arguments = []
475                        while 1 :
476                            token = tokenizer.get_token()
477                            if token :
478                                arguments.append(token)
479                            else :
480                                break
481                        fdevice.close()
482                        try :
483                            (devicetype, device, name, fullname) = arguments
[641]484                        except ValueError :
[568]485                            pass    # ignore this 'bizarre' device
[641]486                        else :
[568]487                            if name.startswith('"') and name.endswith('"') :
488                                name = name[1:-1]
489                            if fullname.startswith('"') and fullname.endswith('"') :
490                                fullname = fullname[1:-1]
[577]491                            available.append('%s %s:%s "%s+%s" "%s managed %s"' \
492                                                 % (devicetype, self.myname, device, self.MyName, name, self.MyName, fullname))
[568]493            os.remove(lockfilename)
[599]494        available.append('direct %s:// "%s+Nothing" "%s managed Virtual Printer"' \
495                             % (self.myname, self.MyName, self.MyName))
[568]496        return available
[641]497
498    def initBackend(self) :
[577]499        """Initializes the backend's attributes."""
[641]500        # check that the DEVICE_URI environment variable's value is
[577]501        # prefixed with self.myname otherwise don't touch it.
[641]502        # If this is the case, we have to remove the prefix from
503        # the environment before launching the real backend
[577]504        muststartwith = "%s:" % self.myname
505        device_uri = os.environ.get("DEVICE_URI", "")
506        if device_uri.startswith(muststartwith) :
507            fulldevice_uri = device_uri[:]
508            device_uri = fulldevice_uri[len(muststartwith):]
[583]509            for i in range(2) :
[641]510                if device_uri.startswith("/") :
[583]511                    device_uri = device_uri[1:]
[577]512        try :
[641]513            (backend, destination) = device_uri.split(":", 1)
514        except ValueError :
[583]515            if not device_uri :
[600]516                self.logDebug("Not attached to an existing print queue.")
[583]517                backend = ""
[641]518            else :
[583]519                raise TeeError, "Invalid DEVICE_URI : %s\n" % device_uri
[641]520
[577]521        self.JobId = sys.argv[1].strip()
[630]522        self.UserName = sys.argv[2].strip() or pwd.getpwuid(os.geteuid())[0] # use CUPS' user when printing test pages from CUPS' web interface
[577]523        self.Title = sys.argv[3].strip()
524        self.Copies = int(sys.argv[4].strip())
525        self.Options = sys.argv[5].strip()
526        if len(sys.argv) == 7 :
527            self.InputFile = sys.argv[6] # read job's datas from file
[641]528        else :
[577]529            self.InputFile = None        # read job's datas from stdin
[641]530
[577]531        self.RealBackend = backend
532        self.DeviceURI = device_uri
533        self.PrinterName = os.environ.get("PRINTER", "")
[676]534        self.Directory = self.getPrintQueueOption(self.PrinterName, "directory", ignore=1) or tempfile.gettempdir()
[581]535        self.DataFile = os.path.join(self.Directory, "%s-%s-%s-%s" % (self.myname, self.PrinterName, self.UserName, self.JobId))
[646]536        (ippfilename, ippmessage) = self.parseIPPRequestFile()
[615]537        self.ControlFile = ippfilename
[652]538        john = ippmessage.operation_attributes.get("job-originating-host-name", \
539               ippmessage.job_attributes.get("job-originating-host-name", \
540               (None, None)))
541        if type(john) == type([]) :                         
542            john = john[-1]
543        (chtype, self.ClientHost) = john                         
[631]544        (jbtype, self.JobBilling) = ippmessage.job_attributes.get("job-billing", (None, None))
[641]545
[583]546    def getCupsConfigDirectives(self, directives=[]) :
547        """Retrieves some CUPS directives from its configuration file.
[641]548
549           Returns a mapping with lowercased directives as keys and
[583]550           their setting as values.
551        """
[641]552        dirvalues = {}
[583]553        cupsroot = os.environ.get("CUPS_SERVERROOT", "/etc/cups")
554        cupsdconf = os.path.join(cupsroot, "cupsd.conf")
555        try :
556            conffile = open(cupsdconf, "r")
[641]557        except IOError :
[583]558            raise TeeError, "Unable to open %s" % cupsdconf
[641]559        else :
[583]560            for line in conffile.readlines() :
561                linecopy = line.strip().lower()
562                for di in [d.lower() for d in directives] :
563                    if linecopy.startswith("%s " % di) :
564                        try :
565                            val = line.split()[1]
[641]566                        except :
[583]567                            pass # ignore errors, we take the last value in any case.
[641]568                        else :
[583]569                            dirvalues[di] = val
[641]570            conffile.close()
571        return dirvalues
572
[646]573    def parseIPPRequestFile(self) :
[624]574        """Parses the IPP message file and returns a tuple (filename, parsedvalue)."""
[583]575        cupsdconf = self.getCupsConfigDirectives(["RequestRoot"])
[582]576        requestroot = cupsdconf.get("requestroot", "/var/spool/cups")
577        if (len(self.JobId) < 5) and self.JobId.isdigit() :
578            ippmessagefile = "c%05i" % int(self.JobId)
[641]579        else :
[582]580            ippmessagefile = "c%s" % self.JobId
581        ippmessagefile = os.path.join(requestroot, ippmessagefile)
582        ippmessage = {}
583        try :
584            ippdatafile = open(ippmessagefile)
[641]585        except :
[582]586            self.logInfo("Unable to open IPP message file %s" % ippmessagefile, "warn")
[641]587        else :
[582]588            self.logDebug("Parsing of IPP message file %s begins." % ippmessagefile)
589            try :
[646]590                ippmessage = IPPRequest(ippdatafile.read())
591                ippmessage.parse()
[641]592            except IPPError, msg :
[582]593                self.logInfo("Error while parsing %s : %s" % (ippmessagefile, msg), "warn")
[641]594            else :
[582]595                self.logDebug("Parsing of IPP message file %s ends." % ippmessagefile)
596            ippdatafile.close()
[615]597        return (ippmessagefile, ippmessage)
[641]598
599    def exportAttributes(self) :
[577]600        """Exports our backend's attributes to the environment."""
601        os.environ["DEVICE_URI"] = self.DeviceURI       # WARNING !
602        os.environ["TEAPRINTERNAME"] = self.PrinterName
603        os.environ["TEADIRECTORY"] = self.Directory
604        os.environ["TEADATAFILE"] = self.DataFile
[579]605        os.environ["TEAJOBSIZE"] = str(self.JobSize)
[577]606        os.environ["TEAMD5SUM"] = self.JobMD5Sum
[582]607        os.environ["TEACLIENTHOST"] = self.ClientHost or ""
[577]608        os.environ["TEAJOBID"] = self.JobId
609        os.environ["TEAUSERNAME"] = self.UserName
610        os.environ["TEATITLE"] = self.Title
611        os.environ["TEACOPIES"] = str(self.Copies)
612        os.environ["TEAOPTIONS"] = self.Options
613        os.environ["TEAINPUTFILE"] = self.InputFile or ""
[615]614        os.environ["TEABILLING"] = self.JobBilling or ""
615        os.environ["TEACONTROLFILE"] = self.ControlFile
[641]616
[577]617    def saveDatasAndCheckSum(self) :
618        """Saves the input datas into a static file."""
[583]619        self.logDebug("Duplicating data stream into %s" % self.DataFile)
[577]620        mustclose = 0
621        if self.InputFile is not None :
622            infile = open(self.InputFile, "rb")
623            mustclose = 1
[641]624        else :
[577]625            infile = sys.stdin
[659]626           
627        filtercommand = self.getPrintQueueOption(self.PrinterName, "filter", \
628                                                 ignore=1)
629        if filtercommand :                                                 
630            self.logDebug("Data stream will be filtered through [%s]" % filtercommand)
631            filteroutput = "%s.filteroutput" % self.DataFile
632            outf = open(filteroutput, "wb")
633            filterstatus = self.stdioRedirSystem(filtercommand, infile.fileno(), outf.fileno())
634            outf.close()
635            self.logDebug("Filter's output status : %s" % repr(filterstatus))
636            if mustclose :
637                infile.close()
638            infile = open(filteroutput, "rb")
639            mustclose = 1
640        else :   
641            self.logDebug("Data stream will be used as-is (no filter defined)")
642           
[577]643        CHUNK = 64*1024         # read 64 Kb at a time
644        dummy = 0
645        sizeread = 0
646        checksum = md5.new()
[641]647        outfile = open(self.DataFile, "wb")
[577]648        while 1 :
[641]649            data = infile.read(CHUNK)
[577]650            if not data :
651                break
[641]652            sizeread += len(data)
[577]653            outfile.write(data)
[641]654            checksum.update(data)
[577]655            if not (dummy % 32) : # Only display every 2 Mb
656                self.logDebug("%s bytes saved..." % sizeread)
[641]657            dummy += 1
[577]658        outfile.close()
[659]659       
660        if filtercommand :
661            self.logDebug("Removing filter's output file %s" % filteroutput)
662            try :
663                os.remove(filteroutput)
664            except :   
665                pass
666               
[641]667        if mustclose :
[579]668            infile.close()
[659]669           
670        self.logDebug("%s bytes saved..." % sizeread)
[641]671        self.JobSize = sizeread
[577]672        self.JobMD5Sum = checksum.hexdigest()
673        self.logDebug("Job %s is %s bytes long." % (self.JobId, self.JobSize))
674        self.logDebug("Job %s MD5 sum is %s" % (self.JobId, self.JobMD5Sum))
[568]675
[577]676    def cleanUp(self) :
677        """Cleans up the place."""
[676]678        if (not self.isTrue(self.getPrintQueueOption(self.PrinterName, "keepfiles", ignore=1))) \
679            and os.path.exists(self.DataFile) :
680            try :
681                os.remove(self.DataFile)
682            except OSError, msg :   
683                self.logInfo("Problem when removing %s : %s" % (self.DataFile, msg), "error")
[641]684
[588]685    def sigtermHandler(self, signum, frame) :
686        """Sets an attribute whenever SIGTERM is received."""
687        self.gotSigTerm = 1
688        self.logInfo("SIGTERM received for Job %s." % self.JobId)
[641]689
690    def runBranches(self) :
[640]691        """Launches each hook defined for the current print queue."""
[604]692        self.isCancelled = 0    # did a prehook cancel the print job ?
693        self.gotSigTerm = 0
[588]694        signal.signal(signal.SIGTERM, self.sigtermHandler)
[598]695        serialize = self.isTrue(self.getPrintQueueOption(self.PrinterName, "serialize", ignore=1))
[643]696        self.pipes = { 0: (0, 1) }
[640]697        branches = self.enumBranches(self.PrinterName, "prehook")
[662]698        for b in branches.keys() :
[640]699            self.pipes[b.split("_", 1)[1]] = os.pipe()
700        retcode = self.runCommands("prehook", branches, serialize)
[643]701        for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] :
[640]702            os.close(p[1][1])
703        if not self.isCancelled and not self.gotSigTerm :
704            if self.RealBackend :
[676]705                retcode = self.launchOriginalBackend()
[659]706                if retcode :
707                    onfail = self.getPrintQueueOption(self.PrinterName, \
708                                                      "onfail", ignore=1)
709                    if onfail :
710                        self.logDebug("Launching onfail script %s" % onfail)
711                        os.system(onfail)
[640]712            if not self.gotSigTerm :
713                os.environ["TEASTATUS"] = str(retcode)
714                branches = self.enumBranches(self.PrinterName, "posthook")
715                if self.runCommands("posthook", branches, serialize) :
[625]716                    self.logInfo("An error occured during the execution of posthooks.", "warn")
[643]717        for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] :
[640]718            os.close(p[1][0])
[598]719        signal.signal(signal.SIGTERM, signal.SIG_IGN)
[640]720        if not retcode :
[600]721            self.logInfo("OK")
[641]722        else :
[600]723            self.logInfo("An error occured, please check CUPS' error_log file.")
[640]724        return retcode
[641]725
[639]726    def stdioRedirSystem(self, cmd, stdin=0, stdout=1) :
727        """Launches a command with stdio redirected."""
728        # Code contributed by Peter Stuge on May 23rd and June 7th 2005
[636]729        pid = os.fork()
730        if pid == 0 :
[639]731            if stdin != 0 :
732                os.dup2(stdin, 0)
733                os.close(stdin)
734            if stdout != 1 :
735                os.dup2(stdout, 1)
736                os.close(stdout)
737            try :
738                os.execl("/bin/sh", "sh", "-c", cmd)
[640]739            except OSError, msg :
[639]740                self.logDebug("execl() failed: %s" % msg)
[640]741            os._exit(-1)
742        status = os.waitpid(pid, 0)[1]
743        if os.WIFEXITED(status) :
744            return os.WEXITSTATUS(status)
745        return -1
[641]746
[639]747    def runCommand(self, branch, command) :
748        """Runs a particular branch command."""
749        # Code contributed by Peter Stuge on June 7th 2005
750        self.logDebug("Launching %s : %s" % (branch, command))
751        btype, bname = branch.split("_", 1)
[643]752        if bname not in self.pipes.keys() :
[640]753            bname = 0
754        if btype == "prehook" :
[639]755            return self.stdioRedirSystem(command, 0, self.pipes[bname][1])
756        else :
757            return self.stdioRedirSystem(command, self.pipes[bname][0])
758
[641]759    def runCommands(self, btype, branches, serialize) :
[598]760        """Runs the commands for a particular branch type."""
[641]761        exitcode = 0
[598]762        btype = btype.lower()
763        btypetitle = btype.title()
[641]764        branchlist = branches.keys()
[598]765        branchlist.sort()
766        if serialize :
[600]767            self.logDebug("Begin serialized %ss" % btypetitle)
[598]768            for branch in branchlist :
[588]769                if self.gotSigTerm :
770                    break
[640]771                retcode = self.runCommand(branch, branches[branch])
[598]772                self.logDebug("Exit code for %s %s on printer %s is %s" % (btype, branch, self.PrinterName, retcode))
[641]773                if retcode :
[601]774                    if (btype == "prehook") and (retcode == 255) : # -1
775                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
[602]776                        self.isCancelled = 1
[641]777                    else :
[601]778                        self.logInfo("%s %s on printer %s didn't exit successfully." % (btypetitle, branch, self.PrinterName), "error")
779                        exitcode = 1
[600]780            self.logDebug("End serialized %ss" % btypetitle)
[641]781        else :
[600]782            self.logDebug("Begin forked %ss" % btypetitle)
[584]783            pids = {}
[598]784            for branch in branchlist :
[588]785                if self.gotSigTerm :
786                    break
[584]787                pid = os.fork()
788                if pid :
789                    pids[branch] = pid
[641]790                else :
[640]791                    os._exit(self.runCommand(branch, branches[branch]))
[584]792            for (branch, pid) in pids.items() :
[640]793                retcode = os.waitpid(pid, 0)[1]
[584]794                if os.WIFEXITED(retcode) :
795                    retcode = os.WEXITSTATUS(retcode)
[640]796                else :
797                    retcode = -1
798                self.logDebug("Exit code for %s %s (PID %s) on printer %s is %s" % (btype, branch, pid, self.PrinterName, retcode))
[641]799                if retcode :
[601]800                    if (btype == "prehook") and (retcode == 255) : # -1
801                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
[602]802                        self.isCancelled = 1
[641]803                    else :
[640]804                        self.logInfo("%s %s (PID %s) on printer %s didn't exit successfully." % (btypetitle, branch, pid, self.PrinterName), "error")
[601]805                        exitcode = 1
[600]806            self.logDebug("End forked %ss" % btypetitle)
[584]807        return exitcode
[641]808
[676]809    def launchOriginalBackend(self) :
810        """Launches the original backend, optionally retrying if needed."""
811        number = 1
812        delay = 0
813        retry = self.getPrintQueueOption(self.PrinterName, "retry", ignore=1)
814        if retry is not None :
815            try :
816                (number, delay) = [int(p) for p in retry.strip().split(",")]
817            except (ValueError, AttributeError, TypeError) :   
818                self.logInfo("Invalid value '%s' for the 'retry' directive for printer %s in %s." % (retry, self.PrinterName, self.conffile), "error")
819                number = 1
820                delay = 0
821               
822        loopcount = 1 
823        while 1 :           
824            retcode = self.runOriginalBackend()
825            if not retcode :
826                break
827            else :
828                if (not number) or (loopcount < number) :
829                    self.logInfo(_("The real backend produced an error, we will try again in %s seconds.") % delay, "warn")
830                    time.sleep(delay)
831                    loopcount += 1
832                else :   
833                    break
834        return retcode           
835       
[641]836    def runOriginalBackend(self) :
[587]837        """Launches the original backend."""
838        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
[640]839        arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:]
840        self.logDebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in arguments])))
841
842        pid = os.fork()
843        if pid == 0 :
844            if self.InputFile is None :
[653]845                f = open(self.DataFile, "rb")
[640]846                os.dup2(f.fileno(), 0)
847                f.close()
[676]848            else :   
849                arguments[6] = self.DataFile # in case a tea4cups filter was applied
[640]850            try :
[643]851                os.execve(originalbackend, arguments, os.environ)
[640]852            except OSError, msg :
853                self.logDebug("execve() failed: %s" % msg)
854            os._exit(-1)
[588]855        killed = 0
856        status = -1
[640]857        while status == -1 :
[588]858            try :
[640]859                status = os.waitpid(pid, 0)[1]
860            except OSError, (err, msg) :
[648]861                if (err == 4) and self.gotSigTerm :
[640]862                    os.kill(pid, signal.SIGTERM)
863                    killed = 1
[587]864        if os.WIFEXITED(status) :
[640]865            status = os.WEXITSTATUS(status)
866            if status :
[676]867                self.logInfo("CUPS backend %s returned %d." % (originalbackend,\
[643]868                                                             status), "error")
[640]869            return status
[641]870        elif not killed :
[643]871            self.logInfo("CUPS backend %s died abnormally." % originalbackend,\
872                                                              "error")
[588]873            return -1
[641]874        else :
[587]875            return 1
[641]876
877if __name__ == "__main__" :
[565]878    # This is a CUPS backend, we should act and die like a CUPS backend
[568]879    wrapper = CupsBackend()
[565]880    if len(sys.argv) == 1 :
[568]881        print "\n".join(wrapper.discoverOtherBackends())
[641]882        sys.exit(0)
883    elif len(sys.argv) not in (6, 7) :
[568]884        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
885                              % sys.argv[0])
886        sys.exit(1)
[641]887    else :
[676]888        retcode = 1
[585]889        try :
[676]890            try :
891                wrapper.readConfig()
892                wrapper.initBackend()
893                wrapper.saveDatasAndCheckSum()
894                wrapper.exportAttributes()
895                retcode = wrapper.runBranches()
896            except SystemExit, e :
897                retcode = e.code
898            except :
899                import traceback
900                lines = []
901                for line in traceback.format_exception(*sys.exc_info()) :
902                    lines.extend([l for l in line.split("\n") if l])
903                msg = "ERROR: ".join(["%s (PID %s) : %s\n" % (wrapper.MyName, \
904                                                              wrapper.pid, l) \
905                            for l in (["ERROR: Tea4CUPS v%s" % __version__] + lines)])
906                sys.stderr.write(msg)
907                sys.stderr.flush()
908                retcode = 1
909        finally :       
[585]910            wrapper.cleanUp()
[579]911        sys.exit(retcode)
Note: See TracBrowser for help on using the browser.