root / pykota / trunk / bin / cupspykota @ 2797

Revision 2797, 60.9 kB (checked in by jerome, 18 years ago)

Now uses the 'mailto' directive when user doesn't exist or doesn't have a print quota entry,
or when printer doesn't exist. NB : UNTESTED !

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