root / pykota / trunk / bin / cupspykota @ 2883

Revision 2883, 62.9 kB (checked in by jerome, 18 years ago)

Added locking to prevent two or more cupspykota processes to handle different
jobs for the same print queue at the very same time.

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