root / pykota / trunk / bin / cupspykota @ 2766

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

Made cupspykota use the new API to create the billing code.

  • 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.storage.addBillingCode(self.BillingCode)
755                    self.BillingCode = self.storage.getBillingCode(self.JobBillingCode)
756                    if self.BillingCode.Exists :
757                        self.logdebug(msg + "has been created.")
758                    else :   
759                        self.printInfo(msg + "couldn't be created.", "error")
760                else :   
761                    self.logdebug(msg + "job will be denied.")
762                    self.Action = newaction
763                    if script is not None : 
764                        self.logdebug(msg + "launching subprocess [%s] to notify user." % script)
765                        os.system(script)
766        self.logdebug("Retrieval of billing code information done.")
767       
768    def checkIfDupe(self) :   
769        """Checks if the job is a duplicate, and handles the situation."""
770        self.logdebug("Checking if the job is a duplicate...")
771        denyduplicates = self.config.getDenyDuplicates(self.PrinterName)
772        if not denyduplicates :
773            self.logdebug("We don't care about duplicate jobs after all.")
774        else :
775            if self.Printer.LastJob.Exists \
776                    and (self.Printer.LastJob.UserName == self.UserName) \
777                    and (self.Printer.LastJob.JobMD5Sum == self.JobMD5Sum) :
778                now = DateTime.now()
779                try :
780                    previous = DateTime.ISO.ParseDateTime(self.Printer.LastJob.JobDate).localtime()
781                except :
782                    previous = now
783                difference = (now - previous).seconds
784                duplicatesdelay = self.config.getDuplicatesDelay(self.PrinterName)
785                self.logdebug("Difference with previous job : %.2f seconds. Duplicates delay : %.2f seconds." % (difference, duplicatesdelay))
786                if difference > duplicatesdelay :
787                    self.logdebug("Duplicate job allowed because previous one is more than %.2f seconds old." % duplicatesdelay)
788                else :
789                    # TODO : use the current user's last job instead of 
790                    # TODO : the current printer's last job. This would be
791                    # TODO : better but requires an additional database query
792                    # TODO : with SQL, and is much more complex with the
793                    # TODO : actual LDAP schema. Maybe this is not very
794                    # TODO : important, because usually duplicate jobs are sucessive.
795                    msg = _("Job is a dupe")
796                    if denyduplicates == 1 :
797                        self.printInfo("%s : %s." % (msg, _("Printing is denied by configuration")), "warn")
798                        self.Action = "DENY"
799                        self.Reason = _("Duplicate print jobs are not allowed on printer %s.") % self.PrinterName
800                    else :   
801                        self.logdebug("Launching subprocess [%s] to see if duplicate jobs should be allowed or not." % denyduplicates)
802                        fanswer = os.popen(denyduplicates, "r")
803                        self.Action = fanswer.read().strip().upper()
804                        fanswer.close()
805                        if self.Action == "DENY" :     
806                            self.printInfo("%s : %s." % (msg, _("Subprocess denied printing of a dupe")), "warn")
807                            self.Reason = _("Duplicate print jobs are not allowed on printer %s at this time.") % self.PrinterName
808                        else :   
809                            self.printInfo("%s : %s." % (msg, _("Subprocess allowed printing of a dupe")), "warn")
810            else :           
811                self.logdebug("Job doesn't seem to be a duplicate.")
812        self.logdebug("Checking if the job is a duplicate done.")
813       
814    def tellUser(self) :
815        """Sends a message to an user."""
816        self.logdebug("Sending some feedback to user %s..." % self.UserName) 
817        if not self.Reason :
818            self.logdebug("No feedback to send to user %s." % self.UserName)
819        else :   
820            (mailto, arguments) = self.config.getMailTo(self.PrinterName)
821            if mailto == "EXTERNAL" :
822                # TODO : clean this again
823                self.regainPriv()
824                self.externalMailTo(arguments, self.Action, self.User, self.Printer, self.Reason)
825                self.dropPriv()
826            else :   
827                # TODO : clean this again
828                admin = self.config.getAdmin(self.PrinterName)
829                adminmail = self.config.getAdminMail(self.PrinterName)
830                usermail = self.User.Email or self.User.Name
831                if "@" not in usermail :
832                    usermail = "%s@%s" % (usermail, self.maildomain or self.smtpserver)
833                destination = []
834                if mailto in ("BOTH", "ADMIN") :
835                    destination.append(adminmail)
836                if mailto in ("BOTH", "USER") :   
837                    destination.append(usermail)
838                   
839                fullmessage = self.Reason + (_("\n\nYour system administrator :\n\n\t%s - <%s>\n") % (admin, adminmail))
840                try :   
841                    server = smtplib.SMTP(self.smtpserver)
842                except socket.error, msg :   
843                    self.printInfo(_("Impossible to connect to SMTP server : %s") % msg, "error")
844                else :
845                    try :
846                        msg = MIMEText(fullmessage, _charset=self.charset)
847                        msg["Subject"] = str(Header(_("Print Quota"), charset=self.charset))
848                        msg["From"] = adminmail
849                        msg["To"] = usermail
850                        msg["Cc"] = adminmail
851                        server.sendmail(adminmail, destination, msg.as_string())
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 "NOCHARGE" in onbackenderror :
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 ("CHARGE" in onbackenderror) : 
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 ("NOCHARGE" in onbackenderror)) 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 = 0.0
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 ("CHARGE" in onbackenderror)) :
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       
1092        delay = 0
1093        number = 1
1094        for onb in self.config.getPrinterOnBackendError(self.PrinterName) :
1095            if onb.startswith("RETRY:") :
1096                try :
1097                    (number, delay) = [int(p) for p in onb[6:].split(":", 2)]
1098                    if (number < 0) or (delay < 0) :
1099                        raise ValueError
1100                except ValueError :   
1101                    self.printInfo(_("Incorrect value for the 'onbackenderror' directive in section [%s]") % self.PrinterName, "error")
1102                    delay = 0
1103                    number = 1
1104                else :   
1105                    break
1106        loopcnt = 1 
1107        while True :           
1108            if self.InputFile is None :
1109                infile = open(self.DataFile, "rb")
1110            else :   
1111                infile = None
1112            retcode = self.runOriginalBackend(infile)
1113            if self.InputFile is None :
1114                infile.close()
1115            if not retcode :
1116                break
1117            else :
1118                if (not number) or (loopcnt < number) :
1119                    self.logdebug(_("The real backend produced an error, we will try again in %s seconds.") % delay)
1120                    time.sleep(delay)
1121                    loopcnt += 1
1122                else :   
1123                    break
1124           
1125        self.logdebug("Job's datas sent to real backend.")
1126        return retcode
1127       
1128    def runOriginalBackend(self, filehandle=None, isBanner=0) :
1129        """Launches the original backend."""
1130        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
1131        if not isBanner :
1132            arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:]
1133        else :   
1134            # For banners, we absolutely WANT
1135            # to remove any filename from the command line !
1136            self.logdebug("It looks like we try to print a banner.")
1137            arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:6]
1138        arguments[2] = self.UserName # in case it was overwritten by external script
1139        # TODO : do something about job-billing option, in case it was overwritten as well...
1140        # TODO : do something about the job title : if we are printing a banner and the backend
1141        # TODO : uses the job's title to name an output file (cups-pdf:// for example), we're stuck !
1142       
1143        self.logdebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in arguments])))
1144        self.regainPriv()   
1145        pid = os.fork()
1146        self.logdebug("Forked !")
1147        if pid == 0 :
1148            if filehandle is not None :
1149                self.logdebug("Redirecting file handle to real backend's stdin")
1150                os.dup2(filehandle.fileno(), 0)
1151            try :
1152                self.logdebug("Calling execve...")
1153                os.execve(originalbackend, arguments, os.environ)
1154            except OSError, msg :
1155                self.logdebug("execve() failed: %s" % msg)
1156            self.logdebug("We shouldn't be there !!!")   
1157            os._exit(-1)
1158        self.dropPriv()   
1159       
1160        self.logdebug("Waiting for original backend to exit...")   
1161        killed = 0
1162        status = -1
1163        while status == -1 :
1164            try :
1165                status = os.waitpid(pid, 0)[1]
1166            except OSError, (err, msg) :
1167                if (err == 4) and self.gotSigTerm :
1168                    os.kill(pid, signal.SIGTERM)
1169                    killed = 1
1170                   
1171        if os.WIFEXITED(status) :
1172            status = os.WEXITSTATUS(status)
1173            message = "CUPS backend %s returned %d." % \
1174                            (originalbackend, status)
1175            if status :
1176                level = "error"
1177                self.Reason = message
1178            else :   
1179                level = "info"
1180            self.printInfo(message, level)
1181            return status
1182        elif not killed :
1183            self.Reason = "CUPS backend %s died abnormally." % originalbackend
1184            self.printInfo(self.Reason, "error")
1185            return -1
1186        else :
1187            self.Reason = "CUPS backend %s was killed." % originalbackend
1188            self.printInfo(self.Reason, "warn")
1189            return 1
1190       
1191if __name__ == "__main__" :   
1192    # This is a CUPS backend, we should act and die like a CUPS backend
1193    wrapper = CUPSBackend()
1194    if len(sys.argv) == 1 :
1195        print "\n".join(wrapper.discoverOtherBackends())
1196        sys.exit(0)               
1197    elif len(sys.argv) not in (6, 7) :   
1198        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
1199                              % sys.argv[0])
1200        sys.exit(1)
1201    else :   
1202        try :
1203            wrapper.deferredInit()
1204            wrapper.initBackendParameters()
1205            wrapper.saveDatasAndCheckSum()
1206            wrapper.preaccounter = openAccounter(wrapper, ispreaccounter=1)
1207            wrapper.accounter = openAccounter(wrapper)
1208            wrapper.precomputeJobSize()
1209            wrapper.exportJobInfo() # exports a first time to give hints to external scripts
1210            wrapper.overwriteJobAttributes()
1211            wrapper.exportJobInfo() # re-exports in case it was overwritten
1212            retcode = wrapper.mainWork()
1213        except KeyboardInterrupt :   
1214            wrapper.printInfo(_("Job %s interrupted by the administrator !") % wrapper.JobId, "warn")
1215            retcode = 0
1216        except SystemExit, e :   
1217            retcode = e.code
1218        except :   
1219            try :
1220                wrapper.crashed("cupspykota backend failed")
1221            except :   
1222                crashed("cupspykota backend failed")
1223            retcode = 1
1224        wrapper.clean()
1225        sys.exit(retcode)
Note: See TracBrowser for help on using the browser.