root / pykota / trunk / bin / cupspykota @ 2635

Revision 2635, 57.0 kB (checked in by jerome, 18 years ago)

Introduced the 'preaccounter' directive.

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