root / pykota / trunk / bin / cupspykota @ 2581

Revision 2580, 55.1 kB (checked in by jerome, 19 years ago)

Fixed a missing return statement which caused the original CUPS' backend exit status to be ignored.

  • 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 Jerome Alet <alet@librelogiciel.com>
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22#
23# $Id$
24#
25#
26
27import sys
28import os
29import errno
30import tempfile
31import popen2
32import cStringIO
33import shlex
34import signal
35import md5
36import fnmatch
37import pwd
38import socket
39import smtplib
40
41from pykota.tool import PyKotaTool, PyKotaToolError, crashed
42from pykota.accounter import openAccounter
43from pykota.ipp import IPPRequest, IPPError
44from pykota.storage import PyKotaStorageError
45       
46class CUPSBackend(PyKotaTool) :
47    """Base class for tools with no database access."""
48    def __init__(self) :
49        """Initializes the CUPS backend wrapper."""
50        PyKotaTool.__init__(self)
51        signal.signal(signal.SIGTERM, signal.SIG_IGN)
52        signal.signal(signal.SIGPIPE, signal.SIG_IGN)
53        self.MyName = "PyKota"
54        self.myname = "cupspykota"
55        self.pid = os.getpid()
56       
57    def deferredInit(self) :   
58        """Deferred initialization."""
59        PyKotaTool.deferredInit(self)
60        self.gotSigTerm = 0
61        self.installSigTermHandler()
62       
63    def sigtermHandler(self, signum, frame) :
64        """Sets an attribute whenever SIGTERM is received."""
65        self.gotSigTerm = 1
66        self.printInfo(_("SIGTERM received, job %s cancelled.") % self.JobId)
67        os.environ["PYKOTASTATUS"] = "CANCELLED"
68       
69    def deinstallSigTermHandler(self) :           
70        """Deinstalls the SIGTERM handler."""
71        self.logdebug("Deinstalling SIGTERM handler...")
72        signal.signal(signal.SIGTERM, signal.SIG_IGN)
73        self.logdebug("SIGTERM handler deinstalled.")
74       
75    def installSigTermHandler(self) :           
76        """Installs the SIGTERM handler."""
77        self.logdebug("Installing SIGTERM handler...")
78        signal.signal(signal.SIGTERM, self.sigtermHandler)
79        self.logdebug("SIGTERM handler installed.")
80       
81    def discoverOtherBackends(self) :   
82        """Discovers the other CUPS backends.
83       
84           Executes each existing backend in turn in device enumeration mode.
85           Returns the list of available backends.
86        """
87        # Unfortunately this method can't output any debug information
88        # to stdout or stderr, else CUPS considers that the device is
89        # not available.
90        available = []
91        (directory, myname) = os.path.split(sys.argv[0])
92        if not directory :
93            directory = "./"
94        tmpdir = tempfile.gettempdir()
95        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
96        if os.path.exists(lockfilename) :
97            lockfile = open(lockfilename, "r")
98            pid = int(lockfile.read())
99            lockfile.close()
100            try :
101                # see if the pid contained in the lock file is still running
102                os.kill(pid, 0)
103            except OSError, e :   
104                if e.errno != errno.EPERM :
105                    # process doesn't exist anymore
106                    os.remove(lockfilename)
107           
108        if not os.path.exists(lockfilename) :
109            lockfile = open(lockfilename, "w")
110            lockfile.write("%i" % self.pid)
111            lockfile.close()
112            allbackends = [ os.path.join(directory, b) \
113                                for b in os.listdir(directory) \
114                                    if os.access(os.path.join(directory, b), os.X_OK) \
115                                        and (b != myname)] 
116            for backend in allbackends :                           
117                answer = os.popen(backend, "r")
118                try :
119                    devices = [line.strip() for line in answer.readlines()]
120                except :   
121                    devices = []
122                status = answer.close()
123                if status is None :
124                    for d in devices :
125                        # each line is of the form :
126                        # 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
127                        # so we have to decompose it carefully
128                        fdevice = cStringIO.StringIO(d)
129                        tokenizer = shlex.shlex(fdevice)
130                        tokenizer.wordchars = tokenizer.wordchars + \
131                                                        r".:,?!~/\_$*-+={}[]()#"
132                        arguments = []
133                        while 1 :
134                            token = tokenizer.get_token()
135                            if token :
136                                arguments.append(token)
137                            else :
138                                break
139                        fdevice.close()
140                        try :
141                            (devicetype, device, name, fullname) = arguments
142                        except ValueError :   
143                            pass    # ignore this 'bizarre' device
144                        else :   
145                            if name.startswith('"') and name.endswith('"') :
146                                name = name[1:-1]
147                            if fullname.startswith('"') and fullname.endswith('"') :
148                                fullname = fullname[1:-1]
149                            available.append('%s %s:%s "%s+%s" "%s managed %s"' \
150                                                 % (devicetype, self.myname, \
151                                                    device, self.MyName, \
152                                                    name, self.MyName, \
153                                                    fullname))
154            os.remove(lockfilename)
155        available.append('direct %s:// "%s+Nothing" "%s managed Virtual Printer"' \
156                             % (self.myname, self.MyName, self.MyName))
157        return available
158                       
159    def initBackendParameters(self) :   
160        """Initializes the backend's attributes."""
161        # check that the DEVICE_URI environment variable's value is
162        # prefixed with self.myname otherwise don't touch it.
163        # If this is the case, we have to remove the prefix from
164        # the environment before launching the real backend
165        self.logdebug("Initializing backend...")
166        muststartwith = "%s:" % self.myname
167        device_uri = os.environ.get("DEVICE_URI", "")
168        if device_uri.startswith(muststartwith) :
169            fulldevice_uri = device_uri[:]
170            device_uri = fulldevice_uri[len(muststartwith):]
171            for i in range(2) :
172                if device_uri.startswith("/") : 
173                    device_uri = device_uri[1:]
174        try :
175            (backend, destination) = device_uri.split(":", 1) 
176        except ValueError :   
177            if not device_uri :
178                self.logdebug("Not attached to an existing print queue.")
179                backend = ""
180                printerhostname = ""
181            else :   
182                raise PyKotaToolError, "Invalid DEVICE_URI : %s\n" % device_uri
183        else :       
184            while destination.startswith("/") :
185                destination = destination[1:]
186            checkauth = destination.split("@", 1)   
187            if len(checkauth) == 2 :
188                destination = checkauth[1]
189            printerhostname = destination.split("/")[0].split(":")[0]
190       
191        self.Action = "ALLOW"   # job allowed by default
192        self.Reason = None
193        self.JobId = sys.argv[1].strip()
194        # use CUPS' user when printing test pages from CUPS' web interface
195        self.UserName = sys.argv[2].strip() or pwd.getpwuid(os.geteuid())[0]
196        self.Title = sys.argv[3].strip()
197        self.Copies = int(sys.argv[4].strip())
198        self.Options = sys.argv[5].strip()
199        if len(sys.argv) == 7 :
200            self.InputFile = sys.argv[6] # read job's datas from file
201        else :   
202            self.InputFile = None        # read job's datas from stdin
203           
204        self.PrinterHostName = printerhostname   
205        self.RealBackend = backend
206        self.DeviceURI = device_uri
207        self.PrinterName = os.environ.get("PRINTER", "")
208        self.Directory = self.config.getPrinterDirectory(self.PrinterName)
209        self.DataFile = os.path.join(self.Directory, "%s-%s-%s-%s" % \
210                   (self.myname, self.PrinterName, self.UserName, self.JobId))
211       
212        (ippfilename, ippmessage) = self.parseIPPRequestFile()
213        self.ControlFile = ippfilename
214        john = ippmessage.operation_attributes.get("job-originating-host-name", \
215               ippmessage.job_attributes.get("job-originating-host-name", \
216               (None, None)))
217        if type(john) == type([]) : 
218            john = john[-1]
219        (chtype, self.ClientHost) = john 
220        jbing = ippmessage.job_attributes.get("job-billing", (None, None))
221        if type(jbing) == type([]) : 
222            jbing = jbing[-1]
223        (jbtype, self.JobBillingCode) = jbing
224       
225        self.logdebug("Backend : %s" % self.RealBackend)
226        self.logdebug("DeviceURI : %s" % self.DeviceURI)
227        self.logdebug("Printername : %s" % self.PrinterName)
228        self.logdebug("Username : %s" % self.UserName)
229        self.logdebug("JobId : %s" % self.JobId)
230        self.logdebug("Title : %s" % self.Title)
231        self.logdebug("Filename : %s" % self.InputFile)
232        self.logdebug("Copies : %s" % self.Copies)
233        self.logdebug("Options : %s" % self.Options)
234        self.logdebug("Directory : %s" % self.Directory) 
235        self.logdebug("DataFile : %s" % self.DataFile)
236        self.logdebug("ControlFile : %s" % self.ControlFile)
237        self.logdebug("JobBillingCode : %s" % self.JobBillingCode)
238        self.logdebug("JobOriginatingHostName : %s" % self.ClientHost)
239       
240        self.logdebug("Backend initialized.")
241       
242    def overwriteJobAttributes(self) :
243        """Overwrites some of the job's attributes if needed."""
244        self.logdebug("Sanitizing job's attributes...")
245        # First overwrite the job ticket
246        self.overwriteJobTicket()
247       
248        # do we want to strip out the Samba/Winbind domain name ?
249        separator = self.config.getWinbindSeparator()
250        if separator is not None :
251            self.UserName = self.UserName.split(separator)[-1]
252           
253        # do we want to lowercase usernames ?   
254        if self.config.getUserNameToLower() :
255            self.UserName = self.UserName.lower()
256           
257        # do we want to strip some prefix off of titles ?   
258        stripprefix = self.config.getStripTitle(self.PrinterName)
259        if stripprefix :
260            if fnmatch.fnmatch(self.Title[:len(stripprefix)], stripprefix) :
261                self.logdebug("Prefix [%s] removed from job's title [%s]." \
262                                      % (stripprefix, self.Title))
263                self.Title = self.Title[len(stripprefix):]
264               
265        self.logdebug("Username : %s" % self.UserName)
266        self.logdebug("BillingCode : %s" % self.JobBillingCode)
267        self.logdebug("Title : %s" % self.Title)
268        self.logdebug("Job's attributes sanitizing done.")
269               
270    def overwriteJobTicket(self) :   
271        """Should we overwrite the job's ticket (username and billingcode) ?"""
272        self.logdebug("Checking if we need to overwrite the job ticket...")
273        jobticketcommand = self.config.getOverwriteJobTicket(self.PrinterName)
274        if jobticketcommand is not None :
275            username = billingcode = action = None
276            self.logdebug("Launching subprocess [%s] to overwrite the job ticket." \
277                                     % jobticketcommand)
278            inputfile = os.popen(jobticketcommand, "r")
279            for line in inputfile.xreadlines() :
280                line = line.strip()
281                if line == "DENY" :
282                    self.logdebug("Seen DENY command.")
283                    action = "DENY"
284                elif line.startswith("USERNAME=") :   
285                    username = line.split("=", 1)[1].strip()
286                    self.logdebug("Seen new username [%s]" % username)
287                    action = None
288                elif line.startswith("BILLINGCODE=") :   
289                    billingcode = line.split("=", 1)[1].strip()
290                    self.logdebug("Seen new billing code [%s]" % billingcode)
291                    action = None
292            inputfile.close()   
293           
294            # now overwrite the job's ticket if new data was supplied
295            if action :
296                self.Action = action
297                self.Reason = _("You are not allowed to print at this time.")
298            if username :
299                self.UserName = username
300            # NB : we overwrite the billing code even if empty   
301            self.JobBillingCode = billingcode 
302        self.logdebug("Job ticket overwriting done.")
303           
304    def saveDatasAndCheckSum(self) :
305        """Saves the input datas into a static file."""
306        self.logdebug("Duplicating data stream into %s" % self.DataFile)
307        mustclose = 0
308        outfile = open(self.DataFile, "wb")   
309        if self.InputFile is not None :
310            self.regainPriv()
311            infile = open(self.InputFile, "rb")
312            mustclose = 1
313        else :   
314            infile = sys.stdin
315        CHUNK = 64*1024         # read 64 Kb at a time
316        dummy = 0
317        sizeread = 0
318        checksum = md5.new()
319        while 1 :
320            data = infile.read(CHUNK) 
321            if not data :
322                break
323            sizeread += len(data)   
324            outfile.write(data)
325            checksum.update(data)   
326            if not (dummy % 32) : # Only display every 2 Mb
327                self.logdebug("%s bytes saved..." % sizeread)
328            dummy += 1   
329        if mustclose :   
330            infile.close()
331            self.dropPriv()
332           
333        outfile.close()
334        self.JobSizeBytes = sizeread   
335        self.JobMD5Sum = checksum.hexdigest()
336       
337        self.logdebug("JobSizeBytes : %s" % self.JobSizeBytes)
338        self.logdebug("JobMD5Sum : %s" % self.JobMD5Sum)
339        self.logdebug("Data stream duplicated into %s" % self.DataFile)
340           
341    def clean(self) :
342        """Cleans up the place."""
343        self.logdebug("Cleaning up...")
344        self.deinstallSigTermHandler()
345        if not self.config.getPrinterKeepFiles(self.PrinterName) :
346            try :
347                self.logdebug("Work file %s will be deleted." % self.DataFile)
348            except AttributeError :   
349                pass
350            else :   
351                os.remove(self.DataFile)
352                self.logdebug("Work file %s has been deleted." % self.DataFile)
353        else :   
354            self.logdebug("Work file %s will be kept." % self.DataFile)
355        PyKotaTool.clean(self)   
356        self.logdebug("Clean.")
357           
358    def precomputeJobSize(self) :   
359        """Computes the job size with a software method."""
360        self.logdebug("Precomputing job's size...")
361        jobsize = 0
362        if self.JobSizeBytes :
363            try :
364                from pkpgpdls import analyzer, pdlparser
365            except ImportError :   
366                self.printInfo("pkpgcounter is now distributed separately, please grab it from http://www.librelogiciel.com/software/pkpgcounter/action_Download", "error")
367                self.printInfo("Precomputed job size will be forced to 0 pages.", "error")
368            else :     
369                infile = open(self.DataFile, "rb")
370                try :
371                    parser = analyzer.PDLAnalyzer(infile)
372                    jobsize = parser.getJobSize()
373                except pdlparser.PDLParserError, msg :   
374                    # Here we just log the failure, but
375                    # we finally ignore it and return 0 since this
376                    # computation is just an indication of what the
377                    # job's size MAY be.
378                    self.printInfo(_("Unable to precompute the job's size with the generic PDL analyzer : %s") % msg, "warn")
379                else :   
380                    if self.InputFile is not None :
381                        # when a filename is passed as an argument, the backend
382                        # must generate the correct number of copies.
383                        jobsize *= self.Copies
384                infile.close()       
385        self.softwareJobSize = jobsize
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.externalMailTo(arguments, self.Action, self.User, self.Printer, self.Reason)
807            else :   
808                # TODO : clean this again
809                admin = self.config.getAdmin(self.PrinterName)
810                adminmail = self.config.getAdminMail(self.PrinterName)
811                usermail = self.User.Email or self.User.Name
812                if "@" not in usermail :
813                    usermail = "%s@%s" % (usermail, self.maildomain or self.smtpserver)
814                destination = []
815                if mailto in ("BOTH", "ADMIN") :
816                    destination.append(adminmail)
817                if mailto in ("BOTH", "USER") :   
818                    destination.append(usermail)
819                   
820                fullmessage = self.Reason + (_("\n\nYour system administrator :\n\n\t%s - <%s>\n") % (admin, adminmail))
821                try :   
822                    server = smtplib.SMTP(self.smtpserver)
823                except socket.error, msg :   
824                    self.printInfo(_("Impossible to connect to SMTP server : %s") % msg, "error")
825                else :
826                    try :
827                        server.sendmail(adminmail, destination, \
828                          "From: %s\nTo: %s\nCc: %s\nSubject: %s\n\n%s" \
829                          % (adminmail, usermail, adminmail, _("Print Quota"), fullmessage))
830                    except smtplib.SMTPException, answer :   
831                        for (k, v) in answer.recipients.items() :
832                            self.printInfo(_("Impossible to send mail to %s, error %s : %s") % (k, v[0], v[1]), "error")
833                    server.quit()
834            self.logdebug("Feedback sent to user %s." % self.UserName)
835               
836    def mainWork(self) :   
837        """Main work is done here."""
838        if not self.JobSizeBytes :
839            # if no data to pass to real backend, probably a filter
840            # higher in the chain failed because of a misconfiguration.
841            # we deny the job in this case (nothing to print anyway)
842            self.printInfo(_("Job contains no data. Printing is denied."), "error")
843            return self.removeJob()
844           
845        self.getPrinterUserAndUserPQuota()
846        if self.Policy == "EXTERNALERROR" :
847            # Policy was 'EXTERNAL' and the external command returned an error code
848            return self.removeJob()
849        elif self.Policy == "EXTERNAL" :
850            # Policy was 'EXTERNAL' and the external command wasn't able
851            # to add either the printer, user or user print quota
852            return self.removeJob()
853        elif self.Policy == "DENY" :   
854            # Either printer, user or user print quota doesn't exist,
855            # and the job should be rejected.
856            return self.removeJob()
857        elif self.Policy == "ALLOW" :
858            # ALLOW means : Either printer, user or user print quota doesn't exist,
859            #               but the job should be allowed anyway.
860            self.printInfo(_("Job allowed by printer policy. No accounting will be done."), "warn")
861            return self.printJobDatas()
862        elif self.Policy == "OK" :
863            # OK means : Both printer, user and user print quota exist, job should
864            #            be allowed if current user is allowed to print on this printer
865            return self.doWork()
866        else :   
867            self.printInfo(_("Invalid policy %s for printer %s") % (self.Policy, self.PrinterName), "error")
868            return self.removeJob()
869   
870    def doWork(self) :   
871        """The accounting work is done here."""
872        self.precomputeJobPrice()
873        self.exportUserInfo()
874        self.exportPrinterInfo()
875        self.exportPhaseInfo("BEFORE")
876       
877        if self.Action != "DENY" : 
878            if self.Printer.MaxJobSize and (self.softwareJobSize > self.Printer.MaxJobSize) :
879                # This printer was set to refuse jobs this large.
880                self.printInfo(_("Precomputed job size (%s pages) too large for printer %s.") % (self.softwareJobSize, self.PrinterName), "warn")
881                self.Action = "DENY"
882                # here we don't put the precomputed job size in the message
883                # because in case of error the user could complain :-)
884                self.Reason = _("You are not allowed to print so many pages on printer %s at this time.") % self.PrinterName
885           
886        if self.Action != "DENY" :
887            if self.User.LimitBy == "noprint" :
888                self.printInfo(_("User %s is not allowed to print at this time.") % self.UserName, "warn")
889                self.Action = "DENY"
890                self.Reason = _("Your account settings forbid you to print at this time.")
891               
892        if self.Action != "DENY" :
893            # If printing is still allowed at this time, we
894            # need to extract the billing code information from the database.
895            # No need to do this if the job is denied, this way we
896            # save some database queries.
897            self.getBillingCode()
898           
899        if self.Action != "DENY" :
900            # If printing is still allowed at this time, we
901            # need to check if the job is a dupe or not, and what to do then.
902            # No need to do this if the job is denied, this way we
903            # save some database queries.
904            self.checkIfDupe()
905                   
906        if self.Action != "DENY" :
907            # If printing is still allowed at this time, we
908            # need to check the user's print quota on the current printer.
909            # No need to do this if the job is denied, this way we
910            # save some database queries.
911            if self.User.LimitBy in ('noquota', 'nochange') :
912                self.logdebug("User %s is allowed to print with no limit, no need to check quota." % self.UserName)
913            elif self.Printer.PassThrough :   
914                self.logdebug("Printer %s is in PassThrough mode, no need to check quota." % self.PrinterName)
915            else :
916                self.logdebug("Checking user %s print quota entry on printer %s" \
917                                    % (self.UserName, self.PrinterName))
918                self.Action = self.checkUserPQuota(self.UserPQuota)
919                if self.Action.startswith("POLICY_") :
920                    self.Action = self.Action[7:]
921                if self.Action == "DENY" :
922                    self.printInfo(_("Print Quota exceeded for user %s on printer %s") % (self.UserName, self.PrinterName))
923                    self.Reason = self.config.getHardWarn(self.PrinterName)
924                elif self.Action == "WARN" :   
925                    self.printInfo(_("Print Quota low for user %s on printer %s") % (self.UserName, self.PrinterName))
926                    if self.User.LimitBy and (self.User.LimitBy.lower() == "balance") : 
927                        self.Reason = self.config.getPoorWarn()
928                    else :     
929                        self.Reason = self.config.getSoftWarn(self.PrinterName)
930           
931        # exports some new environment variables
932        self.exportReason()
933       
934        # now tell the user if he needs to know something
935        self.tellUser()
936       
937        # launches the pre hook
938        self.launchPreHook()
939       
940        # handle starting banner pages without accounting
941        self.BannerSize = 0
942        accountbanner = self.config.getAccountBanner(self.PrinterName)
943        if accountbanner in ["ENDING", "NONE"] :
944            self.handleBanner("starting", 0)
945       
946        if self.Action == "DENY" :
947            self.printInfo(_("Job denied, no accounting will be done."))
948        else :
949            self.printInfo(_("Job accounting begins."))
950            self.deinstallSigTermHandler()
951            self.accounter.beginJob(self.Printer)
952            self.installSigTermHandler()
953       
954        # handle starting banner pages with accounting
955        if accountbanner in ["STARTING", "BOTH"] :
956            if not self.gotSigTerm :
957                self.handleBanner("starting", 1)
958       
959        # pass the job's data to the real backend   
960        if (not self.gotSigTerm) and (self.Action in ["ALLOW", "WARN"]) :
961            retcode = self.printJobDatas()
962        else :       
963            retcode = self.removeJob()
964       
965        # indicate phase change
966        self.exportPhaseInfo("AFTER")
967       
968        # handle ending banner pages with accounting
969        if accountbanner in ["ENDING", "BOTH"] :
970            if not self.gotSigTerm :
971                self.handleBanner("ending", 1)
972       
973        # stops accounting
974        if self.Action == "DENY" :
975            self.printInfo(_("Job denied, no accounting has been done."))
976        else :
977            self.deinstallSigTermHandler()
978            self.accounter.endJob(self.Printer)
979            self.installSigTermHandler()
980            self.printInfo(_("Job accounting ends."))
981       
982        # Do all these database changes within a single transaction   
983        # NB : we don't enclose ALL the changes within a single transaction
984        # because while waiting for the printer to answer its internal page
985        # counter, we would open the door to accounting problems for other
986        # jobs launched by the same user at the same time on other printers.
987        # All the code below doesn't take much time, so it's fine.
988        self.storage.beginTransaction()
989        try :
990            # retrieve the job size   
991            if self.Action == "DENY" :
992                self.JobSize = 0
993                self.printInfo(_("Job size forced to 0 because printing is denied."))
994            else :   
995                self.UserPQuota.resetDenyBannerCounter()
996                self.JobSize = self.accounter.getJobSize(self.Printer)
997                self.sanitizeJobSize()
998                self.JobSize += self.BannerSize
999            self.printInfo(_("Job size : %i") % self.JobSize)
1000           
1001            if (self.User.LimitBy == "nochange") or self.Printer.PassThrough :
1002                # no need to update the quota for the current user on this printer
1003                self.printInfo(_("User %s's quota on printer %s won't be modified") % (self.UserName, self.PrinterName))
1004                self.JobPrice = self.UserPQuota.computeJobPrice(self.JobSize)
1005            else :
1006                # update the quota for the current user on this printer
1007                self.printInfo(_("Updating user %s's quota on printer %s") % (self.UserName, self.PrinterName))
1008                self.JobPrice = self.UserPQuota.increasePagesUsage(self.JobSize)
1009           
1010            # adds the current job to history   
1011            self.Printer.addJobToHistory(self.JobId, self.User, self.accounter.getLastPageCounter(), \
1012                                    self.Action, self.JobSize, self.JobPrice, self.InputFile, \
1013                                    self.Title, self.Copies, self.Options, self.ClientHost, \
1014                                    self.JobSizeBytes, self.JobMD5Sum, None, self.JobBillingCode, \
1015                                    self.softwareJobSize, self.softwareJobPrice)
1016            self.printInfo(_("Job added to history."))
1017           
1018            if hasattr(self, "BillingCode") and self.BillingCode and self.BillingCode.Exists :
1019                self.BillingCode.consume(self.JobSize, self.JobPrice)
1020                self.printInfo(_("Billing code %s was updated.") % self.BillingCode.BillingCode)
1021        except :   
1022            self.storage.rollbackTransaction()
1023            raise
1024        else :   
1025            self.storage.commitTransaction()
1026           
1027        # exports some new environment variables
1028        self.exportJobSizeAndPrice()
1029       
1030        # then re-export user information with new values
1031        self.exportUserInfo()
1032       
1033        # handle ending banner pages without accounting
1034        if accountbanner in ["STARTING", "NONE"] :
1035            self.handleBanner("ending", 0)
1036                   
1037        self.launchPostHook()
1038           
1039        return retcode   
1040               
1041    def printJobDatas(self) :           
1042        """Sends the job's datas to the real backend."""
1043        self.logdebug("Sending job's datas to real backend...")
1044        if self.InputFile is None :
1045            infile = open(self.DataFile, "rb")
1046        else :   
1047            infile = None
1048        retcode = self.runOriginalBackend(infile)
1049        if self.InputFile is None :
1050            infile.close()
1051        self.logdebug("Job's datas sent to real backend.")
1052        return retcode
1053       
1054    def runOriginalBackend(self, filehandle=None, isBanner=0) :
1055        """Launches the original backend."""
1056        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
1057        if not isBanner :
1058            arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:]
1059        else :   
1060            # For banners, we absolutely WANT
1061            # to remove any filename from the command line !
1062            self.logdebug("It looks like we try to print a banner.")
1063            arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:6]
1064        arguments[2] = self.UserName # in case it was overwritten by external script
1065        # TODO : do something about job-billing option, in case it was overwritten as well...
1066       
1067        self.logdebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in arguments])))
1068        self.regainPriv()   
1069        pid = os.fork()
1070        self.logdebug("Forked !")
1071        if pid == 0 :
1072            if filehandle is not None :
1073                self.logdebug("Redirecting file handle to real backend's stdin")
1074                os.dup2(filehandle.fileno(), 0)
1075            try :
1076                self.logdebug("Calling execve...")
1077                os.execve(originalbackend, arguments, os.environ)
1078            except OSError, msg :
1079                self.logdebug("execve() failed: %s" % msg)
1080            self.logdebug("We shouldn't be there !!!")   
1081            os._exit(-1)
1082        self.dropPriv()   
1083       
1084        self.logdebug("Waiting for original backend to exit...")   
1085        killed = 0
1086        status = -1
1087        while status == -1 :
1088            try :
1089                status = os.waitpid(pid, 0)[1]
1090            except OSError, (err, msg) :
1091                if (err == 4) and self.gotSigTerm :
1092                    os.kill(pid, signal.SIGTERM)
1093                    killed = 1
1094                   
1095        if os.WIFEXITED(status) :
1096            status = os.WEXITSTATUS(status)
1097            if status :
1098                level = "error"
1099            else :   
1100                level = "info"
1101            self.printInfo("CUPS backend %s returned %d." % \
1102                                     (originalbackend, status), level)
1103            return status
1104        elif not killed :
1105            self.printInfo("CUPS backend %s died abnormally." % \
1106                               originalbackend, "error")
1107            return -1
1108        else :
1109            self.printInfo("CUPS backend %s was killed." % \
1110                               originalbackend, "warn")
1111            return 1
1112       
1113if __name__ == "__main__" :   
1114    # This is a CUPS backend, we should act and die like a CUPS backend
1115    wrapper = CUPSBackend()
1116    if len(sys.argv) == 1 :
1117        print "\n".join(wrapper.discoverOtherBackends())
1118        sys.exit(0)               
1119    elif len(sys.argv) not in (6, 7) :   
1120        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
1121                              % sys.argv[0])
1122        sys.exit(1)
1123    else :   
1124        try :
1125            wrapper.deferredInit()
1126            wrapper.initBackendParameters()
1127            wrapper.saveDatasAndCheckSum()
1128            wrapper.accounter = openAccounter(wrapper)
1129            wrapper.precomputeJobSize()
1130            wrapper.exportJobInfo() # exports a first time to give hints to external scripts
1131            wrapper.overwriteJobAttributes()
1132            wrapper.exportJobInfo() # re-exports in case it was overwritten
1133            retcode = wrapper.mainWork()
1134        except SystemExit, e :   
1135            retcode = e.code
1136        except :   
1137            try :
1138                wrapper.crashed("cupspykota backend failed")
1139            except :   
1140                crashed("cupspykota backend failed")
1141            retcode = 1
1142        wrapper.clean()
1143        sys.exit(retcode)
Note: See TracBrowser for help on using the browser.