root / pykota / trunk / bin / cupspykota @ 2624

Revision 2624, 57.2 kB (checked in by jerome, 18 years ago)

Now cupspykota can be interrupted cleanly with SIGINT.

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