root / pykota / trunk / bin / cupspykota @ 2631

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

Added support for the CANCEL command in subprocesses launched from the
overwrite_jobticket directive : this will allow end users to be asked
if they really want to proceed to printing once the new version
of pykoticon will be ready.
Several minor fixes.

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