root / tea4cups / trunk / tea4cups @ 630

Revision 630, 40.8 kB (checked in by jerome, 19 years ago)

Fix for empty username (test pages launched from CUPS' web interface)

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