root / pykota / trunk / bin / cupspykota @ 3040

Revision 3040, 69.0 kB (checked in by jerome, 18 years ago)

Removed parts of the locking code incompatible with a multi-server
configuration in which all servers share the same locking directory.

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