root / pykota / trunk / bin / cupspykota @ 2829

Revision 2829, 61.1 kB (checked in by jerome, 19 years ago)

Did a pass with pylint.

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