root / pykota / trunk / bin / cupspykota @ 2961

Revision 2961, 66.7 kB (checked in by jerome, 18 years ago)

Ensures no problem can occur during early cleanup, by moving
data initialization code upwards.

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