root / pykota / trunk / bin / cupspykota @ 2895

Revision 2895, 66.0 kB (checked in by jerome, 18 years ago)

Introduced the 'askconfirmation' directive in pykota.conf

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