root / pykota / trunk / bin / cupspykota @ 2803

Revision 2803, 60.9 kB (checked in by jerome, 18 years ago)

Changed the behavior of the overwrite_jobticket directive : it's now
impossible to overwrite the username when the job has to be denied
or cancelled (although for cancelled I'm not sure if this is a good
or a bad idea).

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