root / pykota / trunk / bin / cupspykota @ 2632

Revision 2632, 58.1 kB (checked in by jerome, 18 years ago)

If the end user cancels the print job for some reason, report this in the PYKOTASTATUS environment variable.

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