root / pykota / trunk / bin / cupspykota @ 2884

Revision 2884, 63.1 kB (checked in by jerome, 17 years ago)

Exports two additionnal environment variables to subprocesses.

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