root / pykota / trunk / bin / cupspykota @ 3288

Revision 3288, 67.0 kB (checked in by jerome, 16 years ago)

Moved all exceptions definitions to a dedicated module.

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