root / pykota / trunk / bin / cupspykota @ 2622

Revision 2622, 56.5 kB (checked in by jerome, 19 years ago)

Added 2006 to the copyright's years.

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
Line 
1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
4# CUPSPyKota accounting backend
5#
6# PyKota - Print Quotas for CUPS and LPRng
7#
8# (c) 2003, 2004, 2005, 2006 Jerome Alet <alet@librelogiciel.com>
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22#
23# $Id$
24#
25#
26
27import sys
28import os
29import errno
30import tempfile
31import popen2
32import cStringIO
33import shlex
34import signal
35import md5
36import fnmatch
37import pwd
38import socket
39import smtplib
40
41from pykota.tool import PyKotaTool, PyKotaToolError, crashed
42from pykota.accounter import openAccounter
43from pykota.ipp import IPPRequest, IPPError
44from pykota.storage import PyKotaStorageError
45       
46class CUPSBackend(PyKotaTool) :
47    """Base class for tools with no database access."""
48    def __init__(self) :
49        """Initializes the CUPS backend wrapper."""
50        PyKotaTool.__init__(self)
51        signal.signal(signal.SIGTERM, signal.SIG_IGN)
52        signal.signal(signal.SIGPIPE, signal.SIG_IGN)
53        self.MyName = "PyKota"
54        self.myname = "cupspykota"
55        self.pid = os.getpid()
56       
57    def deferredInit(self) :   
58        """Deferred initialization."""
59        PyKotaTool.deferredInit(self)
60        self.gotSigTerm = 0
61        self.installSigTermHandler()
62       
63    def sigtermHandler(self, signum, frame) :
64        """Sets an attribute whenever SIGTERM is received."""
65        self.gotSigTerm = 1
66        self.printInfo(_("SIGTERM received, job %s cancelled.") % self.JobId)
67        os.environ["PYKOTASTATUS"] = "CANCELLED"
68       
69    def deinstallSigTermHandler(self) :           
70        """Deinstalls the SIGTERM handler."""
71        self.logdebug("Deinstalling SIGTERM handler...")
72        signal.signal(signal.SIGTERM, signal.SIG_IGN)
73        self.logdebug("SIGTERM handler deinstalled.")
74       
75    def installSigTermHandler(self) :           
76        """Installs the SIGTERM handler."""
77        self.logdebug("Installing SIGTERM handler...")
78        signal.signal(signal.SIGTERM, self.sigtermHandler)
79        self.logdebug("SIGTERM handler installed.")
80       
81    def discoverOtherBackends(self) :   
82        """Discovers the other CUPS backends.
83       
84           Executes each existing backend in turn in device enumeration mode.
85           Returns the list of available backends.
86        """
87        # Unfortunately this method can't output any debug information
88        # to stdout or stderr, else CUPS considers that the device is
89        # not available.
90        available = []
91        (directory, myname) = os.path.split(sys.argv[0])
92        if not directory :
93            directory = "./"
94        tmpdir = tempfile.gettempdir()
95        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
96        if os.path.exists(lockfilename) :
97            lockfile = open(lockfilename, "r")
98            pid = int(lockfile.read())
99            lockfile.close()
100            try :
101                # see if the pid contained in the lock file is still running
102                os.kill(pid, 0)
103            except OSError, e :   
104                if e.errno != errno.EPERM :
105                    # process doesn't exist anymore
106                    os.remove(lockfilename)
107           
108        if not os.path.exists(lockfilename) :
109            lockfile = open(lockfilename, "w")
110            lockfile.write("%i" % self.pid)
111            lockfile.close()
112            allbackends = [ os.path.join(directory, b) \
113                                for b in os.listdir(directory) \
114                                    if os.access(os.path.join(directory, b), os.X_OK) \
115                                        and (b != myname)] 
116            for backend in allbackends :                           
117                answer = os.popen(backend, "r")
118                try :
119                    devices = [line.strip() for line in answer.readlines()]
120                except :   
121                    devices = []
122                status = answer.close()
123                if status is None :
124                    for d in devices :
125                        # each line is of the form :
126                        # 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
127                        # so we have to decompose it carefully
128                        fdevice = cStringIO.StringIO(d)
129                        tokenizer = shlex.shlex(fdevice)
130                        tokenizer.wordchars = tokenizer.wordchars + \
131                                                        r".:,?!~/\_$*-+={}[]()#"
132                        arguments = []
133                        while 1 :
134                            token = tokenizer.get_token()
135                            if token :
136                                arguments.append(token)
137                            else :
138                                break
139                        fdevice.close()
140                        try :
141                            (devicetype, device, name, fullname) = arguments
142                        except ValueError :   
143                            pass    # ignore this 'bizarre' device
144                        else :   
145                            if name.startswith('"') and name.endswith('"') :
146                                name = name[1:-1]
147                            if fullname.startswith('"') and fullname.endswith('"') :
148                                fullname = fullname[1:-1]
149                            available.append('%s %s:%s "%s+%s" "%s managed %s"' \
150                                                 % (devicetype, self.myname, \
151                                                    device, self.MyName, \
152                                                    name, self.MyName, \
153                                                    fullname))
154            os.remove(lockfilename)
155        available.append('direct %s:// "%s+Nothing" "%s managed Virtual Printer"' \
156                             % (self.myname, self.MyName, self.MyName))
157        return available
158                       
159    def initBackendParameters(self) :   
160        """Initializes the backend's attributes."""
161        # check that the DEVICE_URI environment variable's value is
162        # prefixed with self.myname otherwise don't touch it.
163        # If this is the case, we have to remove the prefix from
164        # the environment before launching the real backend
165        self.logdebug("Initializing backend...")
166        muststartwith = "%s:" % self.myname
167        device_uri = os.environ.get("DEVICE_URI", "")
168        if device_uri.startswith(muststartwith) :
169            fulldevice_uri = device_uri[:]
170            device_uri = fulldevice_uri[len(muststartwith):]
171            for i in range(2) :
172                if device_uri.startswith("/") : 
173                    device_uri = device_uri[1:]
174        try :
175            (backend, destination) = device_uri.split(":", 1) 
176        except ValueError :   
177            if not device_uri :
178                self.logdebug("Not attached to an existing print queue.")
179                backend = ""
180                printerhostname = ""
181            else :   
182                raise PyKotaToolError, "Invalid DEVICE_URI : %s\n" % device_uri
183        else :       
184            while destination.startswith("/") :
185                destination = destination[1:]
186            checkauth = destination.split("@", 1)   
187            if len(checkauth) == 2 :
188                destination = checkauth[1]
189            printerhostname = destination.split("/")[0].split(":")[0]
190       
191        self.Action = "ALLOW"   # job allowed by default
192        self.Reason = None
193        self.JobId = sys.argv[1].strip()
194        # use CUPS' user when printing test pages from CUPS' web interface
195        self.UserName = sys.argv[2].strip() or pwd.getpwuid(os.geteuid())[0]
196        self.Title = sys.argv[3].strip()
197        self.Copies = int(sys.argv[4].strip())
198        self.Options = sys.argv[5].strip()
199        if len(sys.argv) == 7 :
200            self.InputFile = sys.argv[6] # read job's datas from file
201        else :   
202            self.InputFile = None        # read job's datas from stdin
203           
204        self.PrinterHostName = printerhostname   
205        self.RealBackend = backend
206        self.DeviceURI = device_uri
207        self.PrinterName = os.environ.get("PRINTER", "")
208        self.Directory = self.config.getPrinterDirectory(self.PrinterName)
209        self.DataFile = os.path.join(self.Directory, "%s-%s-%s-%s" % \
210                   (self.myname, self.PrinterName, self.UserName, self.JobId))
211       
212        (ippfilename, ippmessage) = self.parseIPPRequestFile()
213        self.ControlFile = ippfilename
214        john = ippmessage.operation_attributes.get("job-originating-host-name", \
215               ippmessage.job_attributes.get("job-originating-host-name", \
216               (None, None)))
217        if type(john) == type([]) : 
218            john = john[-1]
219        (chtype, self.ClientHost) = john 
220        jbing = ippmessage.job_attributes.get("job-billing", (None, None))
221        if type(jbing) == type([]) : 
222            jbing = jbing[-1]
223        (jbtype, self.JobBillingCode) = jbing
224       
225        self.logdebug("Backend : %s" % self.RealBackend)
226        self.logdebug("DeviceURI : %s" % self.DeviceURI)
227        self.logdebug("Printername : %s" % self.PrinterName)
228        self.logdebug("Username : %s" % self.UserName)
229        self.logdebug("JobId : %s" % self.JobId)
230        self.logdebug("Title : %s" % self.Title)
231        self.logdebug("Filename : %s" % self.InputFile)
232        self.logdebug("Copies : %s" % self.Copies)
233        self.logdebug("Options : %s" % self.Options)
234        self.logdebug("Directory : %s" % self.Directory) 
235        self.logdebug("DataFile : %s" % self.DataFile)
236        self.logdebug("ControlFile : %s" % self.ControlFile)
237        self.logdebug("JobBillingCode : %s" % self.JobBillingCode)
238        self.logdebug("JobOriginatingHostName : %s" % self.ClientHost)
239       
240        self.logdebug("Backend initialized.")
241       
242    def overwriteJobAttributes(self) :
243        """Overwrites some of the job's attributes if needed."""
244        self.logdebug("Sanitizing job's attributes...")
245        # First overwrite the job ticket
246        self.overwriteJobTicket()
247       
248        # do we want to strip out the Samba/Winbind domain name ?
249        separator = self.config.getWinbindSeparator()
250        if separator is not None :
251            self.UserName = self.UserName.split(separator)[-1]
252           
253        # do we want to lowercase usernames ?   
254        if self.config.getUserNameToLower() :
255            self.UserName = self.UserName.lower()
256           
257        # do we want to strip some prefix off of titles ?   
258        stripprefix = self.config.getStripTitle(self.PrinterName)
259        if stripprefix :
260            if fnmatch.fnmatch(self.Title[:len(stripprefix)], stripprefix) :
261                self.logdebug("Prefix [%s] removed from job's title [%s]." \
262                                      % (stripprefix, self.Title))
263                self.Title = self.Title[len(stripprefix):]
264               
265        self.logdebug("Username : %s" % self.UserName)
266        self.logdebug("BillingCode : %s" % self.JobBillingCode)
267        self.logdebug("Title : %s" % self.Title)
268        self.logdebug("Job's attributes sanitizing done.")
269               
270    def overwriteJobTicket(self) :   
271        """Should we overwrite the job's ticket (username and billingcode) ?"""
272        self.logdebug("Checking if we need to overwrite the job ticket...")
273        jobticketcommand = self.config.getOverwriteJobTicket(self.PrinterName)
274        if jobticketcommand is not None :
275            username = billingcode = action = None
276            self.logdebug("Launching subprocess [%s] to overwrite the job ticket." \
277                                     % jobticketcommand)
278            inputfile = os.popen(jobticketcommand, "r")
279            for line in inputfile.xreadlines() :
280                line = line.strip()
281                if line == "DENY" :
282                    self.logdebug("Seen DENY command.")
283                    action = "DENY"
284                elif line.startswith("USERNAME=") :   
285                    username = line.split("=", 1)[1].strip()
286                    self.logdebug("Seen new username [%s]" % username)
287                    action = None
288                elif line.startswith("BILLINGCODE=") :   
289                    billingcode = line.split("=", 1)[1].strip()
290                    self.logdebug("Seen new billing code [%s]" % billingcode)
291                    action = None
292            inputfile.close()   
293           
294            # now overwrite the job's ticket if new data was supplied
295            if action :
296                self.Action = action
297                self.Reason = _("You are not allowed to print at this time.")
298            if username :
299                self.UserName = username
300            # NB : we overwrite the billing code even if empty   
301            self.JobBillingCode = billingcode 
302        self.logdebug("Job ticket overwriting done.")
303           
304    def saveDatasAndCheckSum(self) :
305        """Saves the input datas into a static file."""
306        self.logdebug("Duplicating data stream into %s" % self.DataFile)
307        mustclose = 0
308        outfile = open(self.DataFile, "wb")   
309        if self.InputFile is not None :
310            self.regainPriv()
311            infile = open(self.InputFile, "rb")
312            mustclose = 1
313        else :   
314            infile = sys.stdin
315        CHUNK = 64*1024         # read 64 Kb at a time
316        dummy = 0
317        sizeread = 0
318        checksum = md5.new()
319        while 1 :
320            data = infile.read(CHUNK) 
321            if not data :
322                break
323            sizeread += len(data)   
324            outfile.write(data)
325            checksum.update(data)   
326            if not (dummy % 32) : # Only display every 2 Mb
327                self.logdebug("%s bytes saved..." % sizeread)
328            dummy += 1   
329        if mustclose :   
330            infile.close()
331            self.dropPriv()
332           
333        outfile.close()
334        self.JobSizeBytes = sizeread   
335        self.JobMD5Sum = checksum.hexdigest()
336       
337        self.logdebug("JobSizeBytes : %s" % self.JobSizeBytes)
338        self.logdebug("JobMD5Sum : %s" % self.JobMD5Sum)
339        self.logdebug("Data stream duplicated into %s" % self.DataFile)
340           
341    def clean(self) :
342        """Cleans up the place."""
343        self.logdebug("Cleaning up...")
344        self.deinstallSigTermHandler()
345        if not self.config.getPrinterKeepFiles(self.PrinterName) :
346            try :
347                self.logdebug("Work file %s will be deleted." % self.DataFile)
348            except AttributeError :   
349                pass
350            else :   
351                os.remove(self.DataFile)
352                self.logdebug("Work file %s has been deleted." % self.DataFile)
353        else :   
354            self.logdebug("Work file %s will be kept." % self.DataFile)
355        PyKotaTool.clean(self)   
356        self.logdebug("Clean.")
357           
358    def precomputeJobSize(self) :   
359        """Computes the job size with a software method."""
360        self.logdebug("Precomputing job's size...")
361        jobsize = 0
362        if self.JobSizeBytes :
363            try :
364                from pkpgpdls import analyzer, pdlparser
365            except ImportError :   
366                self.printInfo("pkpgcounter is now distributed separately, please grab it from http://www.librelogiciel.com/software/pkpgcounter/action_Download", "error")
367                self.printInfo("Precomputed job size will be forced to 0 pages.", "error")
368            else :     
369                infile = open(self.DataFile, "rb")
370                try :
371                    parser = analyzer.PDLAnalyzer(infile)
372                    jobsize = parser.getJobSize()
373                except pdlparser.PDLParserError, msg :   
374                    # Here we just log the failure, but
375                    # we finally ignore it and return 0 since this
376                    # computation is just an indication of what the
377                    # job's size MAY be.
378                    self.printInfo(_("Unable to precompute the job's size with the generic PDL analyzer : %s") % msg, "warn")
379                else :   
380                    if self.InputFile is not None :
381                        # when a filename is passed as an argument, the backend
382                        # must generate the correct number of copies.
383                        jobsize *= self.Copies
384                infile.close()       
385        self.softwareJobSize = jobsize
386        self.logdebug("Precomputed job's size is %s pages." % self.softwareJobSize)
387       
388    def precomputeJobPrice(self) :   
389        """Precomputes the job price with a software method."""
390        self.logdebug("Precomputing job's price...")
391        self.softwareJobPrice = self.UserPQuota.computeJobPrice(self.softwareJobSize)
392        self.logdebug("Precomputed job's price is %.3f credits." \
393                                   % self.softwareJobPrice)
394       
395    def getCupsConfigDirectives(self, directives=[]) :
396        """Retrieves some CUPS directives from its configuration file.
397       
398           Returns a mapping with lowercased directives as keys and
399           their setting as values.
400        """
401        self.logdebug("Parsing CUPS' configuration file...")
402        dirvalues = {} 
403        cupsroot = os.environ.get("CUPS_SERVERROOT", "/etc/cups")
404        cupsdconf = os.path.join(cupsroot, "cupsd.conf")
405        try :
406            conffile = open(cupsdconf, "r")
407        except IOError :   
408            raise PyKotaToolError, "Unable to open %s" % cupsdconf
409        else :   
410            for line in conffile.readlines() :
411                linecopy = line.strip().lower()
412                for di in [d.lower() for d in directives] :
413                    if linecopy.startswith("%s " % di) :
414                        try :
415                            val = line.split()[1]
416                        except :   
417                            pass # ignore errors, we take the last value in any case.
418                        else :   
419                            dirvalues[di] = val
420            conffile.close()           
421        self.logdebug("CUPS' configuration file parsed successfully.")
422        return dirvalues       
423           
424    def parseIPPRequestFile(self) :       
425        """Parses the IPP message file and returns a tuple (filename, parsedvalue)."""
426        self.logdebug("Parsing IPP request file...")
427       
428        class DummyClass :
429            operation_attributes = {}
430            job_attributes = {}
431           
432        ippmessage = DummyClass() # in case the code below fails
433       
434        self.regainPriv()
435        cupsdconf = self.getCupsConfigDirectives(["RequestRoot"])
436        requestroot = cupsdconf.get("requestroot", "/var/spool/cups")
437        if (len(self.JobId) < 5) and self.JobId.isdigit() :
438            ippmessagefile = "c%05i" % int(self.JobId)
439        else :   
440            ippmessagefile = "c%s" % self.JobId
441        ippmessagefile = os.path.join(requestroot, ippmessagefile)
442        try :
443            ippdatafile = open(ippmessagefile)
444        except :   
445            self.logdebug("Unable to open IPP request file %s" % ippmessagefile)
446        else :   
447            self.logdebug("Parsing of IPP request file %s begins." % ippmessagefile)
448            try :
449                ippmessage = IPPRequest(ippdatafile.read())
450                ippmessage.parse()
451            except IPPError, msg :   
452                self.printInfo("Error while parsing %s : %s" \
453                                      % (ippmessagefile, msg), "warn")
454            else :   
455                self.logdebug("Parsing of IPP request file %s ends." \
456                                       % ippmessagefile)
457            ippdatafile.close()
458        self.dropPriv()
459        self.logdebug("IPP request file parsed successfully.")
460        return (ippmessagefile, ippmessage)
461               
462    def exportJobInfo(self) :   
463        """Exports the actual job's attributes to the environment."""
464        self.logdebug("Exporting job information to the environment...")
465        os.environ["DEVICE_URI"] = self.DeviceURI       # WARNING !
466        os.environ["PYKOTAPRINTERNAME"] = self.PrinterName
467        os.environ["PYKOTADIRECTORY"] = self.Directory
468        os.environ["PYKOTADATAFILE"] = self.DataFile
469        os.environ["PYKOTAJOBSIZEBYTES"] = str(self.JobSizeBytes)
470        os.environ["PYKOTAMD5SUM"] = self.JobMD5Sum
471        os.environ["PYKOTAJOBORIGINATINGHOSTNAME"] = self.ClientHost or ""
472        os.environ["PYKOTAJOBID"] = self.JobId
473        os.environ["PYKOTAUSERNAME"] = self.UserName
474        os.environ["PYKOTATITLE"] = self.Title
475        os.environ["PYKOTACOPIES"] = str(self.Copies)
476        os.environ["PYKOTAOPTIONS"] = self.Options
477        os.environ["PYKOTAFILENAME"] = self.InputFile or ""
478        os.environ["PYKOTAJOBBILLING"] = self.JobBillingCode or ""
479        os.environ["PYKOTACONTROLFILE"] = self.ControlFile
480        os.environ["PYKOTAPRINTERHOSTNAME"] = self.PrinterHostName
481        os.environ["PYKOTAPRECOMPUTEDJOBSIZE"] = str(self.softwareJobSize)
482        self.logdebug("Environment updated.")
483       
484    def exportUserInfo(self) :
485        """Exports user information to the environment."""
486        self.logdebug("Exporting user information to the environment...")
487        os.environ["PYKOTAOVERCHARGE"] = str(self.User.OverCharge)
488        os.environ["PYKOTALIMITBY"] = str(self.User.LimitBy)
489        os.environ["PYKOTABALANCE"] = str(self.User.AccountBalance or 0.0)
490        os.environ["PYKOTALIFETIMEPAID"] = str(self.User.LifeTimePaid or 0.0)
491       
492        os.environ["PYKOTAPAGECOUNTER"] = str(self.UserPQuota.PageCounter or 0)
493        os.environ["PYKOTALIFEPAGECOUNTER"] = str(self.UserPQuota.LifePageCounter or 0)
494        os.environ["PYKOTASOFTLIMIT"] = str(self.UserPQuota.SoftLimit)
495        os.environ["PYKOTAHARDLIMIT"] = str(self.UserPQuota.HardLimit)
496        os.environ["PYKOTADATELIMIT"] = str(self.UserPQuota.DateLimit)
497        os.environ["PYKOTAWARNCOUNT"] = str(self.UserPQuota.WarnCount)
498       
499        # TODO : move this elsewhere once software accounting is done only once.
500        os.environ["PYKOTAPRECOMPUTEDJOBPRICE"] = str(self.softwareJobPrice)
501       
502        self.logdebug("Environment updated.")
503       
504    def exportPrinterInfo(self) :
505        """Exports printer information to the environment."""
506        self.logdebug("Exporting printer information to the environment...")
507        # exports the list of printers groups the current
508        # printer is a member of
509        os.environ["PYKOTAPGROUPS"] = ",".join([p.Name for p in self.storage.getParentPrinters(self.Printer)])
510        self.logdebug("Environment updated.")
511       
512    def exportPhaseInfo(self, phase) :
513        """Exports phase information to the environment."""
514        self.logdebug("Exporting phase information [%s] to the environment..." % phase)
515        os.environ["PYKOTAPHASE"] = phase
516        self.logdebug("Environment updated.")
517       
518    def exportJobSizeAndPrice(self) :
519        """Exports job's size and price information to the environment."""
520        self.logdebug("Exporting job's size and price information to the environment...")
521        os.environ["PYKOTAJOBSIZE"] = str(self.JobSize)
522        os.environ["PYKOTAJOBPRICE"] = str(self.JobPrice)
523        self.logdebug("Environment updated.")
524       
525    def exportReason(self) :
526        """Exports the job's action status and optional reason."""
527        self.logdebug("Exporting job's action status...")
528        os.environ["PYKOTAACTION"] = str(self.Action)
529        if self.Reason :
530            os.environ["PYKOTAREASON"] = str(self.Reason)
531        self.logdebug("Environment updated.")
532       
533    def acceptJob(self) :       
534        """Returns the appropriate exit code to tell CUPS all is OK."""
535        return 0
536           
537    def removeJob(self) :           
538        """Returns the appropriate exit code to let CUPS think all is OK.
539       
540           Returning 0 (success) prevents CUPS from stopping the print queue.
541        """   
542        return 0
543       
544    def launchPreHook(self) :
545        """Allows plugging of an external hook before the job gets printed."""
546        prehook = self.config.getPreHook(self.PrinterName)
547        if prehook :
548            self.logdebug("Executing pre-hook [%s]..." % prehook)
549            retcode = os.system(prehook)
550            self.logdebug("pre-hook exited with status %s." % retcode)
551       
552    def launchPostHook(self) :
553        """Allows plugging of an external hook after the job gets printed and/or denied."""
554        posthook = self.config.getPostHook(self.PrinterName)
555        if posthook :
556            self.logdebug("Executing post-hook [%s]..." % posthook)
557            retcode = os.system(posthook)
558            self.logdebug("post-hook exited with status %s." % retcode)
559           
560    def improveMessage(self, message) :       
561        """Improves a message by adding more informations in it if possible."""
562        try :
563            return "%s@%s(%s) => %s" % (self.UserName, \
564                                        self.PrinterName, \
565                                        self.JobId, \
566                                        message)
567        except :                                               
568            return message
569       
570    def logdebug(self, message) :       
571        """Improves the debug message before outputting it."""
572        PyKotaTool.logdebug(self, self.improveMessage(message))
573       
574    def printInfo(self, message, level="info") :       
575        """Improves the informational message before outputting it."""
576        self.logger.log_message(self.improveMessage(message), level)
577   
578    def startingBanner(self, withaccounting) :
579        """Retrieves a starting banner for current printer and returns its content."""
580        self.logdebug("Retrieving starting banner...")
581        self.printBanner(self.config.getStartingBanner(self.PrinterName), withaccounting)
582        self.logdebug("Starting banner retrieved.")
583   
584    def endingBanner(self, withaccounting) :
585        """Retrieves an ending banner for current printer and returns its content."""
586        self.logdebug("Retrieving ending banner...")
587        self.printBanner(self.config.getEndingBanner(self.PrinterName), withaccounting)
588        self.logdebug("Ending banner retrieved.")
589       
590    def printBanner(self, bannerfileorcommand, withaccounting) :
591        """Reads a banner or generates one through an external command.
592       
593           Returns the banner's content in a format which MUST be accepted
594           by the printer.
595        """
596        self.logdebug("Printing banner...")
597        if bannerfileorcommand :
598            if os.access(bannerfileorcommand, os.X_OK) or \
599                  not os.path.isfile(bannerfileorcommand) :
600                self.logdebug("Launching %s to generate a banner." % bannerfileorcommand)
601                child = popen2.Popen3(bannerfileorcommand, capturestderr=1)
602                self.runOriginalBackend(child.fromchild, isBanner=1)
603                child.tochild.close()
604                child.childerr.close()
605                child.fromchild.close()
606                status = child.wait()
607                if os.WIFEXITED(status) :
608                    status = os.WEXITSTATUS(status)
609                self.printInfo(_("Banner generator %s exit code is %s") \
610                                         % (bannerfileorcommand, str(status)))
611                if withaccounting :
612                    if self.accounter.isSoftware :
613                        self.BannerSize += 1 # TODO : fix this by passing the banner's content through software accounting
614            else :
615                self.logdebug("Using %s as the banner." % bannerfileorcommand)
616                try :
617                    fh = open(bannerfileorcommand, 'rb')
618                except IOError, msg :   
619                    self.printInfo("Impossible to open %s : %s" \
620                                       % (bannerfileorcommand, msg), "error")
621                else :   
622                    self.runOriginalBackend(fh, isBanner=1)
623                    fh.close()
624                    if withaccounting :
625                        if self.accounter.isSoftware :
626                            self.BannerSize += 1 # TODO : fix this by passing the banner's content through software accounting
627        self.logdebug("Banner printed...")
628               
629    def handleBanner(self, bannertype, withaccounting) :
630        """Handles the banner with or without accounting."""
631        if withaccounting :
632            acc = "with"
633        else :   
634            acc = "without"
635        self.logdebug("Handling %s banner %s accounting..." % (bannertype, acc))
636        if (self.Action == 'DENY') and \
637           (self.UserPQuota.WarnCount >= \
638                            self.config.getMaxDenyBanners(self.PrinterName)) :
639            self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), \
640                             "warn")
641        else :
642            if self.Action == 'DENY' :
643                self.logdebug("Incrementing the number of deny banners for user %s on printer %s" \
644                                  % (self.UserName, self.PrinterName))
645                self.UserPQuota.incDenyBannerCounter() # increments the warning counter
646                self.exportUserInfo()
647            getattr(self, "%sBanner" % bannertype)(withaccounting)
648        self.logdebug("%s banner done." % bannertype.title())
649       
650    def sanitizeJobSize(self) :   
651        """Sanitizes the job's size if needed."""
652        # TODO : there's a difficult to see bug here when banner accounting is activated and hardware accounting is used.
653        self.logdebug("Sanitizing job's size...")
654        if self.softwareJobSize and (self.JobSize != self.softwareJobSize) :
655            self.printInfo(_("Beware : computed job size (%s) != precomputed job size (%s)") % \
656                                       (self.JobSize, self.softwareJobSize), \
657                           "error")
658            (limit, replacement) = self.config.getTrustJobSize(self.PrinterName)
659            if limit is None :
660                self.printInfo(_("The job size will be trusted anyway according to the 'trustjobsize' directive"), "warn")
661            else :
662                if self.JobSize <= limit :
663                    self.printInfo(_("The job size will be trusted because it is inferior to the 'trustjobsize' directive's limit %s") % limit, "warn")
664                else :
665                    self.printInfo(_("The job size will be modified according to the 'trustjobsize' directive : %s") % replacement, "warn")
666                    if replacement == "PRECOMPUTED" :
667                        self.JobSize = self.softwareJobSize
668                    else :   
669                        self.JobSize = replacement
670        self.logdebug("Job's size sanitized.")
671                       
672    def getPrinterUserAndUserPQuota(self) :       
673        """Returns a tuple (policy, printer, user, and user print quota) on this printer.
674       
675           "OK" is returned in the policy if both printer, user and user print quota
676           exist in the Quota Storage.
677           Otherwise, the policy as defined for this printer in pykota.conf is returned.
678           
679           If policy was set to "EXTERNAL" and one of printer, user, or user print quota
680           doesn't exist in the Quota Storage, then an external command is launched, as
681           defined in the external policy for this printer in pykota.conf
682           This external command can do anything, like automatically adding printers
683           or users, for example, and finally extracting printer, user and user print
684           quota from the Quota Storage is tried a second time.
685           
686           "EXTERNALERROR" is returned in case policy was "EXTERNAL" and an error status
687           was returned by the external command.
688        """
689        self.logdebug("Retrieving printer, user, and user print quota entry from database...")
690        for passnumber in range(1, 3) :
691            printer = self.storage.getPrinter(self.PrinterName)
692            user = self.storage.getUser(self.UserName)
693            userpquota = self.storage.getUserPQuota(user, printer)
694            if printer.Exists and user.Exists and userpquota.Exists :
695                policy = "OK"
696                break
697            (policy, args) = self.config.getPrinterPolicy(self.PrinterName)
698            if policy == "EXTERNAL" :   
699                commandline = self.formatCommandLine(args, user, printer)
700                if not printer.Exists :
701                    self.printInfo(_("Printer %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.PrinterName, commandline, self.PrinterName))
702                if not user.Exists :
703                    self.printInfo(_("User %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.UserName, commandline, self.PrinterName))
704                if not userpquota.Exists :
705                    self.printInfo(_("User %s doesn't have quota on printer %s in the PyKota system, applying external policy (%s) for printer %s") % (self.UserName, self.PrinterName, commandline, self.PrinterName))
706                if os.system(commandline) :
707                    self.printInfo(_("External policy %s for printer %s produced an error. Job rejected. Please check PyKota's configuration files.") % (commandline, self.PrinterName), "error")
708                    policy = "EXTERNALERROR"
709                    break
710            else :       
711                if not printer.Exists :
712                    self.printInfo(_("Printer %s not registered in the PyKota system, applying default policy (%s)") % (self.PrinterName, policy))
713                if not user.Exists :
714                    self.printInfo(_("User %s not registered in the PyKota system, applying default policy (%s) for printer %s") % (self.UserName, policy, self.PrinterName))
715                if not userpquota.Exists :
716                    self.printInfo(_("User %s doesn't have quota on printer %s in the PyKota system, applying default policy (%s)") % (self.UserName, self.PrinterName, policy))
717                break
718               
719        if policy == "EXTERNAL" :   
720            if not printer.Exists :
721                self.printInfo(_("Printer %s still not registered in the PyKota system, job will be rejected") % self.PrinterName)
722            if not user.Exists :
723                self.printInfo(_("User %s still not registered in the PyKota system, job will be rejected on printer %s") % (self.UserName, self.PrinterName))
724            if not userpquota.Exists :
725                self.printInfo(_("User %s still doesn't have quota on printer %s in the PyKota system, job will be rejected") % (self.UserName, self.PrinterName))
726        self.Policy = policy         
727        self.Printer = printer
728        self.User = user
729        self.UserPQuota = userpquota
730        self.logdebug("Retrieval of printer, user and user print quota entry done.")
731       
732    def getBillingCode(self) :   
733        """Extracts the billing code from the database.
734         
735           An optional script is launched to notify the user when
736           the billing code is unknown and PyKota was configured to
737           deny printing in this case.
738        """
739        self.logdebug("Retrieving billing code information from the database...")
740        self.BillingCode = None
741        if self.JobBillingCode :
742            self.BillingCode = self.storage.getBillingCode(self.JobBillingCode)
743            if self.BillingCode.Exists :
744                self.logdebug("Billing code [%s] found in database." % self.JobBillingCode)
745            else :
746                msg = "Unknown billing code [%s] : " % self.JobBillingCode
747                (newaction, script) = self.config.getUnknownBillingCode(self.PrinterName)
748                if newaction == "CREATE" :
749                    self.logdebug(msg + "will be created.")
750                    self.BillingCode = self.storage.addBillingCode(self.JobBillingCode)
751                    if self.BillingCode.Exists :
752                        self.logdebug(msg + "has been created.")
753                    else :   
754                        self.printInfo(msg + "couldn't be created.", "error")
755                else :   
756                    self.logdebug(msg + "job will be denied.")
757                    self.Action = newaction
758                    if script is not None : 
759                        self.logdebug(msg + "launching subprocess [%s] to notify user." % script)
760                        os.system(script)
761        self.logdebug("Retrieval of billing code information done.")
762       
763    def checkIfDupe(self) :   
764        """Checks if the job is a duplicate, and handles the situation."""
765        self.logdebug("Checking if the job is a duplicate...")
766        denyduplicates = self.config.getDenyDuplicates(self.PrinterName)
767        if not denyduplicates :
768            self.logdebug("We don't care about duplicate jobs after all.")
769        elif self.Printer.LastJob.Exists \
770             and (self.Printer.LastJob.UserName == self.UserName) \
771             and (self.Printer.LastJob.JobMD5Sum == self.JobMD5Sum) :
772            # TODO : use the current user's last job instead of 
773            # TODO : the current printer's last job. This would be
774            # TODO : better but requires an additional database query
775            # TODO : with SQL, and is much more complex with the
776            # TODO : actual LDAP schema. Maybe this is not very
777            # TODO : important, because usually duplicate jobs are sucessive.
778            msg = _("Job is a dupe")
779            if denyduplicates == 1 :
780                self.printInfo("%s : %s." % (msg, _("Printing is denied by configuration")), "warn")
781                self.Action = "DENY"
782                self.Reason = _("Duplicate print jobs are not allowed on printer %s.") % self.PrinterName
783            else :   
784                self.logdebug("Launching subprocess [%s] to see if duplicate jobs should be allowed or not." % denyduplicates)
785                fanswer = os.popen(denyduplicates, "r")
786                self.Action = fanswer.read().strip().upper()
787                fanswer.close()
788                if self.Action == "DENY" :     
789                    self.printInfo("%s : %s." % (msg, _("Subprocess denied printing of a dupe")), "warn")
790                    self.Reason = _("Duplicate print jobs are not allowed on printer %s at this time.") % self.PrinterName
791                else :   
792                    self.printInfo("%s : %s." % (msg, _("Subprocess allowed printing of a dupe")), "warn")
793        else :           
794            self.logdebug("Job doesn't seem to be a duplicate.")
795        self.logdebug("Checking if the job is a duplicate done.")
796       
797    def tellUser(self) :
798        """Sends a message to an user."""
799        self.logdebug("Sending some feedback to user %s..." % self.UserName) 
800        if not self.Reason :
801            self.logdebug("No feedback to send to user %s." % self.UserName)
802        else :   
803            (mailto, arguments) = self.config.getMailTo(self.PrinterName)
804            if mailto == "EXTERNAL" :
805                # TODO : clean this again
806                self.regainPriv()
807                self.externalMailTo(arguments, self.Action, self.User, self.Printer, self.Reason)
808                self.dropPriv()
809            else :   
810                # TODO : clean this again
811                admin = self.config.getAdmin(self.PrinterName)
812                adminmail = self.config.getAdminMail(self.PrinterName)
813                usermail = self.User.Email or self.User.Name
814                if "@" not in usermail :
815                    usermail = "%s@%s" % (usermail, self.maildomain or self.smtpserver)
816                destination = []
817                if mailto in ("BOTH", "ADMIN") :
818                    destination.append(adminmail)
819                if mailto in ("BOTH", "USER") :   
820                    destination.append(usermail)
821                   
822                fullmessage = self.Reason + (_("\n\nYour system administrator :\n\n\t%s - <%s>\n") % (admin, adminmail))
823                try :   
824                    server = smtplib.SMTP(self.smtpserver)
825                except socket.error, msg :   
826                    self.printInfo(_("Impossible to connect to SMTP server : %s") % msg, "error")
827                else :
828                    try :
829                        server.sendmail(adminmail, destination, \
830                          "From: %s\nTo: %s\nCc: %s\nSubject: %s\n\n%s" \
831                          % (adminmail, usermail, adminmail, _("Print Quota"), fullmessage))
832                    except smtplib.SMTPException, answer :   
833                        for (k, v) in answer.recipients.items() :
834                            self.printInfo(_("Impossible to send mail to %s, error %s : %s") % (k, v[0], v[1]), "error")
835                    server.quit()
836            self.logdebug("Feedback sent to user %s." % self.UserName)
837               
838    def mainWork(self) :   
839        """Main work is done here."""
840        if not self.JobSizeBytes :
841            # if no data to pass to real backend, probably a filter
842            # higher in the chain failed because of a misconfiguration.
843            # we deny the job in this case (nothing to print anyway)
844            self.printInfo(_("Job contains no data. Printing is denied."), "error")
845            return self.removeJob()
846           
847        self.getPrinterUserAndUserPQuota()
848        if self.Policy == "EXTERNALERROR" :
849            # Policy was 'EXTERNAL' and the external command returned an error code
850            return self.removeJob()
851        elif self.Policy == "EXTERNAL" :
852            # Policy was 'EXTERNAL' and the external command wasn't able
853            # to add either the printer, user or user print quota
854            return self.removeJob()
855        elif self.Policy == "DENY" :   
856            # Either printer, user or user print quota doesn't exist,
857            # and the job should be rejected.
858            return self.removeJob()
859        elif self.Policy == "ALLOW" :
860            # ALLOW means : Either printer, user or user print quota doesn't exist,
861            #               but the job should be allowed anyway.
862            self.printInfo(_("Job allowed by printer policy. No accounting will be done."), "warn")
863            return self.printJobDatas()
864        elif self.Policy == "OK" :
865            # OK means : Both printer, user and user print quota exist, job should
866            #            be allowed if current user is allowed to print on this printer
867            return self.doWork()
868        else :   
869            self.printInfo(_("Invalid policy %s for printer %s") % (self.Policy, self.PrinterName), "error")
870            return self.removeJob()
871   
872    def doWork(self) :   
873        """The accounting work is done here."""
874        self.precomputeJobPrice()
875        self.exportUserInfo()
876        self.exportPrinterInfo()
877        self.exportPhaseInfo("BEFORE")
878       
879        if self.Action != "DENY" : 
880            if self.Printer.MaxJobSize and (self.softwareJobSize > self.Printer.MaxJobSize) :
881                # This printer was set to refuse jobs this large.
882                self.printInfo(_("Precomputed job size (%s pages) too large for printer %s.") % (self.softwareJobSize, self.PrinterName), "warn")
883                self.Action = "DENY"
884                # here we don't put the precomputed job size in the message
885                # because in case of error the user could complain :-)
886                self.Reason = _("You are not allowed to print so many pages on printer %s at this time.") % self.PrinterName
887           
888        if self.Action != "DENY" :
889            if self.User.LimitBy == "noprint" :
890                self.printInfo(_("User %s is not allowed to print at this time.") % self.UserName, "warn")
891                self.Action = "DENY"
892                self.Reason = _("Your account settings forbid you to print at this time.")
893               
894        if self.Action != "DENY" :
895            # If printing is still allowed at this time, we
896            # need to extract the billing code information from the database.
897            # No need to do this if the job is denied, this way we
898            # save some database queries.
899            self.getBillingCode()
900           
901        if self.Action != "DENY" :
902            # If printing is still allowed at this time, we
903            # need to check if the job is a dupe or not, and what to do then.
904            # No need to do this if the job is denied, this way we
905            # save some database queries.
906            self.checkIfDupe()
907                   
908        if self.Action != "DENY" :
909            # If printing is still allowed at this time, we
910            # need to check the user's print quota on the current printer.
911            # No need to do this if the job is denied, this way we
912            # save some database queries.
913            if self.User.LimitBy in ('noquota', 'nochange') :
914                self.logdebug("User %s is allowed to print with no limit, no need to check quota." % self.UserName)
915            elif self.Printer.PassThrough :   
916                self.logdebug("Printer %s is in PassThrough mode, no need to check quota." % self.PrinterName)
917            else :
918                self.logdebug("Checking user %s print quota entry on printer %s" \
919                                    % (self.UserName, self.PrinterName))
920                self.Action = self.checkUserPQuota(self.UserPQuota)
921                if self.Action.startswith("POLICY_") :
922                    self.Action = self.Action[7:]
923                if self.Action == "DENY" :
924                    self.printInfo(_("Print Quota exceeded for user %s on printer %s") % (self.UserName, self.PrinterName))
925                    self.Reason = self.config.getHardWarn(self.PrinterName)
926                elif self.Action == "WARN" :   
927                    self.printInfo(_("Print Quota low for user %s on printer %s") % (self.UserName, self.PrinterName))
928                    if self.User.LimitBy and (self.User.LimitBy.lower() == "balance") : 
929                        self.Reason = self.config.getPoorWarn()
930                    else :     
931                        self.Reason = self.config.getSoftWarn(self.PrinterName)
932           
933        # exports some new environment variables
934        self.exportReason()
935       
936        # now tell the user if he needs to know something
937        self.tellUser()
938       
939        # launches the pre hook
940        self.launchPreHook()
941       
942        # handle starting banner pages without accounting
943        self.BannerSize = 0
944        accountbanner = self.config.getAccountBanner(self.PrinterName)
945        if accountbanner in ["ENDING", "NONE"] :
946            self.handleBanner("starting", 0)
947       
948        if self.Action == "DENY" :
949            self.printInfo(_("Job denied, no accounting will be done."))
950        else :
951            self.printInfo(_("Job accounting begins."))
952            self.deinstallSigTermHandler()
953            self.accounter.beginJob(self.Printer)
954            self.installSigTermHandler()
955       
956        # handle starting banner pages with accounting
957        if accountbanner in ["STARTING", "BOTH"] :
958            if not self.gotSigTerm :
959                self.handleBanner("starting", 1)
960       
961        # pass the job's data to the real backend   
962        if (not self.gotSigTerm) and (self.Action in ["ALLOW", "WARN"]) :
963            retcode = self.printJobDatas()
964        else :       
965            retcode = self.removeJob()
966       
967        # indicate phase change
968        self.exportPhaseInfo("AFTER")
969       
970        # handle ending banner pages with accounting
971        if accountbanner in ["ENDING", "BOTH"] :
972            if not self.gotSigTerm :
973                self.handleBanner("ending", 1)
974       
975        # stops accounting
976        if self.Action == "DENY" :
977            self.printInfo(_("Job denied, no accounting has been done."))
978        else :
979            self.deinstallSigTermHandler()
980            self.accounter.endJob(self.Printer)
981            self.installSigTermHandler()
982            self.printInfo(_("Job accounting ends."))
983       
984        # Do all these database changes within a single transaction   
985        # NB : we don't enclose ALL the changes within a single transaction
986        # because while waiting for the printer to answer its internal page
987        # counter, we would open the door to accounting problems for other
988        # jobs launched by the same user at the same time on other printers.
989        # All the code below doesn't take much time, so it's fine.
990        self.storage.beginTransaction()
991        try :
992            onbackenderror = self.config.getPrinterOnBackendError(self.PrinterName)
993            if retcode :
994                # NB : We don't send any feedback to the end user. Only the admin
995                # has to know that the real CUPS backend failed.
996                self.Action = "PROBLEM"
997                self.exportReason()
998                if onbackenderror == "NOCHARGE" :
999                    self.JobSize = 0
1000                    self.printInfo(_("Job size forced to 0 because the real CUPS backend failed. No accounting will be done."), "warn")
1001                else :   
1002                    self.printInfo(_("The real CUPS backend failed, but the job will be accounted for anyway."), "warn")
1003                   
1004            # retrieve the job size   
1005            if self.Action == "DENY" :
1006                self.JobSize = 0
1007                self.printInfo(_("Job size forced to 0 because printing is denied."))
1008            else :   
1009                self.UserPQuota.resetDenyBannerCounter()
1010                if (self.Action != "PROBLEM") or (onbackenderror == "CHARGE") : 
1011                    self.JobSize = self.accounter.getJobSize(self.Printer)
1012                    self.sanitizeJobSize()
1013                    self.JobSize += self.BannerSize
1014            self.printInfo(_("Job size : %i") % self.JobSize)
1015           
1016            if (self.Action == "PROBLEM") and (onbackenderror == "NOCHARGE") :
1017                self.JobPrice = 0.0
1018            elif (self.User.LimitBy == "nochange") or self.Printer.PassThrough :
1019                # no need to update the quota for the current user on this printer
1020                self.printInfo(_("User %s's quota on printer %s won't be modified") % (self.UserName, self.PrinterName))
1021                self.JobPrice = self.UserPQuota.computeJobPrice(self.JobSize)
1022            else :
1023                # update the quota for the current user on this printer
1024                self.printInfo(_("Updating user %s's quota on printer %s") % (self.UserName, self.PrinterName))
1025                self.JobPrice = self.UserPQuota.increasePagesUsage(self.JobSize)
1026           
1027            # adds the current job to history   
1028            self.Printer.addJobToHistory(self.JobId, self.User, self.accounter.getLastPageCounter(), \
1029                                    self.Action, self.JobSize, self.JobPrice, self.InputFile, \
1030                                    self.Title, self.Copies, self.Options, self.ClientHost, \
1031                                    self.JobSizeBytes, self.JobMD5Sum, None, self.JobBillingCode, \
1032                                    self.softwareJobSize, self.softwareJobPrice)
1033            self.printInfo(_("Job added to history."))
1034           
1035            if hasattr(self, "BillingCode") and self.BillingCode and self.BillingCode.Exists :
1036                if (self.Action != "PROBLEM") or (onbackenderror == "CHARGE") :
1037                    self.BillingCode.consume(self.JobSize, self.JobPrice)
1038                    self.printInfo(_("Billing code %s was updated.") % self.BillingCode.BillingCode)
1039        except :   
1040            self.storage.rollbackTransaction()
1041            raise
1042        else :   
1043            self.storage.commitTransaction()
1044           
1045        # exports some new environment variables
1046        self.exportJobSizeAndPrice()
1047       
1048        # then re-export user information with new values
1049        self.exportUserInfo()
1050       
1051        # handle ending banner pages without accounting
1052        if accountbanner in ["STARTING", "NONE"] :
1053            self.handleBanner("ending", 0)
1054                   
1055        self.launchPostHook()
1056           
1057        return retcode   
1058               
1059    def printJobDatas(self) :           
1060        """Sends the job's datas to the real backend."""
1061        self.logdebug("Sending job's datas to real backend...")
1062        if self.InputFile is None :
1063            infile = open(self.DataFile, "rb")
1064        else :   
1065            infile = None
1066        retcode = self.runOriginalBackend(infile)
1067        if self.InputFile is None :
1068            infile.close()
1069        self.logdebug("Job's datas sent to real backend.")
1070        return retcode
1071       
1072    def runOriginalBackend(self, filehandle=None, isBanner=0) :
1073        """Launches the original backend."""
1074        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
1075        if not isBanner :
1076            arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:]
1077        else :   
1078            # For banners, we absolutely WANT
1079            # to remove any filename from the command line !
1080            self.logdebug("It looks like we try to print a banner.")
1081            arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:6]
1082        arguments[2] = self.UserName # in case it was overwritten by external script
1083        # TODO : do something about job-billing option, in case it was overwritten as well...
1084        # TODO : do something about the job title : if we are printing a banner and the backend
1085        # TODO : uses the job's title to name an output file (cups-pdf:// for example), we're stuck !
1086       
1087        self.logdebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in arguments])))
1088        self.regainPriv()   
1089        pid = os.fork()
1090        self.logdebug("Forked !")
1091        if pid == 0 :
1092            if filehandle is not None :
1093                self.logdebug("Redirecting file handle to real backend's stdin")
1094                os.dup2(filehandle.fileno(), 0)
1095            try :
1096                self.logdebug("Calling execve...")
1097                os.execve(originalbackend, arguments, os.environ)
1098            except OSError, msg :
1099                self.logdebug("execve() failed: %s" % msg)
1100            self.logdebug("We shouldn't be there !!!")   
1101            os._exit(-1)
1102        self.dropPriv()   
1103       
1104        self.logdebug("Waiting for original backend to exit...")   
1105        killed = 0
1106        status = -1
1107        while status == -1 :
1108            try :
1109                status = os.waitpid(pid, 0)[1]
1110            except OSError, (err, msg) :
1111                if (err == 4) and self.gotSigTerm :
1112                    os.kill(pid, signal.SIGTERM)
1113                    killed = 1
1114                   
1115        if os.WIFEXITED(status) :
1116            status = os.WEXITSTATUS(status)
1117            message = "CUPS backend %s returned %d." % \
1118                            (originalbackend, status)
1119            if status :
1120                level = "error"
1121                self.Reason = message
1122            else :   
1123                level = "info"
1124            self.printInfo(message, level)
1125            return status
1126        elif not killed :
1127            self.Reason = "CUPS backend %s died abnormally." % originalbackend
1128            self.printInfo(self.Reason, "error")
1129            return -1
1130        else :
1131            self.Reason = "CUPS backend %s was killed." % originalbackend
1132            self.printInfo(self.Reason, "warn")
1133            return 1
1134       
1135if __name__ == "__main__" :   
1136    # This is a CUPS backend, we should act and die like a CUPS backend
1137    wrapper = CUPSBackend()
1138    if len(sys.argv) == 1 :
1139        print "\n".join(wrapper.discoverOtherBackends())
1140        sys.exit(0)               
1141    elif len(sys.argv) not in (6, 7) :   
1142        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
1143                              % sys.argv[0])
1144        sys.exit(1)
1145    else :   
1146        try :
1147            wrapper.deferredInit()
1148            wrapper.initBackendParameters()
1149            wrapper.saveDatasAndCheckSum()
1150            wrapper.accounter = openAccounter(wrapper)
1151            wrapper.precomputeJobSize()
1152            wrapper.exportJobInfo() # exports a first time to give hints to external scripts
1153            wrapper.overwriteJobAttributes()
1154            wrapper.exportJobInfo() # re-exports in case it was overwritten
1155            retcode = wrapper.mainWork()
1156        except SystemExit, e :   
1157            retcode = e.code
1158        except :   
1159            try :
1160                wrapper.crashed("cupspykota backend failed")
1161            except :   
1162                crashed("cupspykota backend failed")
1163            retcode = 1
1164        wrapper.clean()
1165        sys.exit(retcode)
Note: See TracBrowser for help on using the browser.