root / pykota / trunk / bin / cupspykota @ 2882

Revision 2882, 61.5 kB (checked in by jerome, 18 years ago)

Exports more environment variables.

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