root / pykota / trunk / bin / cupspykota @ 2996

Revision 2996, 67.6 kB (checked in by jerome, 18 years ago)

Fixed problem when the cupspykota process waits for the lock
and the admin or the user cancels the job with lprm.

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