root / pykota / trunk / bin / cupspykota @ 2891

Revision 2891, 64.5 kB (checked in by jerome, 18 years ago)

Now uses pkipplib if present.

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