root / pykota / trunk / bin / cupspykota @ 2687

Revision 2687, 57.2 kB (checked in by jerome, 19 years ago)

When an user is in 'nochange' mode, the price of a job should be set to 0.0
otherwise pkinvoice would invoice jobs which didn't impact the user's account
balance in reality.

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