root / pykota / trunk / bin / cupspykota @ 2692

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

Added the 'duplicatesdelay' and 'balancezero' directives.

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