root / pykota / trunk / bin / cupspykota @ 3294

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

Added modules to store utility functions and application
intialization code, which has nothing to do in classes.
Modified tool.py accordingly (far from being finished)
Use these new modules where necessary.
Now converts all command line arguments to unicode before
beginning to work. Added a proper logging method for already
encoded query strings.

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