root / pykota / trunk / bin / cupspykota @ 3008

Revision 3008, 69.2 kB (checked in by jerome, 18 years ago)

Rewrote the locking mechanism.

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