root / tea4cups / trunk / tea4cups @ 648

Revision 648, 35.3 kB (checked in by jerome, 19 years ago)

3.00 is now out !
Official packages available later in the night...

  • 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.00_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        (chtype, self.ClientHost) = ippmessage.operation_attributes.get("job-originating-host-name", \
541                                          ippmessage.job_attributes.get("job-originating-host-name", (None, None)))
542        (jbtype, self.JobBilling) = ippmessage.job_attributes.get("job-billing", (None, None))
543
544    def getCupsConfigDirectives(self, directives=[]) :
545        """Retrieves some CUPS directives from its configuration file.
546
547           Returns a mapping with lowercased directives as keys and
548           their setting as values.
549        """
550        dirvalues = {}
551        cupsroot = os.environ.get("CUPS_SERVERROOT", "/etc/cups")
552        cupsdconf = os.path.join(cupsroot, "cupsd.conf")
553        try :
554            conffile = open(cupsdconf, "r")
555        except IOError :
556            raise TeeError, "Unable to open %s" % cupsdconf
557        else :
558            for line in conffile.readlines() :
559                linecopy = line.strip().lower()
560                for di in [d.lower() for d in directives] :
561                    if linecopy.startswith("%s " % di) :
562                        try :
563                            val = line.split()[1]
564                        except :
565                            pass # ignore errors, we take the last value in any case.
566                        else :
567                            dirvalues[di] = val
568            conffile.close()
569        return dirvalues
570
571    def parseIPPRequestFile(self) :
572        """Parses the IPP message file and returns a tuple (filename, parsedvalue)."""
573        cupsdconf = self.getCupsConfigDirectives(["RequestRoot"])
574        requestroot = cupsdconf.get("requestroot", "/var/spool/cups")
575        if (len(self.JobId) < 5) and self.JobId.isdigit() :
576            ippmessagefile = "c%05i" % int(self.JobId)
577        else :
578            ippmessagefile = "c%s" % self.JobId
579        ippmessagefile = os.path.join(requestroot, ippmessagefile)
580        ippmessage = {}
581        try :
582            ippdatafile = open(ippmessagefile)
583        except :
584            self.logInfo("Unable to open IPP message file %s" % ippmessagefile, "warn")
585        else :
586            self.logDebug("Parsing of IPP message file %s begins." % ippmessagefile)
587            try :
588                ippmessage = IPPRequest(ippdatafile.read())
589                ippmessage.parse()
590            except IPPError, msg :
591                self.logInfo("Error while parsing %s : %s" % (ippmessagefile, msg), "warn")
592            else :
593                self.logDebug("Parsing of IPP message file %s ends." % ippmessagefile)
594            ippdatafile.close()
595        return (ippmessagefile, ippmessage)
596
597    def exportAttributes(self) :
598        """Exports our backend's attributes to the environment."""
599        os.environ["DEVICE_URI"] = self.DeviceURI       # WARNING !
600        os.environ["TEAPRINTERNAME"] = self.PrinterName
601        os.environ["TEADIRECTORY"] = self.Directory
602        os.environ["TEADATAFILE"] = self.DataFile
603        os.environ["TEAJOBSIZE"] = str(self.JobSize)
604        os.environ["TEAMD5SUM"] = self.JobMD5Sum
605        os.environ["TEACLIENTHOST"] = self.ClientHost or ""
606        os.environ["TEAJOBID"] = self.JobId
607        os.environ["TEAUSERNAME"] = self.UserName
608        os.environ["TEATITLE"] = self.Title
609        os.environ["TEACOPIES"] = str(self.Copies)
610        os.environ["TEAOPTIONS"] = self.Options
611        os.environ["TEAINPUTFILE"] = self.InputFile or ""
612        os.environ["TEABILLING"] = self.JobBilling or ""
613        os.environ["TEACONTROLFILE"] = self.ControlFile
614
615    def saveDatasAndCheckSum(self) :
616        """Saves the input datas into a static file."""
617        self.logDebug("Duplicating data stream into %s" % self.DataFile)
618        mustclose = 0
619        if self.InputFile is not None :
620            infile = open(self.InputFile, "rb")
621            mustclose = 1
622        else :
623            infile = sys.stdin
624        CHUNK = 64*1024         # read 64 Kb at a time
625        dummy = 0
626        sizeread = 0
627        checksum = md5.new()
628        outfile = open(self.DataFile, "wb")
629        while 1 :
630            data = infile.read(CHUNK)
631            if not data :
632                break
633            sizeread += len(data)
634            outfile.write(data)
635            checksum.update(data)
636            if not (dummy % 32) : # Only display every 2 Mb
637                self.logDebug("%s bytes saved..." % sizeread)
638            dummy += 1
639        outfile.close()
640        if mustclose :
641            infile.close()
642        self.JobSize = sizeread
643        self.JobMD5Sum = checksum.hexdigest()
644        self.logDebug("Job %s is %s bytes long." % (self.JobId, self.JobSize))
645        self.logDebug("Job %s MD5 sum is %s" % (self.JobId, self.JobMD5Sum))
646
647    def cleanUp(self) :
648        """Cleans up the place."""
649        if not self.isTrue(self.getPrintQueueOption(self.PrinterName, "keepfiles", ignore=1)) :
650            os.remove(self.DataFile)
651
652    def sigtermHandler(self, signum, frame) :
653        """Sets an attribute whenever SIGTERM is received."""
654        self.gotSigTerm = 1
655        self.logInfo("SIGTERM received for Job %s." % self.JobId)
656
657    def runBranches(self) :
658        """Launches each hook defined for the current print queue."""
659        self.isCancelled = 0    # did a prehook cancel the print job ?
660        self.gotSigTerm = 0
661        signal.signal(signal.SIGTERM, self.sigtermHandler)
662        serialize = self.isTrue(self.getPrintQueueOption(self.PrinterName, "serialize", ignore=1))
663        self.pipes = { 0: (0, 1) }
664        branches = self.enumBranches(self.PrinterName, "prehook")
665        for b in branches :
666            self.pipes[b.split("_", 1)[1]] = os.pipe()
667        retcode = self.runCommands("prehook", branches, serialize)
668        for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] :
669            os.close(p[1][1])
670        if not self.isCancelled and not self.gotSigTerm :
671            if self.RealBackend :
672                retcode = self.runOriginalBackend()
673            if not self.gotSigTerm :
674                os.environ["TEASTATUS"] = str(retcode)
675                branches = self.enumBranches(self.PrinterName, "posthook")
676                if self.runCommands("posthook", branches, serialize) :
677                    self.logInfo("An error occured during the execution of posthooks.", "warn")
678        for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] :
679            os.close(p[1][0])
680        signal.signal(signal.SIGTERM, signal.SIG_IGN)
681        if not retcode :
682            self.logInfo("OK")
683        else :
684            self.logInfo("An error occured, please check CUPS' error_log file.")
685        return retcode
686
687    def stdioRedirSystem(self, cmd, stdin=0, stdout=1) :
688        """Launches a command with stdio redirected."""
689        # Code contributed by Peter Stuge on May 23rd and June 7th 2005
690        pid = os.fork()
691        if pid == 0 :
692            if stdin != 0 :
693                os.dup2(stdin, 0)
694                os.close(stdin)
695            if stdout != 1 :
696                os.dup2(stdout, 1)
697                os.close(stdout)
698            try :
699                os.execl("/bin/sh", "sh", "-c", cmd)
700            except OSError, msg :
701                self.logDebug("execl() failed: %s" % msg)
702            os._exit(-1)
703        status = os.waitpid(pid, 0)[1]
704        if os.WIFEXITED(status) :
705            return os.WEXITSTATUS(status)
706        return -1
707
708    def runCommand(self, branch, command) :
709        """Runs a particular branch command."""
710        # Code contributed by Peter Stuge on June 7th 2005
711        self.logDebug("Launching %s : %s" % (branch, command))
712        btype, bname = branch.split("_", 1)
713        if bname not in self.pipes.keys() :
714            bname = 0
715        if btype == "prehook" :
716            return self.stdioRedirSystem(command, 0, self.pipes[bname][1])
717        else :
718            return self.stdioRedirSystem(command, self.pipes[bname][0])
719
720    def runCommands(self, btype, branches, serialize) :
721        """Runs the commands for a particular branch type."""
722        exitcode = 0
723        btype = btype.lower()
724        btypetitle = btype.title()
725        branchlist = branches.keys()
726        branchlist.sort()
727        if serialize :
728            self.logDebug("Begin serialized %ss" % btypetitle)
729            for branch in branchlist :
730                if self.gotSigTerm :
731                    break
732                retcode = self.runCommand(branch, branches[branch])
733                self.logDebug("Exit code for %s %s on printer %s is %s" % (btype, branch, self.PrinterName, retcode))
734                if retcode :
735                    if (btype == "prehook") and (retcode == 255) : # -1
736                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
737                        self.isCancelled = 1
738                    else :
739                        self.logInfo("%s %s on printer %s didn't exit successfully." % (btypetitle, branch, self.PrinterName), "error")
740                        exitcode = 1
741            self.logDebug("End serialized %ss" % btypetitle)
742        else :
743            self.logDebug("Begin forked %ss" % btypetitle)
744            pids = {}
745            for branch in branchlist :
746                if self.gotSigTerm :
747                    break
748                pid = os.fork()
749                if pid :
750                    pids[branch] = pid
751                else :
752                    os._exit(self.runCommand(branch, branches[branch]))
753            for (branch, pid) in pids.items() :
754                retcode = os.waitpid(pid, 0)[1]
755                if os.WIFEXITED(retcode) :
756                    retcode = os.WEXITSTATUS(retcode)
757                else :
758                    retcode = -1
759                self.logDebug("Exit code for %s %s (PID %s) on printer %s is %s" % (btype, branch, pid, self.PrinterName, retcode))
760                if retcode :
761                    if (btype == "prehook") and (retcode == 255) : # -1
762                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
763                        self.isCancelled = 1
764                    else :
765                        self.logInfo("%s %s (PID %s) on printer %s didn't exit successfully." % (btypetitle, branch, pid, self.PrinterName), "error")
766                        exitcode = 1
767            self.logDebug("End forked %ss" % btypetitle)
768        return exitcode
769
770    def runOriginalBackend(self) :
771        """Launches the original backend."""
772        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
773        arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:]
774        self.logDebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in arguments])))
775
776        pid = os.fork()
777        if pid == 0 :
778            if self.InputFile is None :
779                f = open(self.dataFile, "rb")
780                os.dup2(f.fileno(), 0)
781                f.close()
782            try :
783                os.execve(originalbackend, arguments, os.environ)
784            except OSError, msg :
785                self.logDebug("execve() failed: %s" % msg)
786            os._exit(-1)
787        killed = 0
788        status = -1
789        while status == -1 :
790            try :
791                status = os.waitpid(pid, 0)[1]
792            except OSError, (err, msg) :
793                if (err == 4) and self.gotSigTerm :
794                    os.kill(pid, signal.SIGTERM)
795                    killed = 1
796        if os.WIFEXITED(status) :
797            status = os.WEXITSTATUS(status)
798            if status :
799              self.logInfo("CUPS backend %s returned %d." % (originalbackend,\
800                                                             status), "error")
801            return status
802        elif not killed :
803            self.logInfo("CUPS backend %s died abnormally." % originalbackend,\
804                                                              "error")
805            return -1
806        else :
807            return 1
808
809if __name__ == "__main__" :
810    # This is a CUPS backend, we should act and die like a CUPS backend
811    wrapper = CupsBackend()
812    if len(sys.argv) == 1 :
813        print "\n".join(wrapper.discoverOtherBackends())
814        sys.exit(0)
815    elif len(sys.argv) not in (6, 7) :
816        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
817                              % sys.argv[0])
818        sys.exit(1)
819    else :
820        try :
821            wrapper.readConfig()
822            wrapper.initBackend()
823            wrapper.saveDatasAndCheckSum()
824            wrapper.exportAttributes()
825            retcode = wrapper.runBranches()
826            wrapper.cleanUp()
827        except SystemExit, e :
828            retcode = e.code
829        except :
830            import traceback
831            lines = []
832            for line in traceback.format_exception(*sys.exc_info()) :
833                lines.extend([l for l in line.split("\n") if l])
834            msg = "ERROR: ".join(["%s (PID %s) : %s\n" % (wrapper.MyName, \
835                                                          wrapper.pid, l) \
836                        for l in (["ERROR: Tea4CUPS v%s" % version] + lines)])
837            sys.stderr.write(msg)
838            sys.stderr.flush()
839            retcode = 1
840        sys.exit(retcode)
Note: See TracBrowser for help on using the browser.