root / pykota / trunk / bin / cupspykota @ 2636

Revision 2636, 57.2 kB (checked in by jerome, 18 years ago)

Now uses correct encoding for message's body. TODO : message subject, then similar code in pykota/tool.py

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