root / tea4cups / trunk / tea4cups @ 652

Revision 652, 35.5 kB (checked in by jerome, 19 years ago)

Backported the IPP fix from PyKota

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