root / pykota / trunk / bin / cupspykota @ 2759

Revision 2759, 59.2 kB (checked in by jerome, 18 years ago)

Added support for an extended syntax for the 'onbackenderror' directive.

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