root / pykota / trunk / bin / cupspykota @ 2786

Revision 2786, 59.4 kB (checked in by jerome, 18 years ago)

Combining cupspykota, pknotify and PyKotIcon? now work fine to check user's credentials.

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