root / pykota / trunk / bin / cupspykota @ 2886

Revision 2886, 63.4 kB (checked in by jerome, 18 years ago)

Transformed the per print queue lock into a per backend lock.

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