root / pykota / trunk / bin / cupspykota @ 2885

Revision 2885, 63.1 kB (checked in by jerome, 17 years ago)

Improved the locking stuff.

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