Changeset 2409

Show
Ignore:
Timestamp:
08/28/05 12:45:51 (19 years ago)
Author:
jerome
Message:

The cupspykota backend was rewritten from scratch. MacOSX servers should
work just fine now.
Severity : High. Testers MORE than welcome :-)

Location:
pykota/trunk
Files:
9 modified

Legend:

Unmodified
Added
Removed
  • pykota/trunk/bin/cupspykota

    r2398 r2409  
    3232import cStringIO 
    3333import shlex 
    34 import select 
    3534import signal 
    3635import time 
     36import md5 
     37import fnmatch 
    3738 
    38 from pykota.tool import PyKotaFilterOrBackend, PyKotaToolError, crashed 
    39 from pykota.config import PyKotaConfigError 
     39from pykota.tool import PyKotaTool, PyKotaToolError, crashed 
     40from pykota.accounter import openAccounter 
     41from pykota.ipp import IPPRequest, IPPError 
    4042from pykota.storage import PyKotaStorageError 
    41 from pykota.accounter import PyKotaAccounterError 
    42 from pykota.ipp import IPPRequest, IPPError 
    43      
    44 class PyKotaPopen4(popen2.Popen4) : 
    45     """Our own class to execute real backends. 
    46      
    47        Their first argument is different from their path so using 
    48        native popen2.Popen3 would not be feasible. 
    49     """ 
    50     def __init__(self, cmd, bufsize=-1, arg0=None) : 
    51         self.arg0 = arg0 
    52         popen2.Popen4.__init__(self, cmd, bufsize) 
    53          
    54     def _run_child(self, cmd): 
    55         try : 
    56             MAXFD = os.sysconf("SC_OPEN_MAX") 
    57         except (AttributeError, ValueError) :     
    58             MAXFD = 256 
    59         for i in range(3, MAXFD) :  
    60             try: 
    61                 os.close(i) 
    62             except OSError: 
    63                 pass 
    64         try: 
    65             os.execvpe(cmd[0], [self.arg0 or cmd[0]] + cmd[1:], os.environ) 
    66         finally: 
    67             os._exit(1) 
    68      
    69 class PyKotaBackend(PyKotaFilterOrBackend) :         
    70     """A class for the pykota backend.""" 
    71     def acceptJob(self) :         
    72         """Returns the appropriate exit code to tell CUPS all is OK.""" 
    73         return 0 
    74              
    75     def removeJob(self) :             
    76         """Returns the appropriate exit code to let CUPS think all is OK. 
    77          
    78            Returning 0 (success) prevents CUPS from stopping the print queue. 
    79         """    
    80         return 0 
    81          
    82     def genBanner(self, bannerfileorcommand) : 
    83         """Reads a banner or generates one through an external command. 
    84          
    85            Returns the banner's content in a format which MUST be accepted 
    86            by the printer. 
     43         
     44class CUPSBackend(PyKotaTool) : 
     45    """Base class for tools with no database access.""" 
     46    def __init__(self) : 
     47        """Initializes the CUPS backend wrapper.""" 
     48        PyKotaTool.__init__(self) 
     49        signal.signal(signal.SIGTERM, signal.SIG_IGN) 
     50        signal.signal(signal.SIGPIPE, signal.SIG_IGN) 
     51        self.MyName = "PyKota" 
     52        self.myname = "cupspykota" 
     53        self.pid = os.getpid() 
     54         
     55    def deferredInit(self) :     
     56        """Deferred initialization.""" 
     57        PyKotaTool.deferredInit(self) 
     58        self.gotSigTerm = 0 
     59        self.installSigTermHandler() 
     60         
     61    def sigtermHandler(self, signum, frame) : 
     62        """Sets an attribute whenever SIGTERM is received.""" 
     63        self.gotSigTerm = 1 
     64        self.printInfo(_("SIGTERM received, job %s cancelled.") % self.JobId) 
     65        os.environ["PYKOTASTATUS"] = "CANCELLED" 
     66         
     67    def deinstallSigTermHandler(self) :            
     68        """Deinstalls the SIGTERM handler.""" 
     69        self.logdebug("Deinstalling SIGTERM handler...") 
     70        signal.signal(signal.SIGTERM, signal.SIG_IGN) 
     71        self.logdebug("SIGTERM handler deinstalled.") 
     72         
     73    def installSigTermHandler(self) :            
     74        """Installs the SIGTERM handler.""" 
     75        self.logdebug("Installing SIGTERM handler...") 
     76        signal.signal(signal.SIGTERM, self.sigtermHandler) 
     77        self.logdebug("SIGTERM handler installed.") 
     78         
     79    def discoverOtherBackends(self) :     
     80        """Discovers the other CUPS backends. 
     81         
     82           Executes each existing backend in turn in device enumeration mode. 
     83           Returns the list of available backends. 
    8784        """ 
    88         if bannerfileorcommand : 
    89             banner = "" # no banner by default 
    90             if os.access(bannerfileorcommand, os.X_OK) or not os.path.isfile(bannerfileorcommand) : 
    91                 self.logdebug("Launching %s to generate a banner." % bannerfileorcommand) 
    92                 child = popen2.Popen3(bannerfileorcommand, capturestderr=1) 
    93                 banner = child.fromchild.read() 
    94                 child.tochild.close() 
    95                 child.childerr.close() 
    96                 child.fromchild.close() 
    97                 status = child.wait() 
    98                 if os.WIFEXITED(status) : 
    99                     status = os.WEXITSTATUS(status) 
    100                 self.printInfo(_("Banner generator %s exit code is %s") % (bannerfileorcommand, str(status))) 
    101             else : 
    102                 self.logdebug("Using %s as the banner." % bannerfileorcommand) 
    103                 try : 
    104                     fh = open(bannerfileorcommand, 'r') 
    105                 except IOError, msg :     
    106                     self.printInfo("Impossible to open %s : %s" % (bannerfileorcommand, msg), "error") 
    107                 else :     
    108                     banner = fh.read() 
    109                     fh.close() 
    110             if banner :         
    111                 return cStringIO.StringIO(banner) 
    112      
    113     def startingBanner(self, printername) : 
    114         """Retrieves a starting banner for current printer and returns its content.""" 
    115         self.logdebug("Retrieving starting banner...") 
    116         return self.genBanner(self.config.getStartingBanner(printername)) 
    117      
    118     def endingBanner(self, printername) : 
    119         """Retrieves an ending banner for current printer and returns its content.""" 
    120         self.logdebug("Retrieving ending banner...") 
    121         return self.genBanner(self.config.getEndingBanner(printername)) 
    122          
    123     def getCupsConfigDirectives(self, directives=[]) : 
    124         """Retrieves some CUPS directives from its configuration file. 
    125          
    126            Returns a mapping with lowercased directives as keys and  
    127            their setting as values. 
    128         """ 
    129         dirvalues = {}  
    130         cupsroot = os.environ.get("CUPS_SERVERROOT", "/etc/cups") 
    131         cupsdconf = os.path.join(cupsroot, "cupsd.conf") 
    132         try : 
    133             conffile = open(cupsdconf, "r") 
    134         except IOError :     
    135             self.logdebug("Unable to open %s" % cupsdconf) 
    136         else :     
    137             for line in conffile.readlines() : 
    138                 linecopy = line.strip().lower() 
    139                 for di in [d.lower() for d in directives] : 
    140                     if linecopy.startswith("%s " % di) : 
    141                         try : 
    142                             val = line.split()[1] 
    143                         except :     
    144                             pass # ignore errors, we take the last value in any case. 
    145                         else :     
    146                             dirvalues[di] = val 
    147             conffile.close()             
    148         return dirvalues        
    149              
    150     def getJobInfosFromPageLog(self, cupsconfig, printername, username, jobid) : 
    151         """Retrieves the job-originating-hostname and job-billing attributes from the CUPS page_log file if possible.""" 
    152         pagelogpath = cupsconfig.get("pagelog", "/var/log/cups/page_log") 
    153         self.logdebug("Trying to extract job-originating-host-name from %s" % pagelogpath) 
    154         try : 
    155             pagelog = open(pagelogpath, "r") 
    156         except IOError :     
    157             self.logdebug("Unable to open %s" % pagelogpath) 
    158             return (None, None) # no page log or can't read it, originating hostname unknown yet 
    159         else :     
    160             # TODO : read backward so we could take first value seen 
    161             # TODO : here we read forward so we must take the last value seen 
    162             prefix = ("%s %s %s " % (printername, username, jobid)).lower() 
    163             matchingline = None 
    164             while 1 : 
    165                 line = pagelog.readline() 
    166                 if not line : 
    167                     break 
    168                 else : 
    169                     line = line.strip() 
    170                     if line.lower().startswith(prefix) :     
    171                         matchingline = line # no break, because we read forward 
    172             pagelog.close()         
    173             if matchingline is None : 
    174                 self.logdebug("No matching line found in %s" % pagelogpath) 
    175                 return (None, None) # correct line not found, job-originating-host-name unknown 
    176             else :     
    177                 (jobbilling, hostname) = matchingline.split()[-2:] 
    178                 if jobbilling == "-" : 
    179                     jobbilling = "" 
    180                 return (jobbilling, hostname)    
    181                  
    182     def extractDatasFromCups(self) :             
    183         """Extract datas from CUPS IPP message or page_log file.""" 
    184         # tries to extract job-originating-host-name and other information 
    185         self.regainPriv() 
    186         cupsdconf = self.getCupsConfigDirectives(["PageLog", "RequestRoot"]) 
    187         requestroot = cupsdconf.get("requestroot", "/var/spool/cups") 
    188         if (len(self.jobid) < 5) and self.jobid.isdigit() : 
    189             ippmessagefile = "c%05i" % int(self.jobid) 
    190         else :     
    191             ippmessagefile = "c%s" % self.jobid 
    192         ippmessagefile = os.path.join(requestroot, ippmessagefile) 
    193         ippmessage = {} 
    194         try : 
    195             ippdatafile = open(ippmessagefile) 
    196         except :     
    197             self.printInfo("Unable to open IPP message file %s" % ippmessagefile, "warn") 
    198         else :     
    199             self.logdebug("Parsing of IPP message file %s begins." % ippmessagefile) 
    200             try : 
    201                 ippmessage = IPPRequest(ippdatafile.read()) 
    202                 ippmessage.parse() 
    203             except IPPError, msg :     
    204                 self.printInfo("Error while parsing %s : %s" % (ippmessagefile, msg), "warn") 
    205             else :     
    206                 self.logdebug("Parsing of IPP message file %s ends." % ippmessagefile) 
    207             ippdatafile.close() 
    208         self.dropPriv()     
    209          
    210         try : 
    211             john = ippmessage.operation_attributes.get("job-originating-host-name", \ 
    212                    ippmessage.job_attributes.get("job-originating-host-name", (None, None))) 
    213             if type(john) == type([]) :                           
    214                 john = john[-1] 
    215             (chtype, clienthost) = john                           
    216             (jbtype, bcode) = ippmessage.job_attributes.get("job-billing", (None, None)) 
    217         except AttributeError :     
    218             clienthost = None 
    219             bcode = None 
    220         if clienthost is None : 
    221             # TODO : in case the job ticket is overwritten later, self.username is not the correct one. 
    222             # TODO : doesn't matter much, since this code is only used as a last resort. 
    223             (bcode, clienthost) = self.getJobInfosFromPageLog(cupsdconf, self.printername, self.username, self.jobid) 
    224         self.logdebug("Client Hostname : %s" % (clienthost or "Unknown"))     
    225         self.clientHostname = clienthost 
    226         self.initialBillingCode = bcode 
    227         os.environ["PYKOTAJOBORIGINATINGHOSTNAME"] = str(self.clientHostname or "") 
    228          
    229     def doWork(self, policy, printer, user, userpquota) :     
    230         """Most of the work is done here.""" 
    231         # Two different values possible for policy here : 
    232         # ALLOW means : Either printer, user or user print quota doesn't exist, 
    233         #               but the job should be allowed anyway. 
    234         # OK means : Both printer, user and user print quota exist, job should 
    235         #            be allowed if current user is allowed to print on this printer 
    236         if policy == "OK" : 
    237             # exports user information with initial values 
    238             self.exportUserInfo(userpquota) 
    239              
    240             bcode = self.overwrittenBillingCode or self.initialBillingCode 
    241             self.logdebug("Billing Code : %s" % (bcode or "None"))     
    242              
    243             os.environ["PYKOTAJOBBILLING"] = str(bcode or "") 
    244              
    245             # enters first phase 
    246             os.environ["PYKOTAPHASE"] = "BEFORE" 
    247              
    248             # precomputes the job's price 
    249             self.softwareJobPrice = userpquota.computeJobPrice(self.softwareJobSize) 
    250             os.environ["PYKOTAPRECOMPUTEDJOBPRICE"] = str(self.softwareJobPrice) 
    251             self.logdebug("Precomputed job's size is %s pages, price is %s units" % (self.softwareJobSize, self.softwareJobPrice)) 
    252              
    253             denyduplicates = self.config.getDenyDuplicates(printer.Name) 
    254             if not self.jobSizeBytes : 
    255                 # if no data to pass to real backend, probably a filter 
    256                 # higher in the chain failed because of a misconfiguration. 
    257                 # we deny the job in this case (nothing to print anyway) 
    258                 self.printMoreInfo(user, printer, _("Job contains no data. Printing is denied."), "warn") 
    259                 action = "DENY" 
    260             elif denyduplicates \ 
    261                  and printer.LastJob.Exists \ 
    262                  and (printer.LastJob.UserName == user.Name) \ 
    263                  and (printer.LastJob.JobMD5Sum == self.checksum) : 
    264                 # TODO : use the current user's last job instead of   
    265                 # TODO : the current printer's last job. This would be 
    266                 # TODO : better but requires an additional database query 
    267                 # TODO : with SQL, and is much more complex with the  
    268                 # TODO : actual LDAP schema. Maybe this is not very 
    269                 # TODO : important, because usually dupes are rapidly sucessive. 
    270                 if denyduplicates == 1 : 
    271                     self.printMoreInfo(user, printer, _("Job is a duplicate. Printing is denied."), "warn") 
    272                     action = "DENY" 
    273                 else :     
    274                     self.logdebug("Launching subprocess [%s] to see if dupes should be allowed or not." % denyduplicates) 
    275                     fanswer = os.popen(denyduplicates, "r") 
    276                     action = fanswer.read().strip().upper() 
    277                     fanswer.close() 
    278                     if action == "DENY" :      
    279                         self.printMoreInfo(user, printer, _("Job is a duplicate. Printing is denied by subprocess."), "warn") 
    280                     else :     
    281                         self.printMoreInfo(user, printer, _("Job is a duplicate. Printing is allowed by subprocess."), "warn") 
    282                         action = self.warnUserPQuota(userpquota) 
    283             else :     
    284                 # checks the user's quota 
    285                 action = self.warnUserPQuota(userpquota) 
    286              
    287             # Now handle the billing code 
    288             if bcode is not None : 
    289                 self.logdebug("Checking billing code [%s]." % bcode) 
    290                 billingcode = self.storage.getBillingCode(bcode) 
    291                 if billingcode.Exists : 
    292                     self.logdebug("Billing code [%s] exists in database." % bcode) 
    293                 else : 
    294                     msg = "Unknown billing code [%s] : " % bcode 
    295                     (newaction, script) = self.config.getUnknownBillingCode(printer.Name) 
    296                     if newaction == "CREATE" : 
    297                         self.logdebug(msg + "will be created.") 
    298                         billingcode = self.storage.addBillingCode(bcode) 
    299                         if billingcode.Exists : 
    300                             self.logdebug(msg + "has been created.") 
    301                         else :     
    302                             self.printInfo(msg + "couldn't be created.", "error") 
    303                     else :     
    304                         self.logdebug(msg + "job will be denied.") 
    305                         action = newaction 
    306                         if script is not None :  
    307                             self.logdebug(msg + "launching subprocess [%s] to notify user." % script) 
    308                             os.system(script) 
    309             else :     
    310                 billingcode = None 
    311              
    312             # Should we cancel the job in any case (because job ticket 
    313             # was overwritten) ? 
    314             if self.mustDeny : 
    315                 action = "DENY" 
    316                  
    317             # exports some new environment variables 
    318             os.environ["PYKOTAACTION"] = action 
    319              
    320             # launches the pre hook 
    321             self.prehook(userpquota) 
    322  
    323             # saves the size of banners which have to be accounted for 
    324             # this is needed in the case of software accounting 
    325             bannersize = 0 
    326              
    327             # handle starting banner pages before accounting 
    328             accountbanner = self.config.getAccountBanner(printer.Name) 
    329             if accountbanner in ["ENDING", "NONE"] : 
    330                 if (action == 'DENY') and (userpquota.WarnCount >= self.config.getMaxDenyBanners(printer.Name)) : 
    331                     self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn") 
    332                 else : 
    333                     if action == 'DENY' : 
    334                         self.logdebug("Incrementing the number of deny banners for user %s on printer %s" % (user.Name, printer.Name)) 
    335                         userpquota.incDenyBannerCounter() # increments the warning counter 
    336                         self.exportUserInfo(userpquota) 
    337                     banner = self.startingBanner(printer.Name) 
    338                     if banner : 
    339                         self.logdebug("Printing starting banner before accounting begins.") 
    340                         self.handleData(banner) 
    341   
    342             self.printMoreInfo(user, printer, _("Job accounting begins.")) 
    343             self.accounter.beginJob(printer) 
    344              
    345             # handle starting banner pages during accounting 
    346             if accountbanner in ["STARTING", "BOTH"] : 
    347                 if (action == 'DENY') and (userpquota.WarnCount >= self.config.getMaxDenyBanners(printer.Name)) : 
    348                     self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn") 
    349                 else : 
    350                     if action == 'DENY' : 
    351                         self.logdebug("Incrementing the number of deny banners for user %s on printer %s" % (user.Name, printer.Name)) 
    352                         userpquota.incDenyBannerCounter() # increments the warning counter 
    353                         self.exportUserInfo(userpquota) 
    354                     banner = self.startingBanner(printer.Name) 
    355                     if banner : 
    356                         self.logdebug("Printing starting banner during accounting.") 
    357                         self.handleData(banner) 
    358                         if self.accounter.isSoftware : 
    359                             bannersize += 1 # TODO : fix this by passing the banner's content through PDLAnalyzer 
    360         else :     
    361             action = "ALLOW" 
    362             os.environ["PYKOTAACTION"] = action 
    363              
    364         # pass the job's data to the real backend     
    365         if action in ["ALLOW", "WARN"] : 
    366             if self.gotSigTerm : 
    367                 retcode = self.removeJob() 
    368             else :     
    369                 retcode = self.handleData()         
    370         else :         
    371             retcode = self.removeJob() 
    372          
    373         if policy == "OK" :         
    374             # indicate phase change 
    375             os.environ["PYKOTAPHASE"] = "AFTER" 
    376              
    377             # handle ending banner pages during accounting 
    378             if accountbanner in ["ENDING", "BOTH"] : 
    379                 if (action == 'DENY') and (userpquota.WarnCount >= self.config.getMaxDenyBanners(printer.Name)) : 
    380                     self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn") 
    381                 else : 
    382                     if action == 'DENY' : 
    383                         self.logdebug("Incrementing the number of deny banners for user %s on printer %s" % (user.Name, printer.Name)) 
    384                         userpquota.incDenyBannerCounter() # increments the warning counter 
    385                         self.exportUserInfo(userpquota) 
    386                     banner = self.endingBanner(printer.Name) 
    387                     if banner : 
    388                         self.logdebug("Printing ending banner during accounting.") 
    389                         self.handleData(banner) 
    390                         if self.accounter.isSoftware : 
    391                             bannersize += 1 # TODO : fix this by passing the banner's content through PDLAnalyzer 
    392   
    393             # stops accounting.  
    394             self.accounter.endJob(printer) 
    395             self.printMoreInfo(user, printer, _("Job accounting ends.")) 
    396                  
    397             # retrieve the job size     
    398             if action == "DENY" : 
    399                 jobsize = 0 
    400                 self.printMoreInfo(user, printer, _("Job size forced to 0 because printing is denied.")) 
    401             else :     
    402                 userpquota.resetDenyBannerCounter() 
    403                 jobsize = self.accounter.getJobSize(printer) 
    404                 if self.softwareJobSize and (jobsize != self.softwareJobSize) : 
    405                     self.printInfo(_("Beware : computed job size (%s) != precomputed job size (%s)") % (jobsize, self.softwareJobSize), "error") 
    406                     (limit, replacement) = self.config.getTrustJobSize(printer.Name) 
    407                     if limit is None : 
    408                         self.printInfo(_("The job size will be trusted anyway according to the 'trustjobsize' directive"), "warn") 
    409                     else : 
    410                         if jobsize <= limit : 
    411                             self.printInfo(_("The job size will be trusted because it is inferior to the 'trustjobsize' directive's limit %s") % limit, "warn") 
    412                         else : 
    413                             self.printInfo(_("The job size will be modified according to the 'trustjobsize' directive : %s") % replacement, "warn") 
    414                             if replacement == "PRECOMPUTED" : 
    415                                 jobsize = self.softwareJobSize 
    416                             else :     
    417                                 jobsize = replacement 
    418                 jobsize += bannersize     
    419             self.printMoreInfo(user, printer, _("Job size : %i") % jobsize) 
    420              
    421             # update the quota for the current user on this printer  
    422             self.printInfo(_("Updating user %s's quota on printer %s") % (user.Name, printer.Name)) 
    423             jobprice = userpquota.increasePagesUsage(jobsize) 
    424              
    425             # adds the current job to history     
    426             printer.addJobToHistory(self.jobid, user, self.accounter.getLastPageCounter(), \ 
    427                                     action, jobsize, jobprice, self.preserveinputfile, \ 
    428                                     self.title, self.copies, self.options, self.clientHostname, \ 
    429                                     self.jobSizeBytes, self.checksum, None, bcode) 
    430             self.printMoreInfo(user, printer, _("Job added to history.")) 
    431              
    432             if billingcode and billingcode.Exists : 
    433                 billingcode.consume(jobsize, jobprice) 
    434                 self.printMoreInfo(user, printer, _("Billing code %s was updated.") % billingcode.BillingCode) 
    435                  
    436             # exports some new environment variables 
    437             os.environ["PYKOTAJOBSIZE"] = str(jobsize) 
    438             os.environ["PYKOTAJOBPRICE"] = str(jobprice) 
    439              
    440             # then re-export user information with new value 
    441             self.exportUserInfo(userpquota) 
    442              
    443             # handle ending banner pages after accounting ends 
    444             if accountbanner in ["STARTING", "NONE"] : 
    445                 if (action == 'DENY') and (userpquota.WarnCount >= self.config.getMaxDenyBanners(printer.Name)) : 
    446                     self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn") 
    447                 else : 
    448                     if action == 'DENY' : 
    449                         self.logdebug("Incrementing the number of deny banners for user %s on printer %s" % (user.Name, printer.Name)) 
    450                         userpquota.incDenyBannerCounter() # increments the warning counter 
    451                         self.exportUserInfo(userpquota) 
    452                     banner = self.endingBanner(printer.Name) 
    453                     if banner : 
    454                         self.logdebug("Printing ending banner after accounting ends.") 
    455                         self.handleData(banner) 
    456                          
    457             # Launches the post hook 
    458             self.posthook(userpquota) 
    459              
    460         return retcode     
    461                 
    462     def unregisterFileNo(self, pollobj, fileno) :                 
    463         """Removes a file handle from the polling object.""" 
    464         try : 
    465             pollobj.unregister(fileno) 
    466         except KeyError :     
    467             self.printInfo(_("File number %s unregistered twice from polling object, ignored.") % fileno, "warn") 
    468         except :     
    469             self.logdebug("Error while unregistering file number %s from polling object." % fileno) 
    470         else :     
    471             self.logdebug("File number %s unregistered from polling object." % fileno) 
    472              
    473     def formatFileEvent(self, fd, mask) :         
    474         """Formats file debug info.""" 
    475         maskval = [] 
    476         if mask & select.POLLIN : 
    477             maskval.append("POLLIN") 
    478         if mask & select.POLLOUT : 
    479             maskval.append("POLLOUT") 
    480         if mask & select.POLLPRI : 
    481             maskval.append("POLLPRI") 
    482         if mask & select.POLLERR : 
    483             maskval.append("POLLERR") 
    484         if mask & select.POLLHUP : 
    485             maskval.append("POLLHUP") 
    486         if mask & select.POLLNVAL : 
    487             maskval.append("POLLNVAL") 
    488         return "%s (%s)" % (fd, " | ".join(maskval)) 
    489          
    490     def handleData(self, filehandle=None) : 
    491         """Pass the job's data to the real backend.""" 
    492         # Find the real backend pathname     
    493         realbackend = os.path.join(os.path.split(sys.argv[0])[0], self.originalbackend) 
    494          
    495         # And launch it 
    496         if filehandle is None : 
    497             arguments = sys.argv 
    498         else :     
    499             # Here we absolutely WANT to remove any filename from the command line ! 
    500             arguments = [ "Fake this because we are printing a banner" ] + sys.argv[1:6] 
    501         # in case the username was modified by an external command :     
    502         arguments[2] = self.username 
    503              
    504         self.regainPriv()     
    505          
    506         self.logdebug("Starting real backend %s with args %s" % (realbackend, " ".join(['"%s"' % a for a in ([os.environ["DEVICE_URI"]] + arguments[1:])]))) 
    507         subprocess = PyKotaPopen4([realbackend] + arguments[1:], bufsize=0, arg0=os.environ["DEVICE_URI"]) 
    508          
    509         # Save file descriptors, we will need them later. 
    510         stderrfno = sys.stderr.fileno() 
    511         fromcfno = subprocess.fromchild.fileno() 
    512         tocfno = subprocess.tochild.fileno() 
    513          
    514         # We will have to be careful when dealing with I/O  
    515         # So we use a poll object to know when to read or write 
    516         pollster = select.poll() 
    517         pollster.register(fromcfno, select.POLLIN | select.POLLPRI) 
    518         pollster.register(stderrfno, select.POLLOUT) 
    519         pollster.register(tocfno, select.POLLOUT) 
    520          
    521         # Initialize our buffers 
    522         indata = "" 
    523         outdata = "" 
    524         endinput = endoutput = 0 
    525         inputclosed = outputclosed = 0 
    526         totaltochild = totalfromcups = 0 
    527         totalfromchild = totaltocups = 0 
    528          
    529         if filehandle is None: 
    530             if self.preserveinputfile is None : 
    531                # this is not a real file, we read the job's data 
    532                 # from our temporary file which is a copy of stdin  
    533                 infno = self.jobdatastream.fileno() 
    534                 self.jobdatastream.seek(0) 
    535                 pollster.register(infno, select.POLLIN | select.POLLPRI) 
    536             else :     
    537                 # job's data is in a file, no need to pass the data 
    538                 # to the real backend 
    539                 self.logdebug("Job's data is in %s" % self.preserveinputfile) 
    540                 infno = None 
    541                 endinput = 1 
    542         else: 
    543             self.logdebug("Printing data passed from filehandle") 
    544             indata = filehandle.read() 
    545             infno = None 
    546             endinput = 1 
    547             filehandle.close() 
    548          
    549         self.logdebug("Entering streams polling loop...") 
    550         MEGABYTE = 1024*1024 
    551         killed = 0 
    552         status = -1 
    553         while (status == -1) and (not killed) and not (inputclosed and outputclosed) : 
    554             # First check if original backend is still alive 
    555             status = subprocess.poll() 
    556              
    557             # Now if we got SIGTERM, we have  
    558             # to kill -TERM the original backend 
    559             if self.gotSigTerm and not killed : 
    560                 try : 
    561                     os.kill(subprocess.pid, signal.SIGTERM) 
    562                 except OSError, msg : # ignore but logs if process was already killed. 
    563                     self.logdebug("Error while sending signal to pid %s : %s" % (subprocess.pid, msg)) 
    564                 else :     
    565                     self.printInfo(_("SIGTERM was sent to real backend %s (pid: %s)") % (realbackend, subprocess.pid)) 
    566                     killed = 1 
    567              
    568             # In any case, deal with any remaining I/O 
    569             try : 
    570                 availablefds = pollster.poll(5000) 
    571             except select.error, msg :     
    572                 self.logdebug("Interrupted poll : %s" % msg) 
    573                 availablefds = [] 
    574             if not availablefds : 
    575                 self.logdebug("Nothing to do, sleeping a bit...") 
    576                 time.sleep(0.01) # give some time to the system 
    577             else : 
    578                 for (fd, mask) in availablefds : 
    579                     # self.logdebug(self.formatFileEvent(fd, mask)) 
    580                     try : 
    581                         if mask & select.POLLOUT : 
    582                             # We can write 
    583                             if fd == tocfno : 
    584                                 if indata : 
    585                                     try : 
    586                                         nbwritten = os.write(fd, indata)     
    587                                     except (OSError, IOError), msg :     
    588                                         self.logdebug("Error while writing to real backend's stdin %s : %s" % (fd, msg)) 
    589                                     else :     
    590                                         if len(indata) != nbwritten : 
    591                                             self.logdebug("Short write to real backend's input !") 
    592                                         totaltochild += nbwritten     
    593                                         self.logdebug("%s bytes sent to real backend so far..." % totaltochild) 
    594                                         indata = indata[nbwritten:] 
    595                                 else :         
    596                                     self.logdebug("No data to send to real backend yet, sleeping a bit...") 
    597                                     time.sleep(0.01) 
    598                                      
    599                                 if endinput :     
    600                                     self.unregisterFileNo(pollster, tocfno)         
    601                                     self.logdebug("Closing real backend's stdin.") 
    602                                     os.close(tocfno) 
    603                                     inputclosed = 1 
    604                             elif fd == stderrfno : 
    605                                 if outdata : 
    606                                     try : 
    607                                         nbwritten = os.write(fd, outdata) 
    608                                     except (OSError, IOError), msg :     
    609                                         self.logdebug("Error while writing to CUPS back channel (stderr) %s : %s" % (fd, msg)) 
    610                                     else : 
    611                                         if len(outdata) != nbwritten : 
    612                                             self.logdebug("Short write to stderr (CUPS) !") 
    613                                         totaltocups += nbwritten     
    614                                         self.logdebug("%s bytes sent back to CUPS so far..." % totaltocups) 
    615                                         outdata = outdata[nbwritten:] 
    616                                 else :         
    617                                     # self.logdebug("No data to send back to CUPS yet, sleeping a bit...") # Uncommenting this fills your logs 
    618                                     time.sleep(0.01) # Give some time to the system, stderr is ALWAYS writeable it seems. 
    619                                      
    620                                 if endoutput :     
    621                                     self.unregisterFileNo(pollster, stderrfno)         
    622                                     outputclosed = 1 
    623                             else :     
    624                                 self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask)) 
    625                                 time.sleep(0.01) 
    626                                  
    627                         if mask & (select.POLLIN | select.POLLPRI) :      
    628                             # We have something to read 
    629                             try : 
    630                                 data = os.read(fd, MEGABYTE) 
    631                             except (IOError, OSError), msg :     
    632                                 self.logdebug("Error while reading file %s : %s" % (fd, msg)) 
    633                             else : 
    634                                 if fd == infno : 
    635                                     if not data :    # If yes, then no more input data 
    636                                         self.unregisterFileNo(pollster, infno) 
    637                                         self.logdebug("Input data ends.") 
    638                                         endinput = 1 # this happens with real files. 
    639                                     else :     
    640                                         indata += data 
    641                                         totalfromcups += len(data) 
    642                                         self.logdebug("%s bytes read from CUPS so far..." % totalfromcups) 
    643                                 elif fd == fromcfno : 
    644                                     if not data : 
    645                                         self.logdebug("No back channel data to read from real backend yet, sleeping a bit...") 
    646                                         time.sleep(0.01) 
    647                                     else : 
    648                                         outdata += data 
    649                                         totalfromchild += len(data) 
    650                                         self.logdebug("%s bytes read from real backend so far..." % totalfromchild) 
    651                                 else :     
    652                                     self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask)) 
    653                                     time.sleep(0.01) 
    654                                      
    655                         if mask & (select.POLLHUP | select.POLLERR) : 
    656                             # Treat POLLERR as an EOF. 
    657                             # Some standard I/O stream has no more datas 
    658                             self.unregisterFileNo(pollster, fd) 
    659                             if fd == infno : 
    660                                 # Here we are in the case where the input file is stdin. 
    661                                 # which has no more data to be read. 
    662                                 self.logdebug("Input data ends.") 
    663                                 endinput = 1 
    664                             elif fd == fromcfno :     
    665                                 # We are no more interested in this file descriptor         
    666                                 self.logdebug("Closing real backend's stdout+stderr.") 
    667                                 os.close(fromcfno) 
    668                                 endoutput = 1 
    669                             else :     
    670                                 self.logdebug("Unexpected : %s - Sleeping a bit..." % self.formatFileEvent(fd, mask)) 
    671                                 time.sleep(0.01) 
    672                                  
    673                         if mask & select.POLLNVAL :         
    674                             self.logdebug("File %s was closed. Unregistering from polling object." % fd) 
    675                             self.unregisterFileNo(pollster, fd) 
    676                     except IOError, msg :             
    677                         self.logdebug("Got an IOError : %s" % msg) # we got signalled during an I/O 
    678                  
    679         # We must close the real backend's input stream 
    680         if killed and not inputclosed : 
    681             self.logdebug("Forcing close of real backend's stdin.") 
    682             os.close(tocfno) 
    683          
    684         self.logdebug("Exiting streams polling loop...") 
    685          
    686         self.logdebug("input data's final length : %s" % len(indata)) 
    687         self.logdebug("back-channel data's final length : %s" % len(outdata)) 
    688          
    689         self.logdebug("Total bytes read from CUPS (job's datas) : %s" % totalfromcups) 
    690         self.logdebug("Total bytes sent to real backend (job's datas) : %s" % totaltochild) 
    691          
    692         self.logdebug("Total bytes read from real backend (back-channel datas) : %s" % totalfromchild) 
    693         self.logdebug("Total bytes sent back to CUPS (back-channel datas) : %s" % totaltocups) 
    694          
    695         # Check exit code of original CUPS backend.     
    696         if status == -1 : 
    697             # we exited the loop before the real backend exited 
    698             # now we have to wait for it to finish and get its status 
    699             self.logdebug("Waiting for real backend to exit...") 
    700             try : 
    701                 status = subprocess.wait() 
    702             except OSError : # already dead : TODO : detect when abnormal 
    703                 status = 0 
    704         if os.WIFEXITED(status) : 
    705             retcode = os.WEXITSTATUS(status) 
    706         elif not killed :     
    707             self.sendBackChannelData(_("CUPS backend %s died abnormally.") % realbackend, "error") 
    708             retcode = -1 
    709         else :     
    710             retcode = self.removeJob() 
    711              
    712         self.dropPriv()     
    713          
    714         return retcode     
    715      
    716 if __name__ == "__main__" :     
    717     # This is a CUPS backend, we should act and die like a CUPS backend 
    718     retcode = 0 
    719     if len(sys.argv) == 1 : 
     85        # Unfortunately this method can't output any debug information 
     86        # to stdout or stderr, else CUPS considers that the device is 
     87        # not available. 
     88        available = [] 
    72089        (directory, myname) = os.path.split(sys.argv[0]) 
     90        if not directory : 
     91            directory = "./" 
    72192        tmpdir = tempfile.gettempdir() 
    72293        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname) 
    72394        if os.path.exists(lockfilename) : 
    724             # there's already a lockfile, see if still used 
    72595            lockfile = open(lockfilename, "r") 
    72696            pid = int(lockfile.read()) 
     
    731101            except OSError, e :     
    732102                if e.errno != errno.EPERM : 
    733                     # process doesn't exist anymore, remove the lock 
     103                    # process doesn't exist anymore 
    734104                    os.remove(lockfilename) 
    735105             
    736106        if not os.path.exists(lockfilename) : 
    737107            lockfile = open(lockfilename, "w") 
    738             lockfile.write("%i" % os.getpid()) 
     108            lockfile.write("%i" % self.pid) 
    739109            lockfile.close() 
    740             # we will execute each existing backend in device enumeration mode 
    741             # and generate their PyKota accounting counterpart 
    742110            allbackends = [ os.path.join(directory, b) \ 
    743111                                for b in os.listdir(directory)  
     
    753121                if status is None : 
    754122                    for d in devices : 
    755                         # each line is of the form : 'xxxx xxxx "xxxx xxx" "xxxx xxx"' 
     123                        # each line is of the form :  
     124                        # 'xxxx xxxx "xxxx xxx" "xxxx xxx"' 
    756125                        # so we have to decompose it carefully 
    757                         fdevice = cStringIO.StringIO("%s" % d) 
     126                        fdevice = cStringIO.StringIO(d) 
    758127                        tokenizer = shlex.shlex(fdevice) 
    759                         tokenizer.wordchars = tokenizer.wordchars + r".:,?!~/\_$*-+={}[]()#" 
     128                        tokenizer.wordchars = tokenizer.wordchars + \ 
     129                                                        r".:,?!~/\_$*-+={}[]()#" 
    760130                        arguments = [] 
    761131                        while 1 : 
     
    775145                            if fullname.startswith('"') and fullname.endswith('"') : 
    776146                                fullname = fullname[1:-1] 
    777                             print '%s cupspykota:%s "PyKota+%s" "PyKota managed %s"' % (devicetype, device, name, fullname) 
     147                            available.append('%s %s:%s "%s+%s" "%s managed %s"' \ 
     148                                                 % (devicetype, self.myname, \ 
     149                                                    device, self.MyName, \ 
     150                                                    name, self.MyName, \ 
     151                                                    fullname)) 
    778152            os.remove(lockfilename) 
    779         retcode = 0                 
     153        available.append('direct %s:// "%s+Nothing" "%s managed Virtual Printer"' \ 
     154                             % (self.myname, self.MyName, self.MyName)) 
     155        return available 
     156                         
     157    def initBackendParameters(self) :     
     158        """Initializes the backend's attributes.""" 
     159        # check that the DEVICE_URI environment variable's value is  
     160        # prefixed with self.myname otherwise don't touch it. 
     161        # If this is the case, we have to remove the prefix from  
     162        # the environment before launching the real backend  
     163        self.logdebug("Initializing backend...") 
     164        muststartwith = "%s:" % self.myname 
     165        device_uri = os.environ.get("DEVICE_URI", "") 
     166        if device_uri.startswith(muststartwith) : 
     167            fulldevice_uri = device_uri[:] 
     168            device_uri = fulldevice_uri[len(muststartwith):] 
     169            for i in range(2) : 
     170                if device_uri.startswith("/") :   
     171                    device_uri = device_uri[1:] 
     172        try : 
     173            (backend, destination) = device_uri.split(":", 1)  
     174        except ValueError :     
     175            if not device_uri : 
     176                self.logDebug("Not attached to an existing print queue.") 
     177                backend = "" 
     178                printerhostname = "" 
     179            else :     
     180                raise PyKotaToolError, "Invalid DEVICE_URI : %s\n" % device_uri 
     181        else :         
     182            while destination.startswith("/") : 
     183                destination = destination[1:] 
     184            checkauth = destination.split("@", 1)     
     185            if len(checkauth) == 2 : 
     186                destination = checkauth[1] 
     187            printerhostname = destination.split("/")[0].split(":")[0] 
     188         
     189        self.Action = "ALLOW"   # job allowed by default 
     190        self.JobId = sys.argv[1].strip() 
     191        # use CUPS' user when printing test pages from CUPS' web interface 
     192        self.UserName = sys.argv[2].strip() or pwd.getpwuid(os.geteuid())[0] 
     193        self.Title = sys.argv[3].strip() 
     194        self.Copies = int(sys.argv[4].strip()) 
     195        self.Options = sys.argv[5].strip() 
     196        if len(sys.argv) == 7 : 
     197            self.InputFile = sys.argv[6] # read job's datas from file 
     198        else :     
     199            self.InputFile = None        # read job's datas from stdin 
     200             
     201        self.PrinterHostName = printerhostname     
     202        self.RealBackend = backend 
     203        self.DeviceURI = device_uri 
     204        self.PrinterName = os.environ.get("PRINTER", "") 
     205        self.Directory = self.config.getPrinterDirectory(self.PrinterName) 
     206        self.DataFile = os.path.join(self.Directory, "%s-%s-%s-%s" % \ 
     207                   (self.myname, self.PrinterName, self.UserName, self.JobId)) 
     208         
     209        (ippfilename, ippmessage) = self.parseIPPRequestFile() 
     210        self.ControlFile = ippfilename 
     211        john = ippmessage.operation_attributes.get("job-originating-host-name", \ 
     212               ippmessage.job_attributes.get("job-originating-host-name", \ 
     213               (None, None))) 
     214        if type(john) == type([]) :  
     215            john = john[-1] 
     216        (chtype, self.ClientHost) = john  
     217        jbing = ippmessage.job_attributes.get("job-billing", (None, None)) 
     218        if type(jbing) == type([]) :  
     219            jbing = jbing[-1] 
     220        (jbtype, self.JobBillingCode) = jbing 
     221         
     222        self.logdebug("Backend : %s" % self.RealBackend) 
     223        self.logdebug("DeviceURI : %s" % self.DeviceURI) 
     224        self.logdebug("Printername : %s" % self.PrinterName) 
     225        self.logdebug("Username : %s" % self.UserName) 
     226        self.logdebug("JobId : %s" % self.JobId) 
     227        self.logdebug("Title : %s" % self.Title) 
     228        self.logdebug("Filename : %s" % self.InputFile) 
     229        self.logdebug("Copies : %s" % self.Copies) 
     230        self.logdebug("Options : %s" % self.Options) 
     231        self.logdebug("Directory : %s" % self.Directory)  
     232        self.logdebug("DataFile : %s" % self.DataFile) 
     233        self.logdebug("ControlFile : %s" % self.ControlFile) 
     234        self.logdebug("JobBillingCode : %s" % self.JobBillingCode) 
     235        self.logdebug("JobOriginatingHostName : %s" % self.ClientHost) 
     236         
     237        self.logdebug("Backend initialized.") 
     238         
     239    def overwriteJobAttributes(self) : 
     240        """Overwrites some of the job's attributes if needed.""" 
     241        self.logdebug("Sanitizing job's attributes...") 
     242        # First overwrite the job ticket 
     243        self.overwriteJobTicket() 
     244         
     245        # do we want to strip out the Samba/Winbind domain name ? 
     246        separator = self.config.getWinbindSeparator() 
     247        if separator is not None : 
     248            self.UserName = self.UserName.split(separator)[-1] 
     249             
     250        # do we want to lowercase usernames ?     
     251        if self.config.getUserNameToLower() : 
     252            self.UserName = self.UserName.lower() 
     253             
     254        # do we want to strip some prefix off of titles ?     
     255        stripprefix = self.config.getStripTitle(self.PrinterName) 
     256        if stripprefix : 
     257            if fnmatch.fnmatch(self.Title[:len(stripprefix)], stripprefix) : 
     258                self.logdebug("Prefix [%s] removed from job's title [%s]." \ 
     259                                      % (stripprefix, self.Title)) 
     260                self.Title = self.Title[len(stripprefix):] 
     261                 
     262        self.logdebug("Username : %s" % self.UserName) 
     263        self.logdebug("BillingCode : %s" % self.JobBillingCode) 
     264        self.logdebug("Title : %s" % self.Title) 
     265        self.logdebug("Job's attributes sanitizing done.") 
     266                 
     267    def overwriteJobTicket(self) :     
     268        """Should we overwrite the job's ticket (username and billingcode) ?""" 
     269        self.logdebug("Checking if we need to overwrite the job ticket...") 
     270        jobticketcommand = self.config.getOverwriteJobTicket(self.PrinterName) 
     271        if jobticketcommand is not None : 
     272            username = billingcode = action = None 
     273            self.logdebug("Launching subprocess [%s] to overwrite the job ticket." \ 
     274                                     % jobticketcommand) 
     275            inputfile = os.popen(jobticketcommand, "r") 
     276            for line in inputfile.xreadlines() : 
     277                line = line.strip() 
     278                if line == "DENY" : 
     279                    self.logdebug("Seen DENY command.") 
     280                    action = "DENY" 
     281                elif line.startswith("USERNAME=") :     
     282                    username = line.split("=", 1)[1].strip() 
     283                    self.logdebug("Seen new username [%s]" % username) 
     284                    action = None 
     285                elif line.startswith("BILLINGCODE=") :     
     286                    billingcode = line.split("=", 1)[1].strip() 
     287                    self.logdebug("Seen new billing code [%s]" % billingcode) 
     288                    action = None 
     289            inputfile.close()     
     290             
     291            # now overwrite the job's ticket if new data was supplied 
     292            if action : 
     293                self.Action = action 
     294            if username : 
     295                self.UserName = username 
     296            # NB : we overwrite the billing code even if empty     
     297            self.JobBillingCode = billingcode  
     298        self.logdebug("Job ticket overwriting done.") 
     299             
     300    def saveDatasAndCheckSum(self) : 
     301        """Saves the input datas into a static file.""" 
     302        self.logdebug("Duplicating data stream into %s" % self.DataFile) 
     303        mustclose = 0 
     304        if self.InputFile is not None : 
     305            infile = open(self.InputFile, "rb") 
     306            mustclose = 1 
     307        else :     
     308            infile = sys.stdin 
     309        CHUNK = 64*1024         # read 64 Kb at a time 
     310        dummy = 0 
     311        sizeread = 0 
     312        checksum = md5.new() 
     313        outfile = open(self.DataFile, "wb")     
     314        while 1 : 
     315            data = infile.read(CHUNK)  
     316            if not data : 
     317                break 
     318            sizeread += len(data)     
     319            outfile.write(data) 
     320            checksum.update(data)     
     321            if not (dummy % 32) : # Only display every 2 Mb 
     322                self.logdebug("%s bytes saved..." % sizeread) 
     323            dummy += 1     
     324        outfile.close() 
     325        if mustclose :     
     326            infile.close() 
     327             
     328        self.JobSizeBytes = sizeread     
     329        self.JobMD5Sum = checksum.hexdigest() 
     330         
     331        self.logdebug("JobSizeBytes : %s" % self.JobSizeBytes) 
     332        self.logdebug("JobMD5Sum : %s" % self.JobMD5Sum) 
     333        self.logdebug("Data stream duplicated into %s" % self.DataFile) 
     334             
     335    def clean(self) : 
     336        """Cleans up the place.""" 
     337        self.logdebug("Cleaning up...") 
     338        self.deinstallSigTermHandler() 
     339        if not self.config.getPrinterKeepFiles(self.PrinterName) : 
     340            try : 
     341                self.logdebug("Work file %s will be deleted." % self.DataFile) 
     342            except AttributeError :     
     343                pass 
     344            else :     
     345                os.remove(self.DataFile) 
     346                self.logdebug("Work file %s has been deleted." % self.DataFile) 
     347        else :     
     348            self.logdebug("Work file %s will be kept." % self.DataFile) 
     349        PyKotaTool.clean(self)     
     350        self.logdebug("Clean.") 
     351             
     352    def precomputeJobSize(self) :     
     353        """Computes the job size with a software method.""" 
     354        self.logdebug("Precomputing job's size...") 
     355        jobsize = 0 
     356        if self.JobSizeBytes : 
     357            try : 
     358                from pkpgpdls import analyzer, pdlparser 
     359            except ImportError :     
     360                self.printInfo("pkpgcounter is now distributed separately, please grab it from http://www.librelogiciel.com/software/pkpgcounter/action_Download", "error") 
     361                self.printInfo("Precomputed job size will be forced to 0 pages.", "error") 
     362            else :      
     363                infile = open(self.DataFile, "rb") 
     364                try : 
     365                    parser = analyzer.PDLAnalyzer(infile) 
     366                    jobsize = parser.getJobSize() 
     367                except pdlparser.PDLParserError, msg :     
     368                    # Here we just log the failure, but 
     369                    # we finally ignore it and return 0 since this 
     370                    # computation is just an indication of what the 
     371                    # job's size MAY be. 
     372                    self.printInfo(_("Unable to precompute the job's size with the generic PDL analyzer : %s") % msg, "warn") 
     373                else :     
     374                    if self.InputFile is not None : 
     375                        # when a filename is passed as an argument, the backend  
     376                        # must generate the correct number of copies. 
     377                        jobsize *= self.Copies 
     378                infile.close()         
     379        self.softwareJobSize = jobsize 
     380        self.logdebug("Precomputed job's size is %s pages." % self.softwareJobSize) 
     381         
     382    def precomputeJobPrice(self) :     
     383        """Precomputes the job price with a software method.""" 
     384        self.logdebug("Precomputing job's price...") 
     385        self.softwareJobPrice = self.UserPQuota.computeJobPrice(self.softwareJobSize) 
     386        self.logdebug("Precomputed job's price is %.3f credits." \ 
     387                                   % self.softwareJobPrice) 
     388         
     389    def getCupsConfigDirectives(self, directives=[]) : 
     390        """Retrieves some CUPS directives from its configuration file. 
     391         
     392           Returns a mapping with lowercased directives as keys and  
     393           their setting as values. 
     394        """ 
     395        self.logdebug("Parsing CUPS' configuration file...") 
     396        dirvalues = {}  
     397        cupsroot = os.environ.get("CUPS_SERVERROOT", "/etc/cups") 
     398        cupsdconf = os.path.join(cupsroot, "cupsd.conf") 
     399        try : 
     400            conffile = open(cupsdconf, "r") 
     401        except IOError :     
     402            raise PyKotaToolError, "Unable to open %s" % cupsdconf 
     403        else :     
     404            for line in conffile.readlines() : 
     405                linecopy = line.strip().lower() 
     406                for di in [d.lower() for d in directives] : 
     407                    if linecopy.startswith("%s " % di) : 
     408                        try : 
     409                            val = line.split()[1] 
     410                        except :     
     411                            pass # ignore errors, we take the last value in any case. 
     412                        else :     
     413                            dirvalues[di] = val 
     414            conffile.close()             
     415        self.logdebug("CUPS' configuration file parsed successfully.") 
     416        return dirvalues        
     417             
     418    def parseIPPRequestFile(self) :         
     419        """Parses the IPP message file and returns a tuple (filename, parsedvalue).""" 
     420        self.logdebug("Parsing IPP request file...") 
     421         
     422        class DummyClass : 
     423            operation_attributes = {} 
     424            job_attributes = {} 
     425             
     426        ippmessage = DummyClass() # in case the code below fails 
     427         
     428        self.regainPriv() 
     429        cupsdconf = self.getCupsConfigDirectives(["RequestRoot"]) 
     430        requestroot = cupsdconf.get("requestroot", "/var/spool/cups") 
     431        if (len(self.JobId) < 5) and self.JobId.isdigit() : 
     432            ippmessagefile = "c%05i" % int(self.JobId) 
     433        else :     
     434            ippmessagefile = "c%s" % self.JobId 
     435        ippmessagefile = os.path.join(requestroot, ippmessagefile) 
     436        try : 
     437            ippdatafile = open(ippmessagefile) 
     438        except :     
     439            self.logdebug("Unable to open IPP request file %s" % ippmessagefile) 
     440        else :     
     441            self.logdebug("Parsing of IPP request file %s begins." % ippmessagefile) 
     442            try : 
     443                ippmessage = IPPRequest(ippdatafile.read()) 
     444                ippmessage.parse() 
     445            except IPPError, msg :     
     446                self.printInfo("Error while parsing %s : %s" \ 
     447                                      % (ippmessagefile, msg), "warn") 
     448            else :     
     449                self.logdebug("Parsing of IPP request file %s ends." \ 
     450                                       % ippmessagefile) 
     451            ippdatafile.close() 
     452        self.dropPriv() 
     453        self.logdebug("IPP request file parsed successfully.") 
     454        return (ippmessagefile, ippmessage) 
     455                 
     456    def exportJobInfo(self) :     
     457        """Exports the actual job's attributes to the environment.""" 
     458        self.logdebug("Exporting job information to the environment...") 
     459        os.environ["DEVICE_URI"] = self.DeviceURI       # WARNING ! 
     460        os.environ["PYKOTAPRINTERNAME"] = self.PrinterName 
     461        os.environ["PYKOTADIRECTORY"] = self.Directory 
     462        os.environ["PYKOTADATAFILE"] = self.DataFile 
     463        os.environ["PYKOTAJOBSIZEBYTES"] = str(self.JobSizeBytes) 
     464        os.environ["PYKOTAMD5SUM"] = self.JobMD5Sum 
     465        os.environ["PYKOTAJOBORIGINATINGHOSTNAME"] = self.ClientHost or "" 
     466        os.environ["PYKOTAJOBID"] = self.JobId 
     467        os.environ["PYKOTAUSERNAME"] = self.UserName 
     468        os.environ["PYKOTATITLE"] = self.Title 
     469        os.environ["PYKOTACOPIES"] = str(self.Copies) 
     470        os.environ["PYKOTAOPTIONS"] = self.Options 
     471        os.environ["PYKOTAFILENAME"] = self.InputFile or "" 
     472        os.environ["PYKOTAJOBBILLING"] = self.JobBillingCode or "" 
     473        os.environ["PYKOTACONTROLFILE"] = self.ControlFile 
     474        os.environ["PYKOTAPRINTERHOSTNAME"] = self.PrinterHostName 
     475        os.environ["PYKOTAPRECOMPUTEDJOBSIZE"] = str(self.softwareJobSize) 
     476        self.logdebug("Environment updated.") 
     477         
     478    def exportUserInfo(self) : 
     479        """Exports user information to the environment.""" 
     480        self.logdebug("Exporting user information to the environment...") 
     481        os.environ["PYKOTAOVERCHARGE"] = str(self.User.OverCharge) 
     482        os.environ["PYKOTALIMITBY"] = str(self.User.LimitBy) 
     483        os.environ["PYKOTABALANCE"] = str(self.User.AccountBalance or 0.0) 
     484        os.environ["PYKOTALIFETIMEPAID"] = str(self.User.LifeTimePaid or 0.0) 
     485         
     486        os.environ["PYKOTAPAGECOUNTER"] = str(self.UserPQuota.PageCounter or 0) 
     487        os.environ["PYKOTALIFEPAGECOUNTER"] = str(self.UserPQuota.LifePageCounter or 0) 
     488        os.environ["PYKOTASOFTLIMIT"] = str(self.UserPQuota.SoftLimit) 
     489        os.environ["PYKOTAHARDLIMIT"] = str(self.UserPQuota.HardLimit) 
     490        os.environ["PYKOTADATELIMIT"] = str(self.UserPQuota.DateLimit) 
     491        os.environ["PYKOTAWARNCOUNT"] = str(self.UserPQuota.WarnCount) 
     492         
     493        # TODO : move this elsewhere once software accounting is done only once. 
     494        os.environ["PYKOTAPRECOMPUTEDJOBPRICE"] = str(self.softwareJobPrice) 
     495         
     496        self.logdebug("Environment updated.") 
     497         
     498    def exportPrinterInfo(self) : 
     499        """Exports printer information to the environment.""" 
     500        self.logdebug("Exporting printer information to the environment...") 
     501        # exports the list of printers groups the current 
     502        # printer is a member of 
     503        os.environ["PYKOTAPGROUPS"] = ",".join([p.Name for p in self.storage.getParentPrinters(self.Printer)]) 
     504        self.logdebug("Environment updated.") 
     505         
     506    def exportPhaseInfo(self, phase) : 
     507        """Exports phase information to the environment.""" 
     508        self.logdebug("Exporting phase information [%s] to the environment..." % phase) 
     509        os.environ["PYKOTAPHASE"] = phase 
     510        self.logdebug("Environment updated.") 
     511         
     512    def exportJobSizeAndPrice(self) : 
     513        """Exports job's size and price information to the environment.""" 
     514        self.logdebug("Exporting job's size and price information to the environment...") 
     515        os.environ["PYKOTAJOBSIZE"] = str(self.JobSize) 
     516        os.environ["PYKOTAJOBPRICE"] = str(self.JobPrice) 
     517        self.logdebug("Environment updated.") 
     518         
     519    def acceptJob(self) :         
     520        """Returns the appropriate exit code to tell CUPS all is OK.""" 
     521        return 0 
     522             
     523    def removeJob(self) :             
     524        """Returns the appropriate exit code to let CUPS think all is OK. 
     525         
     526           Returning 0 (success) prevents CUPS from stopping the print queue. 
     527        """    
     528        return 0 
     529         
     530    def launchPreHook(self) : 
     531        """Allows plugging of an external hook before the job gets printed.""" 
     532        prehook = self.config.getPreHook(self.PrinterName) 
     533        if prehook : 
     534            self.logdebug("Executing pre-hook [%s]..." % prehook) 
     535            retcode = os.system(prehook) 
     536            self.logdebug("pre-hook exited with status %s." % retcode) 
     537         
     538    def launchPostHook(self) : 
     539        """Allows plugging of an external hook after the job gets printed and/or denied.""" 
     540        posthook = self.config.getPostHook(self.PrinterName) 
     541        if posthook : 
     542            self.logdebug("Executing post-hook [%s]..." % posthook) 
     543            retcode = os.system(posthook) 
     544            self.logdebug("post-hook exited with status %s." % retcode) 
     545             
     546    def improveMessage(self, message) :         
     547        """Improves a message by adding more informations in it if possible.""" 
     548        try : 
     549            return "%s@%s(%s) => %s" % (self.UserName, \ 
     550                                        self.PrinterName, \ 
     551                                        self.JobId, \ 
     552                                        message) 
     553        except :                                                
     554            return message 
     555         
     556    def logdebug(self, message) :         
     557        """Improves the debug message before outputting it.""" 
     558        PyKotaTool.logdebug(self, self.improveMessage(message)) 
     559         
     560    def printInfo(self, message, level="info") :         
     561        """Improves the informational message before outputting it.""" 
     562        self.logger.log_message(self.improveMessage(message), level) 
     563     
     564    def startingBanner(self) : 
     565        """Retrieves a starting banner for current printer and returns its content.""" 
     566        self.logdebug("Retrieving starting banner...") 
     567        self.printBanner(self.config.getStartingBanner(self.PrinterName)) 
     568        self.logdebug("Starting banner retrieved.") 
     569     
     570    def endingBanner(self) : 
     571        """Retrieves an ending banner for current printer and returns its content.""" 
     572        self.logdebug("Retrieving ending banner...") 
     573        self.printBanner(self.config.getEndingBanner(self.PrinterName)) 
     574        self.logdebug("Ending banner retrieved.") 
     575         
     576    def printBanner(self, bannerfileorcommand) : 
     577        """Reads a banner or generates one through an external command. 
     578         
     579           Returns the banner's content in a format which MUST be accepted 
     580           by the printer. 
     581        """ 
     582        self.logdebug("Printing banner...") 
     583        if bannerfileorcommand : 
     584            if os.access(bannerfileorcommand, os.X_OK) or \ 
     585                  not os.path.isfile(bannerfileorcommand) : 
     586                self.logdebug("Launching %s to generate a banner." % bannerfileorcommand) 
     587                child = popen2.Popen3(bannerfileorcommand, capturestderr=1) 
     588                self.runOriginalBackend(child.fromchild, isBanner=1) 
     589                child.tochild.close() 
     590                child.childerr.close() 
     591                child.fromchild.close() 
     592                status = child.wait() 
     593                if os.WIFEXITED(status) : 
     594                    status = os.WEXITSTATUS(status) 
     595                self.printInfo(_("Banner generator %s exit code is %s") \ 
     596                                         % (bannerfileorcommand, str(status))) 
     597            else : 
     598                self.logdebug("Using %s as the banner." % bannerfileorcommand) 
     599                try : 
     600                    fh = open(bannerfileorcommand, 'rb') 
     601                except IOError, msg :     
     602                    self.printInfo("Impossible to open %s : %s" \ 
     603                                       % (bannerfileorcommand, msg), "error") 
     604                else :     
     605                    self.runOriginalBackend(fh, isBanner=1) 
     606                    fh.close() 
     607        self.logdebug("Banner printed...") 
     608                 
     609    def handleBanner(self, bannertype, withaccounting) : 
     610        """Handles the banner with or without accounting.""" 
     611        if withaccounting : 
     612            acc = "with" 
     613        else :     
     614            acc = "without" 
     615        self.logdebug("Handling %s banner %s accounting..." % (bannertype, acc)) 
     616        if (self.Action == 'DENY') and \ 
     617           (self.UserPQuota.WarnCount >= \ 
     618                            self.config.getMaxDenyBanners(self.PrinterName)) : 
     619            self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), \ 
     620                             "warn") 
     621        else : 
     622            if self.Action == 'DENY' : 
     623                self.logdebug("Incrementing the number of deny banners for user %s on printer %s" \ 
     624                                  % (self.UserName, self.PrinterName)) 
     625                self.UserPQuota.incDenyBannerCounter() # increments the warning counter 
     626                self.exportUserInfo() 
     627            getattr(self, "%sBanner" % bannertype)() 
     628            if withaccounting : 
     629                if self.accounter.isSoftware : 
     630                    self.BannerSize += 1 # TODO : fix this by passing the banner's content through software accounting 
     631        self.logdebug("%s banner done." % bannertype.title()) 
     632         
     633    def sanitizeJobSize(self) :     
     634        """Sanitizes the job's size if needed.""" 
     635        # TODO : there's a difficult to see bug here when banner accounting is activated and hardware accounting is used. 
     636        self.logdebug("Sanitizing job's size...") 
     637        if self.softwareJobSize and (self.JobSize != self.softwareJobSize) : 
     638            self.printInfo(_("Beware : computed job size (%s) != precomputed job size (%s)") % \ 
     639                                       (self.JobSize, self.softwareJobSize), \ 
     640                           "error") 
     641            (limit, replacement) = self.config.getTrustJobSize(self.PrinterName) 
     642            if limit is None : 
     643                self.printInfo(_("The job size will be trusted anyway according to the 'trustjobsize' directive"), "warn") 
     644            else : 
     645                if self.JobSize <= limit : 
     646                    self.printInfo(_("The job size will be trusted because it is inferior to the 'trustjobsize' directive's limit %s") % limit, "warn") 
     647                else : 
     648                    self.printInfo(_("The job size will be modified according to the 'trustjobsize' directive : %s") % replacement, "warn") 
     649                    if replacement == "PRECOMPUTED" : 
     650                        self.JobSize = self.softwareJobSize 
     651                    else :     
     652                        self.JobSize = replacement 
     653        self.logdebug("Job's size sanitized.") 
     654                         
     655    def getPrinterUserAndUserPQuota(self) :         
     656        """Returns a tuple (policy, printer, user, and user print quota) on this printer. 
     657         
     658           "OK" is returned in the policy if both printer, user and user print quota 
     659           exist in the Quota Storage. 
     660           Otherwise, the policy as defined for this printer in pykota.conf is returned. 
     661            
     662           If policy was set to "EXTERNAL" and one of printer, user, or user print quota 
     663           doesn't exist in the Quota Storage, then an external command is launched, as 
     664           defined in the external policy for this printer in pykota.conf 
     665           This external command can do anything, like automatically adding printers 
     666           or users, for example, and finally extracting printer, user and user print 
     667           quota from the Quota Storage is tried a second time. 
     668            
     669           "EXTERNALERROR" is returned in case policy was "EXTERNAL" and an error status 
     670           was returned by the external command. 
     671        """ 
     672        self.logdebug("Retrieving printer, user, and user print quota entry from database...") 
     673        for passnumber in range(1, 3) : 
     674            printer = self.storage.getPrinter(self.PrinterName) 
     675            user = self.storage.getUser(self.UserName) 
     676            userpquota = self.storage.getUserPQuota(user, printer) 
     677            if printer.Exists and user.Exists and userpquota.Exists : 
     678                policy = "OK" 
     679                break 
     680            (policy, args) = self.config.getPrinterPolicy(self.PrinterName) 
     681            if policy == "EXTERNAL" :     
     682                commandline = self.formatCommandLine(args, user, printer) 
     683                if not printer.Exists : 
     684                    self.printInfo(_("Printer %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.PrinterName, commandline, self.PrinterName)) 
     685                if not user.Exists : 
     686                    self.printInfo(_("User %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.UserName, commandline, self.PrinterName)) 
     687                if not userpquota.Exists : 
     688                    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)) 
     689                if os.system(commandline) : 
     690                    self.printInfo(_("External policy %s for printer %s produced an error. Job rejected. Please check PyKota's configuration files.") % (commandline, self.PrinterName), "error") 
     691                    policy = "EXTERNALERROR" 
     692                    break 
     693            else :         
     694                if not printer.Exists : 
     695                    self.printInfo(_("Printer %s not registered in the PyKota system, applying default policy (%s)") % (self.PrinterName, policy)) 
     696                if not user.Exists : 
     697                    self.printInfo(_("User %s not registered in the PyKota system, applying default policy (%s) for printer %s") % (self.UserName, policy, self.PrinterName)) 
     698                if not userpquota.Exists : 
     699                    self.printInfo(_("User %s doesn't have quota on printer %s in the PyKota system, applying default policy (%s)") % (self.UserName, self.PrinterName, policy)) 
     700                break 
     701                 
     702        if policy == "EXTERNAL" :     
     703            if not printer.Exists : 
     704                self.printInfo(_("Printer %s still not registered in the PyKota system, job will be rejected") % self.PrinterName) 
     705            if not user.Exists : 
     706                self.printInfo(_("User %s still not registered in the PyKota system, job will be rejected on printer %s") % (self.UserName, self.PrinterName)) 
     707            if not userpquota.Exists : 
     708                self.printInfo(_("User %s still doesn't have quota on printer %s in the PyKota system, job will be rejected") % (self.UserName, self.PrinterName)) 
     709        self.Policy = policy          
     710        self.Printer = printer 
     711        self.User = user 
     712        self.UserPQuota = userpquota 
     713        self.logdebug("Retrieval of printer, user and user print quota entry done.") 
     714         
     715    def getBillingCode(self) :     
     716        """Extracts the billing code from the database. 
     717          
     718           An optional script is launched to notify the user when 
     719           the billing code is unknown and PyKota was configured to 
     720           deny printing in this case. 
     721        """ 
     722        self.logdebug("Retrieving billing code information from the database...") 
     723        self.BillingCode = None 
     724        if self.JobBillingCode : 
     725            self.BillingCode = self.storage.getBillingCode(self.JobBillingCode) 
     726            if self.BillingCode.Exists : 
     727                self.logdebug("Billing code [%s] found in database." % self.JobBillingCode) 
     728            else : 
     729                msg = "Unknown billing code [%s] : " % self.JobBillingCode 
     730                (newaction, script) = self.config.getUnknownBillingCode(self.PrinterName) 
     731                if newaction == "CREATE" : 
     732                    self.logdebug(msg + "will be created.") 
     733                    self.BillingCode = self.storage.addBillingCode(self.JobBillingCode) 
     734                    if self.BillingCode.Exists : 
     735                        self.logdebug(msg + "has been created.") 
     736                    else :     
     737                        self.printInfo(msg + "couldn't be created.", "error") 
     738                else :     
     739                    self.logdebug(msg + "job will be denied.") 
     740                    self.Action = newaction 
     741                    if script is not None :  
     742                        self.logdebug(msg + "launching subprocess [%s] to notify user." % script) 
     743                        os.system(script) 
     744        self.logdebug("Retrieval of billing code information done.") 
     745         
     746    def checkIfDupe(self) :     
     747        """Checks if the job is a dupe, and handles the situation.""" 
     748        self.logdebug("Checking if the job is a dupe...") 
     749        denyduplicates = self.config.getDenyDuplicates(self.PrinterName) 
     750        if not denyduplicates : 
     751            self.logdebug("We don't care about dupes after all.") 
     752        elif self.Printer.LastJob.Exists \ 
     753             and (self.Printer.LastJob.UserName == self.UserName) \ 
     754             and (self.Printer.LastJob.JobMD5Sum == self.JobMD5Sum) : 
     755            # TODO : use the current user's last job instead of   
     756            # TODO : the current printer's last job. This would be 
     757            # TODO : better but requires an additional database query 
     758            # TODO : with SQL, and is much more complex with the  
     759            # TODO : actual LDAP schema. Maybe this is not very 
     760            # TODO : important, because usually dupes are rapidly sucessive. 
     761            msg = _("Job is a dupe") 
     762            if denyduplicates == 1 : 
     763                self.printInfo("%s : %s." % (msg, _("Printing is denied by configuration")), "warn") 
     764                self.Action = "DENY" 
     765            else :     
     766                self.logdebug("Launching subprocess [%s] to see if dupes should be allowed or not." % denyduplicates) 
     767                fanswer = os.popen(denyduplicates, "r") 
     768                self.Action = fanswer.read().strip().upper() 
     769                fanswer.close() 
     770                if self.Action == "DENY" :      
     771                    self.printInfo("%s : %s." % (msg, _("Subprocess denied printing of a dupe")), "warn") 
     772                else :     
     773                    self.printInfo("%s : %s." % (msg, _("Subprocess allowed printing of a dupe")), "warn") 
     774        else :             
     775            self.logdebug("Job doesn't seem to be a dupe.") 
     776        self.logdebug("Checking if the job is a dupe done.") 
     777         
     778    def mainWork(self) :     
     779        """Main work is done here.""" 
     780        if not self.JobSizeBytes : 
     781            # if no data to pass to real backend, probably a filter 
     782            # higher in the chain failed because of a misconfiguration. 
     783            # we deny the job in this case (nothing to print anyway) 
     784            self.printInfo(_("Job contains no data. Printing is denied."), "error") 
     785            return self.removeJob() 
     786             
     787        self.getPrinterUserAndUserPQuota() 
     788        if self.Policy == "EXTERNALERROR" : 
     789            # Policy was 'EXTERNAL' and the external command returned an error code 
     790            return self.removeJob() 
     791        elif self.Policy == "EXTERNAL" : 
     792            # Policy was 'EXTERNAL' and the external command wasn't able 
     793            # to add either the printer, user or user print quota 
     794            return self.removeJob() 
     795        elif self.Policy == "DENY" :     
     796            # Either printer, user or user print quota doesn't exist, 
     797            # and the job should be rejected. 
     798            return self.removeJob() 
     799        elif self.Policy == "ALLOW" : 
     800            # ALLOW means : Either printer, user or user print quota doesn't exist, 
     801            #               but the job should be allowed anyway. 
     802            self.printInfo(_("Job allowed by printer policy. No accounting will be done."), "warn") 
     803            return self.printJobDatas() 
     804        elif self.Policy == "OK" : 
     805            # OK means : Both printer, user and user print quota exist, job should 
     806            #            be allowed if current user is allowed to print on this printer 
     807            return self.doWork() 
     808        else :     
     809            self.printInfo(_("Invalid policy %s for printer %s") % (self.Policy, self.PrinterName), "error") 
     810            return self.removeJob() 
     811     
     812    def doWork(self) :     
     813        """The accounting work is done here.""" 
     814        self.precomputeJobPrice() 
     815        self.exportUserInfo() 
     816        self.exportPrinterInfo() 
     817        self.exportPhaseInfo("BEFORE") 
     818         
     819        if self.Action != "DENY" : 
     820            # If printing is still allowed at this time, we 
     821            # need to extract the billing code information from the database. 
     822            # No need to do this if the job is denied, this way we 
     823            # save some database queries. 
     824            self.getBillingCode() 
     825             
     826        if self.Action != "DENY" : 
     827            # If printing is still allowed at this time, we 
     828            # need to check if the job is a dupe or not, and what to do then. 
     829            # No need to do this if the job is denied, this way we 
     830            # save some database queries. 
     831            self.checkIfDupe() 
     832                     
     833        if self.Action != "DENY" : 
     834            # If printing is still allowed at this time, we 
     835            # need to check the user's print quota on the current printer. 
     836            # No need to do this if the job is denied, this way we 
     837            # save some database queries. 
     838            self.logdebug("Checking user %s print quota entry on printer %s" \ 
     839                                    % (self.UserName, self.PrinterName)) 
     840            self.Action = self.warnUserPQuota(self.UserPQuota) 
     841             
     842        # exports some new environment variables 
     843        os.environ["PYKOTAACTION"] = str(self.Action) 
     844         
     845        # launches the pre hook 
     846        self.launchPreHook() 
     847         
     848        # handle starting banner pages without accounting 
     849        self.BannerSize = 0 
     850        accountbanner = self.config.getAccountBanner(self.PrinterName) 
     851        if accountbanner in ["ENDING", "NONE"] : 
     852            self.handleBanner("starting", 0) 
     853         
     854        if self.Action == "DENY" : 
     855            self.printInfo(_("Job denied, no accounting will be done.")) 
     856        else : 
     857            self.printInfo(_("Job accounting begins.")) 
     858            self.deinstallSigTermHandler() 
     859            self.accounter.beginJob(self.Printer) 
     860            self.installSigTermHandler() 
     861         
     862        # handle starting banner pages with accounting 
     863        if accountbanner in ["STARTING", "BOTH"] : 
     864            if not self.gotSigTerm : 
     865                self.handleBanner("starting", 1) 
     866         
     867        # pass the job's data to the real backend     
     868        if (not self.gotSigTerm) and (self.Action in ["ALLOW", "WARN"]) : 
     869            retcode = self.printJobDatas() 
     870        else :         
     871            retcode = self.removeJob() 
     872         
     873        # indicate phase change 
     874        self.exportPhaseInfo("AFTER") 
     875         
     876        # handle ending banner pages with accounting 
     877        if accountbanner in ["ENDING", "BOTH"] : 
     878            if not self.gotSigTerm : 
     879                self.handleBanner("ending", 1) 
     880         
     881        # stops accounting 
     882        if self.Action == "DENY" : 
     883            self.printInfo(_("Job denied, no accounting has been done.")) 
     884        else : 
     885            self.deinstallSigTermHandler() 
     886            self.accounter.endJob(self.Printer) 
     887            self.installSigTermHandler() 
     888            self.printInfo(_("Job accounting ends.")) 
     889         
     890        # Do all these database changes within a single transaction     
     891        # NB : we don't enclose ALL the changes within a single transaction 
     892        # because while waiting for the printer to answer its internal page 
     893        # counter, we would open the door to accounting problems for other 
     894        # jobs launched by the same user at the same time on other printers. 
     895        # All the code below doesn't take much time, so it's fine. 
     896        self.storage.beginTransaction() 
     897        try : 
     898            # retrieve the job size     
     899            if self.Action == "DENY" : 
     900                self.JobSize = 0 
     901                self.printInfo(_("Job size forced to 0 because printing is denied.")) 
     902            else :     
     903                self.UserPQuota.resetDenyBannerCounter() 
     904                self.JobSize = self.accounter.getJobSize(self.Printer) 
     905                self.sanitizeJobSize() 
     906                self.JobSize += self.BannerSize 
     907            self.printInfo(_("Job size : %i") % self.JobSize) 
     908             
     909            # update the quota for the current user on this printer  
     910            self.printInfo(_("Updating user %s's quota on printer %s") % (self.UserName, self.PrinterName)) 
     911            self.JobPrice = self.UserPQuota.increasePagesUsage(self.JobSize) 
     912             
     913            # adds the current job to history     
     914            self.Printer.addJobToHistory(self.JobId, self.User, self.accounter.getLastPageCounter(), \ 
     915                                    self.Action, self.JobSize, self.JobPrice, self.InputFile, \ 
     916                                    self.Title, self.Copies, self.Options, self.ClientHost, \ 
     917                                    self.JobSizeBytes, self.JobMD5Sum, None, self.JobBillingCode) 
     918            self.printInfo(_("Job added to history.")) 
     919             
     920            if self.BillingCode and self.BillingCode.Exists : 
     921                self.BillingCode.consume(self.JobSize, self.JobPrice) 
     922                self.printInfo(_("Billing code %s was updated.") % self.BillingCode.BillingCode) 
     923        except :     
     924            self.storage.rollbackTransaction() 
     925            raise 
     926        else :     
     927            self.storage.commitTransaction() 
     928             
     929        # exports some new environment variables 
     930        self.exportJobSizeAndPrice() 
     931         
     932        # then re-export user information with new values 
     933        self.exportUserInfo() 
     934         
     935        # handle ending banner pages without accounting 
     936        if accountbanner in ["STARTING", "NONE"] : 
     937            self.handleBanner("ending", 0) 
     938                     
     939        self.launchPostHook() 
     940             
     941        return retcode     
     942                
     943    def printJobDatas(self) :            
     944        """Sends the job's datas to the real backend.""" 
     945        self.logdebug("Sending job's datas to real backend...") 
     946        if self.InputFile is None : 
     947            infile = open(self.DataFile, "rb") 
     948        else :     
     949            infile = None 
     950        self.runOriginalBackend(infile) 
     951        if self.InputFile is None : 
     952            infile.close() 
     953        self.logdebug("Job's datas sent to real backend.") 
     954         
     955    def runOriginalBackend(self, filehandle=None, isBanner=0) : 
     956        """Launches the original backend.""" 
     957        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend) 
     958        if not isBanner : 
     959            arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:] 
     960        else :     
     961            # For banners, we absolutely WANT 
     962            # to remove any filename from the command line ! 
     963            self.logdebug("It looks like we try to print a banner.") 
     964            arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:6] 
     965        arguments[2] = self.UserName # in case it was overwritten by external script 
     966        # TODO : do something about job-billing option, in case it was overwritten as well... 
     967         
     968        self.logdebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in arguments]))) 
     969        self.regainPriv()     
     970        pid = os.fork() 
     971        self.logdebug("Forked !") 
     972        if pid == 0 : 
     973            if filehandle is not None : 
     974                self.logdebug("Redirecting file handle to real backend's stdin") 
     975                os.dup2(filehandle.fileno(), 0) 
     976            try : 
     977                self.logdebug("Calling execve...") 
     978                os.execve(originalbackend, arguments, os.environ) 
     979            except OSError, msg : 
     980                self.logdebug("execve() failed: %s" % msg) 
     981            self.logdebug("We shouldn't be there !!!")     
     982            os._exit(-1) 
     983        self.dropPriv()     
     984         
     985        killed = 0 
     986        status = -1 
     987        while status == -1 : 
     988            try : 
     989                status = os.waitpid(pid, 0)[1] 
     990            except OSError, (err, msg) : 
     991                if (err == 4) and self.gotSigTerm : 
     992                    os.kill(pid, signal.SIGTERM) 
     993                    killed = 1 
     994                     
     995        if os.WIFEXITED(status) : 
     996            status = os.WEXITSTATUS(status) 
     997            if status : 
     998                level = "error" 
     999            else :     
     1000                level = "info" 
     1001            self.printInfo("CUPS backend %s returned %d." % \ 
     1002                                     (originalbackend, status), level) 
     1003            return status 
     1004        elif not killed : 
     1005            self.printInfo("CUPS backend %s died abnormally." % \ 
     1006                               originalbackend, "error") 
     1007            return -1 
     1008        else : 
     1009            self.printInfo("CUPS backend %s was killed." % \ 
     1010                               originalbackend, "warn") 
     1011            return 1 
     1012         
     1013if __name__ == "__main__" :     
     1014    # This is a CUPS backend, we should act and die like a CUPS backend 
     1015    wrapper = CUPSBackend() 
     1016    if len(sys.argv) == 1 : 
     1017        print "\n".join(wrapper.discoverOtherBackends()) 
     1018        sys.exit(0)                 
    7801019    elif len(sys.argv) not in (6, 7) :     
    781         sys.stderr.write("ERROR: %s job-id user title copies options [file]\n" % sys.argv[0]) 
    782         retcode = 1 
     1020        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\ 
     1021                              % sys.argv[0]) 
     1022        sys.exit(1) 
    7831023    else :     
    7841024        try : 
    785             # Initializes the backend 
    786             kotabackend = PyKotaBackend()     
    787             kotabackend.deferredInit() 
    788             retcode = kotabackend.mainWork() 
    789             kotabackend.storage.close() 
    790             kotabackend.closeJobDataStream()     
    791         except SystemExit :     
    792             retcode = -1 
    793         except : 
     1025            wrapper.deferredInit() 
     1026            wrapper.initBackendParameters() 
     1027            wrapper.saveDatasAndCheckSum() 
     1028            wrapper.accounter = openAccounter(wrapper) 
     1029            wrapper.precomputeJobSize() 
     1030            wrapper.overwriteJobAttributes() 
     1031            wrapper.exportJobInfo() 
     1032            retcode = wrapper.mainWork() 
     1033        except SystemExit, e :     
     1034            retcode = e.code 
     1035        except :     
    7941036            try : 
    795                 kotabackend.crashed("cupspykota backend failed") 
     1037                wrapper.crashed("cupspykota backend failed") 
    7961038            except :     
    7971039                crashed("cupspykota backend failed") 
    798             retcode = 1     
    799          
    800     sys.exit(retcode)     
     1040            retcode = 1 
     1041        wrapper.clean() 
     1042        sys.exit(retcode) 
  • pykota/trunk/NEWS

    r2399 r2409  
    2222PyKota NEWS : 
    2323        
     24    - 1.23alpha22 : 
     25     
     26        - The cupspykota backend was almost entirely rewritten from scratch. 
     27          It's now much more readable, maintainable, etc... 
     28           
     29        - LPRng support is OFFICIALLY dropped for now. LPRng users are   
     30          advised to download earlier releases or purchase the 
     31          PyKota v1.22HotFix1 Official release. LPRng support  
     32          MIGHT be re-added later. 
     33         
    2434    - 1.23alpha21 : 
    2535      
  • pykota/trunk/pykota/accounter.py

    r2302 r2409  
    4040        self.filter = kotafilter 
    4141        self.arguments = arguments 
    42         self.onerror = self.filter.config.getPrinterOnAccounterError(self.filter.printername) 
     42        self.onerror = self.filter.config.getPrinterOnAccounterError(self.filter.PrinterName) 
    4343        self.isSoftware = 1 # by default software accounting 
    4444         
     
    5454        # computes job's size 
    5555        self.JobSize = self.computeJobSize() 
    56         if ((self.filter.printingsystem == "CUPS") \ 
    57             and (self.filter.preserveinputfile is not None)) \ 
    58             or (self.filter.printingsystem != "CUPS") : 
    59             self.JobSize *= self.filter.copies 
     56        if self.filter.InputFile is not None : 
     57            self.JobSize *= self.filter.Copies 
    6058         
    6159        # get last job information for this printer 
     
    9088def openAccounter(kotafilter) : 
    9189    """Returns a connection handle to the appropriate accounter.""" 
    92     (backend, args) = kotafilter.config.getAccounterBackend(kotafilter.printername) 
     90    (backend, args) = kotafilter.config.getAccounterBackend(kotafilter.PrinterName) 
    9391    try : 
    9492        exec "from pykota.accounters import %s as accounterbackend" % backend.lower() 
  • pykota/trunk/pykota/accounters/hardware.py

    r2302 r2409  
    3838    def getPrinterInternalPageCounter(self) :     
    3939        """Returns the printer's internal page counter.""" 
    40         self.filter.logdebug("Reading printer %s's internal page counter..." % self.filter.printername) 
    41         counter = self.askPrinterPageCounter(self.filter.printerhostname) 
    42         self.filter.logdebug("Printer %s's internal page counter value is : %s" % (self.filter.printername, str(counter))) 
     40        self.filter.logdebug("Reading printer %s's internal page counter..." % self.filter.PrinterName) 
     41        counter = self.askPrinterPageCounter(self.filter.PrinterHostName) 
     42        self.filter.logdebug("Printer %s's internal page counter value is : %s" % (self.filter.PrinterName, str(counter))) 
    4343        return counter     
    4444         
     
    117117             
    118118        if printer is None : 
    119             raise PyKotaAccounterError, _("Unknown printer address in HARDWARE(%s) for printer %s") % (commandline, self.filter.printername) 
     119            raise PyKotaAccounterError, _("Unknown printer address in HARDWARE(%s) for printer %s") % (commandline, self.filter.PrinterName) 
    120120        while 1 :     
    121121            self.filter.printInfo(_("Launching HARDWARE(%s)...") % commandline) 
  • pykota/trunk/pykota/accounters/pjl.py

    r2378 r2409  
    143143                        # BUT the page counter increases !!! 
    144144                        # So we can probably quit being sure it is printing. 
    145                         self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.printername, "warn") 
     145                        self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.PrinterName, "warn") 
    146146                        break 
    147             self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.printername) 
     147            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.PrinterName) 
    148148            time.sleep(ITERATIONDELAY) 
    149149         
     
    163163            else :     
    164164                idle_num = 0 
    165             self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.printername) 
     165            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.PrinterName) 
    166166            time.sleep(ITERATIONDELAY) 
    167167     
     
    172172               (os.environ.get("PYKOTAACTION") != "DENY") and \ 
    173173               (os.environ.get("PYKOTAPHASE") == "AFTER") and \ 
    174                self.parent.filter.jobSizeBytes : 
     174               self.parent.filter.JobSizeBytes : 
    175175                self.waitPrinting() 
    176176            self.waitIdle()     
     
    179179                raise 
    180180            else :     
    181                 self.parent.filter.printInfo(_("PJL querying stage interrupted. Using latest value seen for internal page counter (%s) on printer %s.") % (self.printerInternalPageCounter, self.parent.filter.printername), "warn") 
     181                self.parent.filter.printInfo(_("PJL querying stage interrupted. Using latest value seen for internal page counter (%s) on printer %s.") % (self.printerInternalPageCounter, self.parent.filter.PrinterName), "warn") 
    182182        return self.printerInternalPageCounter 
    183183             
     
    191191        class fakeFilter : 
    192192            def __init__(self) : 
    193                 self.printername = "FakePrintQueue" 
    194                 self.jobSizeBytes = 1 
     193                self.PrinterName = "FakePrintQueue" 
     194                self.JobSizeBytes = 1 
    195195                 
    196196            def printInfo(self, msg, level="info") : 
  • pykota/trunk/pykota/accounters/snmp.py

    r2319 r2409  
    137137                            # BUT the page counter increases !!! 
    138138                            # So we can probably quit being sure it is printing. 
    139                             self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.printername, "warn") 
     139                            self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.PrinterName, "warn") 
    140140                            break 
    141                 self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.printername)     
     141                self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.PrinterName)     
    142142                time.sleep(ITERATIONDELAY) 
    143143             
     
    161161                else :     
    162162                    idle_num = 0 
    163                 self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.printername)     
     163                self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.PrinterName)     
    164164                time.sleep(ITERATIONDELAY) 
    165165                 
     
    170170                   (os.environ.get("PYKOTAACTION") != "DENY") and \ 
    171171                   (os.environ.get("PYKOTAPHASE") == "AFTER") and \ 
    172                    self.parent.filter.jobSizeBytes : 
     172                   self.parent.filter.JobSizeBytes : 
    173173                    self.waitPrinting() 
    174174                self.waitIdle()     
     
    177177                    raise 
    178178                else :     
    179                     self.parent.filter.printInfo(_("SNMP querying stage interrupted. Using latest value seen for internal page counter (%s) on printer %s.") % (self.printerInternalPageCounter, self.parent.filter.printername), "warn") 
     179                    self.parent.filter.printInfo(_("SNMP querying stage interrupted. Using latest value seen for internal page counter (%s) on printer %s.") % (self.printerInternalPageCounter, self.parent.filter.PrinterName), "warn") 
    180180            return self.printerInternalPageCounter 
    181181             
     
    189189        class fakeFilter : 
    190190            def __init__(self) : 
    191                 self.printername = "FakePrintQueue" 
    192                 self.jobSizeBytes = 1 
     191                self.PrinterName = "FakePrintQueue" 
     192                self.JobSizeBytes = 1 
    193193                 
    194194            def printInfo(self, msg, level="info") : 
  • pykota/trunk/pykota/accounters/software.py

    r2302 r2409  
    3636        else : 
    3737            MEGABYTE = 1024*1024 
    38             self.filter.jobdatastream.seek(0) 
     38            infile = open(self.filter.DataFile, "rb") 
    3939            child = popen2.Popen4(self.arguments) 
    4040            try : 
    41                 data = self.filter.jobdatastream.read(MEGABYTE)     
     41                data = infile.read(MEGABYTE)     
    4242                while data : 
    4343                    child.tochild.write(data) 
    44                     data = self.filter.jobdatastream.read(MEGABYTE) 
     44                    data = infile.read(MEGABYTE) 
    4545                child.tochild.flush() 
    4646                child.tochild.close()     
     
    4848                msg = "%s : %s" % (self.arguments, msg)  
    4949                self.filter.printInfo(_("Unable to compute job size with accounter %s") % msg) 
    50              
     50            infile.close() 
    5151            pagecounter = None 
    5252            try : 
  • pykota/trunk/pykota/storage.py

    r2388 r2409  
    296296        jobprice = self.computeJobPrice(jobsize) 
    297297        if jobsize : 
    298             self.parent.beginTransaction() 
    299             try : 
    300                 if jobprice : 
    301                     self.User.consumeAccountBalance(jobprice) 
    302                 for upq in [ self ] + self.ParentPrintersUserPQuota : 
    303                     self.parent.increaseUserPQuotaPagesCounters(upq, jobsize) 
    304                     upq.PageCounter = int(upq.PageCounter or 0) + jobsize 
    305                     upq.LifePageCounter = int(upq.LifePageCounter or 0) + jobsize 
    306             except PyKotaStorageError, msg :     
    307                 self.parent.rollbackTransaction() 
    308                 raise PyKotaStorageError, msg 
    309             else :     
    310                 self.parent.commitTransaction() 
     298            if jobprice : 
     299                self.User.consumeAccountBalance(jobprice) 
     300            for upq in [ self ] + self.ParentPrintersUserPQuota : 
     301                self.parent.increaseUserPQuotaPagesCounters(upq, jobsize) 
     302                upq.PageCounter = int(upq.PageCounter or 0) + jobsize 
     303                upq.LifePageCounter = int(upq.LifePageCounter or 0) + jobsize 
    311304        return jobprice 
    312305         
  • pykota/trunk/pykota/tool.py

    r2395 r2409  
    634634        return action         
    635635         
    636 class PyKotaFilterOrBackend(PyKotaTool) :     
    637     """Class for the PyKota filter or backend.""" 
    638     def __init__(self) : 
    639         """Initialize local datas from current environment.""" 
    640         # We begin with ignoring signals, we may de-ignore them later on. 
    641         self.gotSigTerm = 0 
    642         signal.signal(signal.SIGTERM, signal.SIG_IGN) 
    643         # signal.signal(signal.SIGCHLD, signal.SIG_IGN) 
    644         signal.signal(signal.SIGPIPE, signal.SIG_IGN) 
    645          
    646         PyKotaTool.__init__(self) 
    647         (self.printingsystem, \ 
    648          self.printerhostname, \ 
    649          self.printername, \ 
    650          self.username, \ 
    651          self.jobid, \ 
    652          self.inputfile, \ 
    653          self.copies, \ 
    654          self.title, \ 
    655          self.options, \ 
    656          self.originalbackend) = self.extractInfoFromCupsOrLprng() 
    657           
    658     def deferredInit(self) : 
    659         """Deferred initialization.""" 
    660         PyKotaTool.deferredInit(self) 
    661          
    662         arguments = " ".join(['"%s"' % arg for arg in sys.argv]) 
    663         self.logdebug(_("Printing system %s, args=%s") % (str(self.printingsystem), arguments)) 
    664          
    665         self.username = self.username or pwd.getpwuid(os.geteuid())[0] # use CUPS' user when printing test page from CUPS web interface, otherwise username is empty 
    666          
    667         if self.printingsystem == "CUPS" : 
    668             self.extractDatasFromCups() 
    669          
    670         (newusername, newbillingcode, newaction) = self.overwriteJobTicket() 
    671         if newusername : 
    672             self.printInfo(_("Job ticket overwritten : new username = [%s]") % newusername) 
    673             self.username = newusername 
    674         if newbillingcode : 
    675             self.printInfo(_("Job ticket overwritten : new billing code = [%s]") % newbillingcode) 
    676             self.overwrittenBillingCode = newbillingcode 
    677         else :     
    678             self.overwrittenBillingCode = None 
    679         if newaction :     
    680             self.printInfo(_("Job ticket overwritten : job will be denied (but a bit later).")) 
    681             self.mustDeny = 1 
    682         else :     
    683             self.mustDeny = 0 
    684          
    685         # do we want to strip out the Samba/Winbind domain name ? 
    686         separator = self.config.getWinbindSeparator() 
    687         if separator is not None : 
    688             self.username = self.username.split(separator)[-1] 
    689              
    690         # do we want to lowercase usernames ?     
    691         if self.config.getUserNameToLower() : 
    692             self.username = self.username.lower() 
    693              
    694         # do we want to strip some prefix off of titles ?     
    695         stripprefix = self.config.getStripTitle(self.printername) 
    696         if stripprefix : 
    697             if fnmatch.fnmatch(self.title[:len(stripprefix)], stripprefix) : 
    698                 self.logdebug("Prefix [%s] removed from job's title [%s]." % (stripprefix, self.title)) 
    699                 self.title = self.title[len(stripprefix):] 
    700              
    701         self.preserveinputfile = self.inputfile  
    702         try : 
    703             self.accounter = accounter.openAccounter(self) 
    704         except (config.PyKotaConfigError, accounter.PyKotaAccounterError), msg :     
    705             self.crashed(msg) 
    706             raise 
    707         self.exportJobInfo() 
    708         self.jobdatastream = self.openJobDataStream() 
    709         self.checksum = self.computeChecksum() 
    710         self.softwareJobSize = self.precomputeJobSize() 
    711         os.environ["PYKOTAPRECOMPUTEDJOBSIZE"] = str(self.softwareJobSize) 
    712         os.environ["PYKOTAJOBSIZEBYTES"] = str(self.jobSizeBytes) 
    713         self.logdebug("Job size is %s bytes on %s pages." % (self.jobSizeBytes, self.softwareJobSize)) 
    714         self.logdebug("Capturing SIGTERM events.") 
    715         signal.signal(signal.SIGTERM, self.sigterm_handler) 
    716          
    717     def overwriteJobTicket(self) :     
    718         """Should we overwrite the job's ticket (username and billingcode) ?""" 
    719         jobticketcommand = self.config.getOverwriteJobTicket(self.printername) 
    720         if jobticketcommand is not None : 
    721             username = billingcode = action = None 
    722             self.logdebug("Launching subprocess [%s] to overwrite the job's ticket." % jobticketcommand) 
    723             inputfile = os.popen(jobticketcommand, "r") 
    724             for line in inputfile.xreadlines() : 
    725                 line = line.strip() 
    726                 if line == "DENY" : 
    727                     self.logdebug("Seen DENY command.") 
    728                     action = "DENY" 
    729                 elif line.startswith("USERNAME=") :     
    730                     username = line.split("=", 1)[1].strip() 
    731                     self.logdebug("Seen new username [%s]" % username) 
    732                     action = None 
    733                 elif line.startswith("BILLINGCODE=") :     
    734                     billingcode = line.split("=", 1)[1].strip() 
    735                     self.logdebug("Seen new billing code [%s]" % billingcode) 
    736                     action = None 
    737             inputfile.close()     
    738             return (username, billingcode, action) 
    739         else : 
    740             return (None, None, None) 
    741          
    742     def sendBackChannelData(self, message, level="info") :     
    743         """Sends an informational message to CUPS via back channel stream (stderr).""" 
    744         sys.stderr.write("%s: PyKota (PID %s) : %s\n" % (level.upper(), os.getpid(), message.strip())) 
    745         sys.stderr.flush() 
    746          
    747     def computeChecksum(self) :     
    748         """Computes the MD5 checksum of the job's datas, to be able to detect and forbid duplicate jobs.""" 
    749         self.logdebug("Computing MD5 checksum for job %s" % self.jobid) 
    750         MEGABYTE = 1024*1024 
    751         checksum = md5.new() 
    752         while 1 : 
    753             data = self.jobdatastream.read(MEGABYTE)  
    754             if not data : 
    755                 break 
    756             checksum.update(data)     
    757         self.jobdatastream.seek(0) 
    758         digest = checksum.hexdigest() 
    759         self.logdebug("MD5 checksum for job %s is %s" % (self.jobid, digest)) 
    760         os.environ["PYKOTAMD5SUM"] = digest 
    761         return digest 
    762          
    763     def openJobDataStream(self) :     
    764         """Opens the file which contains the job's datas.""" 
    765         if self.preserveinputfile is None : 
    766             # Job comes from sys.stdin, but this is not 
    767             # seekable and complexifies our task, so create 
    768             # a temporary file and use it instead 
    769             self.logdebug("Duplicating data stream from stdin to temporary file") 
    770             dummy = 0 
    771             MEGABYTE = 1024*1024 
    772             self.jobSizeBytes = 0 
    773             infile = tempfile.TemporaryFile() 
    774             while 1 : 
    775                 data = sys.stdin.read(MEGABYTE)  
    776                 if not data : 
    777                     break 
    778                 self.jobSizeBytes += len(data)     
    779                 if not (dummy % 10) : 
    780                     self.logdebug("%s bytes read..." % self.jobSizeBytes) 
    781                 dummy += 1     
    782                 infile.write(data) 
    783             self.logdebug("%s bytes read total." % self.jobSizeBytes) 
    784             infile.flush()     
    785             infile.seek(0) 
    786             return infile 
    787         else :     
    788             # real file, just open it 
    789             self.regainPriv() 
    790             self.logdebug("Opening data stream %s" % self.preserveinputfile) 
    791             self.jobSizeBytes = os.stat(self.preserveinputfile)[6] 
    792             infile = open(self.preserveinputfile, "rb") 
    793             self.dropPriv() 
    794             return infile 
    795          
    796     def closeJobDataStream(self) :     
    797         """Closes the file which contains the job's datas.""" 
    798         self.logdebug("Closing data stream.") 
    799         try : 
    800             self.jobdatastream.close() 
    801         except :     
    802             pass 
    803          
    804     def precomputeJobSize(self) :     
    805         """Computes the job size with a software method.""" 
    806         self.logdebug("Precomputing job's size with generic PDL analyzer...") 
    807         self.jobdatastream.seek(0) 
    808         try : 
    809             parser = analyzer.PDLAnalyzer(self.jobdatastream) 
    810             jobsize = parser.getJobSize() 
    811         except pdlparser.PDLParserError, msg :     
    812             # Here we just log the failure, but 
    813             # we finally ignore it and return 0 since this 
    814             # computation is just an indication of what the 
    815             # job's size MAY be. 
    816             self.printInfo(_("Unable to precompute the job's size with the generic PDL analyzer : %s") % msg, "warn") 
    817             return 0 
    818         else :     
    819             if ((self.printingsystem == "CUPS") \ 
    820                 and (self.preserveinputfile is not None)) \ 
    821                 or (self.printingsystem != "CUPS") : 
    822                 return jobsize * self.copies 
    823             else :         
    824                 return jobsize 
    825              
    826     def sigterm_handler(self, signum, frame) : 
    827         """Sets an attribute whenever SIGTERM is received.""" 
    828         self.gotSigTerm = 1 
    829         os.environ["PYKOTASTATUS"] = "CANCELLED" 
    830         self.printInfo(_("SIGTERM received, job %s cancelled.") % self.jobid) 
    831          
    832     def exportJobInfo(self) :     
    833         """Exports job information to the environment.""" 
    834         os.environ["PYKOTAUSERNAME"] = str(self.username) 
    835         os.environ["PYKOTAPRINTERNAME"] = str(self.printername) 
    836         os.environ["PYKOTAJOBID"] = str(self.jobid) 
    837         os.environ["PYKOTATITLE"] = self.title or "" 
    838         os.environ["PYKOTAFILENAME"] = self.preserveinputfile or "" 
    839         os.environ["PYKOTACOPIES"] = str(self.copies) 
    840         os.environ["PYKOTAOPTIONS"] = self.options or "" 
    841         os.environ["PYKOTAPRINTERHOSTNAME"] = self.printerhostname or "localhost" 
    842      
    843     def exportUserInfo(self, userpquota) : 
    844         """Exports user information to the environment.""" 
    845         os.environ["PYKOTAOVERCHARGE"] = str(userpquota.User.OverCharge) 
    846         os.environ["PYKOTALIMITBY"] = str(userpquota.User.LimitBy) 
    847         os.environ["PYKOTABALANCE"] = str(userpquota.User.AccountBalance or 0.0) 
    848         os.environ["PYKOTALIFETIMEPAID"] = str(userpquota.User.LifeTimePaid or 0.0) 
    849         os.environ["PYKOTAPAGECOUNTER"] = str(userpquota.PageCounter or 0) 
    850         os.environ["PYKOTALIFEPAGECOUNTER"] = str(userpquota.LifePageCounter or 0) 
    851         os.environ["PYKOTASOFTLIMIT"] = str(userpquota.SoftLimit) 
    852         os.environ["PYKOTAHARDLIMIT"] = str(userpquota.HardLimit) 
    853         os.environ["PYKOTADATELIMIT"] = str(userpquota.DateLimit) 
    854         os.environ["PYKOTAWARNCOUNT"] = str(userpquota.WarnCount) 
    855          
    856         # not really an user information, but anyway 
    857         # exports the list of printers groups the current 
    858         # printer is a member of 
    859         os.environ["PYKOTAPGROUPS"] = ",".join([p.Name for p in self.storage.getParentPrinters(userpquota.Printer)]) 
    860          
    861     def prehook(self, userpquota) : 
    862         """Allows plugging of an external hook before the job gets printed.""" 
    863         prehook = self.config.getPreHook(userpquota.Printer.Name) 
    864         if prehook : 
    865             self.logdebug("Executing pre-hook [%s]" % prehook) 
    866             os.system(prehook) 
    867          
    868     def posthook(self, userpquota) : 
    869         """Allows plugging of an external hook after the job gets printed and/or denied.""" 
    870         posthook = self.config.getPostHook(userpquota.Printer.Name) 
    871         if posthook : 
    872             self.logdebug("Executing post-hook [%s]" % posthook) 
    873             os.system(posthook) 
    874              
    875     def printInfo(self, message, level="info") :         
    876         """Sends a message to standard error.""" 
    877         self.logger.log_message("%s" % message, level) 
    878          
    879     def printMoreInfo(self, user, printer, message, level="info") :             
    880         """Prefixes the information printed with 'user@printer(jobid) =>'.""" 
    881         self.printInfo("%s@%s(%s) => %s" % (getattr(user, "Name", None), getattr(printer, "Name", None), self.jobid, message), level) 
    882          
    883     def extractInfoFromCupsOrLprng(self) :     
    884         """Returns a tuple (printingsystem, printerhostname, printername, username, jobid, filename, title, options, backend). 
    885          
    886            Returns (None, None, None, None, None, None, None, None, None, None) if no printing system is recognized. 
    887         """ 
    888         # Try to detect CUPS 
    889         if os.environ.has_key("CUPS_SERVERROOT") and os.path.isdir(os.environ.get("CUPS_SERVERROOT", "")) : 
    890             if len(sys.argv) == 7 : 
    891                 inputfile = sys.argv[6] 
    892             else :     
    893                 inputfile = None 
    894                  
    895             # check that the DEVICE_URI environment variable's value is  
    896             # prefixed with "cupspykota:" otherwise don't touch it. 
    897             # If this is the case, we have to remove the prefix from  
    898             # the environment before launching the real backend in cupspykota 
    899             device_uri = os.environ.get("DEVICE_URI", "") 
    900             if device_uri.startswith("cupspykota:") : 
    901                 fulldevice_uri = device_uri[:] 
    902                 device_uri = fulldevice_uri[len("cupspykota:"):] 
    903                 if device_uri.startswith("//") :    # lpd (at least) 
    904                     device_uri = device_uri[2:] 
    905                 os.environ["DEVICE_URI"] = device_uri   # TODO : side effect ! 
    906             # TODO : check this for more complex urls than ipp://myprinter.dot.com:631/printers/lp 
    907             try : 
    908                 (backend, destination) = device_uri.split(":", 1)  
    909             except ValueError :     
    910                 raise PyKotaToolError, "Invalid DEVICE_URI : %s\n" % device_uri 
    911             while destination.startswith("/") : 
    912                 destination = destination[1:] 
    913             checkauth = destination.split("@", 1)     
    914             if len(checkauth) == 2 : 
    915                 destination = checkauth[1] 
    916             printerhostname = destination.split("/")[0].split(":")[0] 
    917             return ("CUPS", \ 
    918                     printerhostname, \ 
    919                     os.environ.get("PRINTER"), \ 
    920                     sys.argv[2].strip(), \ 
    921                     sys.argv[1].strip(), \ 
    922                     inputfile, \ 
    923                     int(sys.argv[4].strip()), \ 
    924                     sys.argv[3], \ 
    925                     sys.argv[5], \ 
    926                     backend) 
    927         else :     
    928             # Try to detect LPRng 
    929             # TODO : try to extract filename, options if available 
    930             jseen = Jseen = Pseen = nseen = rseen = Kseen = None 
    931             for arg in sys.argv : 
    932                 if arg.startswith("-j") : 
    933                     jseen = arg[2:].strip() 
    934                 elif arg.startswith("-n") :      
    935                     nseen = arg[2:].strip() 
    936                 elif arg.startswith("-P") :     
    937                     Pseen = arg[2:].strip() 
    938                 elif arg.startswith("-r") :     
    939                     rseen = arg[2:].strip() 
    940                 elif arg.startswith("-J") :     
    941                     Jseen = arg[2:].strip() 
    942                 elif arg.startswith("-K") or arg.startswith("-#") :     
    943                     Kseen = int(arg[2:].strip()) 
    944             if Kseen is None :         
    945                 Kseen = 1       # we assume the user wants at least one copy... 
    946             if (rseen is None) and jseen and Pseen and nseen :     
    947                 lparg = [arg for arg in "".join(os.environ.get("PRINTCAP_ENTRY", "").split()).split(":") if arg.startswith("rm=") or arg.startswith("lp=")] 
    948                 try : 
    949                     rseen = lparg[0].split("=")[-1].split("@")[-1].split("%")[0] 
    950                 except :     
    951                     # Not found 
    952                     self.printInfo(_("Printer hostname undefined, set to 'localhost'"), "warn") 
    953                     rseen = "localhost" 
    954                  
    955             spooldir = os.environ.get("SPOOL_DIR", ".")     
    956             df_name = os.environ.get("DATAFILES") 
    957             if not df_name : 
    958                 try :  
    959                     df_name = [line[10:] for line in os.environ.get("HF", "").split() if line.startswith("datafiles=")][0] 
    960                 except IndexError :     
    961                     try :     
    962                         df_name = [line[8:] for line in os.environ.get("HF", "").split() if line.startswith("df_name=")][0] 
    963                     except IndexError : 
    964                         try : 
    965                             cftransfername = [line[15:] for line in os.environ.get("HF", "").split() if line.startswith("cftransfername=")][0] 
    966                         except IndexError :     
    967                             try : 
    968                                 df_name = [line[1:] for line in os.environ.get("CONTROL", "").split() if line.startswith("fdf") or line.startswith("Udf")][0] 
    969                             except IndexError :     
    970                                 raise PyKotaToolError, "Unable to find the file which holds the job's datas. Please file a bug report for PyKota." 
    971                             else :     
    972                                 inputfile = os.path.join(spooldir, df_name) # no need to strip() 
    973                         else :     
    974                             inputfile = os.path.join(spooldir, "d" + cftransfername[1:]) # no need to strip() 
    975                     else :     
    976                         inputfile = os.path.join(spooldir, df_name) # no need to strip() 
    977                 else :     
    978                     inputfile = os.path.join(spooldir, df_name) # no need to strip() 
    979             else :     
    980                 inputfile = os.path.join(spooldir, df_name.strip()) 
    981                  
    982             if jseen and Pseen and nseen and rseen :         
    983                 options = os.environ.get("HF", "") or os.environ.get("CONTROL", "") 
    984                 return ("LPRNG", rseen, Pseen, nseen, jseen, inputfile, Kseen, Jseen, options, None) 
    985         self.printInfo(_("Printing system unknown, args=%s") % " ".join(sys.argv), "warn") 
    986         return (None, None, None, None, None, None, None, None, None, None)   # Unknown printing system 
    987          
    988     def getPrinterUserAndUserPQuota(self) :         
    989         """Returns a tuple (policy, printer, user, and user print quota) on this printer. 
    990          
    991            "OK" is returned in the policy if both printer, user and user print quota 
    992            exist in the Quota Storage. 
    993            Otherwise, the policy as defined for this printer in pykota.conf is returned. 
    994             
    995            If policy was set to "EXTERNAL" and one of printer, user, or user print quota 
    996            doesn't exist in the Quota Storage, then an external command is launched, as 
    997            defined in the external policy for this printer in pykota.conf 
    998            This external command can do anything, like automatically adding printers 
    999            or users, for example, and finally extracting printer, user and user print 
    1000            quota from the Quota Storage is tried a second time. 
    1001             
    1002            "EXTERNALERROR" is returned in case policy was "EXTERNAL" and an error status 
    1003            was returned by the external command. 
    1004         """ 
    1005         for passnumber in range(1, 3) : 
    1006             printer = self.storage.getPrinter(self.printername) 
    1007             user = self.storage.getUser(self.username) 
    1008             userpquota = self.storage.getUserPQuota(user, printer) 
    1009             if printer.Exists and user.Exists and userpquota.Exists : 
    1010                 policy = "OK" 
    1011                 break 
    1012             (policy, args) = self.config.getPrinterPolicy(self.printername) 
    1013             if policy == "EXTERNAL" :     
    1014                 commandline = self.formatCommandLine(args, user, printer) 
    1015                 if not printer.Exists : 
    1016                     self.printInfo(_("Printer %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.printername, commandline, self.printername)) 
    1017                 if not user.Exists : 
    1018                     self.printInfo(_("User %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.username, commandline, self.printername)) 
    1019                 if not userpquota.Exists : 
    1020                     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)) 
    1021                 if os.system(commandline) : 
    1022                     self.printInfo(_("External policy %s for printer %s produced an error. Job rejected. Please check PyKota's configuration files.") % (commandline, self.printername), "error") 
    1023                     policy = "EXTERNALERROR" 
    1024                     break 
    1025             else :         
    1026                 if not printer.Exists : 
    1027                     self.printInfo(_("Printer %s not registered in the PyKota system, applying default policy (%s)") % (self.printername, policy)) 
    1028                 if not user.Exists : 
    1029                     self.printInfo(_("User %s not registered in the PyKota system, applying default policy (%s) for printer %s") % (self.username, policy, self.printername)) 
    1030                 if not userpquota.Exists : 
    1031                     self.printInfo(_("User %s doesn't have quota on printer %s in the PyKota system, applying default policy (%s)") % (self.username, self.printername, policy)) 
    1032                 break 
    1033         if policy == "EXTERNAL" :     
    1034             if not printer.Exists : 
    1035                 self.printInfo(_("Printer %s still not registered in the PyKota system, job will be rejected") % self.printername) 
    1036             if not user.Exists : 
    1037                 self.printInfo(_("User %s still not registered in the PyKota system, job will be rejected on printer %s") % (self.username, self.printername)) 
    1038             if not userpquota.Exists : 
    1039                 self.printInfo(_("User %s still doesn't have quota on printer %s in the PyKota system, job will be rejected") % (self.username, self.printername)) 
    1040         return (policy, printer, user, userpquota) 
    1041          
    1042     def mainWork(self) :     
    1043         """Main work is done here.""" 
    1044         (policy, printer, user, userpquota) = self.getPrinterUserAndUserPQuota() 
    1045         # TODO : check for last user's quota in case pykota filter is used with querying 
    1046         if policy == "EXTERNALERROR" : 
    1047             # Policy was 'EXTERNAL' and the external command returned an error code 
    1048             return self.removeJob() 
    1049         elif policy == "EXTERNAL" : 
    1050             # Policy was 'EXTERNAL' and the external command wasn't able 
    1051             # to add either the printer, user or user print quota 
    1052             return self.removeJob() 
    1053         elif policy == "DENY" :     
    1054             # Either printer, user or user print quota doesn't exist, 
    1055             # and the job should be rejected. 
    1056             return self.removeJob() 
    1057         else : 
    1058             if policy not in ("OK", "ALLOW") : 
    1059                 self.printInfo(_("Invalid policy %s for printer %s") % (policy, self.printername)) 
    1060                 return self.removeJob() 
    1061             else : 
    1062                 return self.doWork(policy, printer, user, userpquota)