Changeset 1495
- Timestamp:
- 05/25/04 00:45:49 (21 years ago)
- Location:
- pykota/trunk
- Files:
-
- 9 modified
Legend:
- Unmodified
- Added
- Removed
-
pykota/trunk/bin/cupspykota
r1494 r1495 24 24 # 25 25 # $Log$ 26 # Revision 1.49 2004/05/24 22:45:48 jalet 27 # New 'enforcement' directive added 28 # Polling loop improvements 29 # 26 30 # Revision 1.48 2004/05/24 14:36:24 jalet 27 31 # Revert to old polling loop. Will need optimisations … … 252 256 253 257 # checks the user's quota 258 self.softwareJobPrice = userpquota.computeJobPrice(self.softwareJobSize) 254 259 action = self.warnUserPQuota(userpquota) 255 260 … … 311 316 return retcode 312 317 313 def setNonBlocking(self, fno) :314 """Sets a file handle to be non-blocking."""315 flags = fcntl.fcntl(fno, fcntl.F_GETFL, 0)316 fcntl.fcntl(fno, fcntl.F_SETFL, flags | os.O_NONBLOCK)317 318 318 def unregisterFileNo(self, pollobj, fileno) : 319 319 """Removes a file handle from the polling object.""" … … 327 327 self.logdebug("File number %s unregistered from polling object." % fileno) 328 328 329 def formatFileEvent(self, fd, mask , ins, outs) :329 def formatFileEvent(self, fd, mask) : 330 330 """Formats file debug info.""" 331 try : 332 name = ins.get(fd, outs.get(fd))["name"] 333 except KeyError : 334 self.logdebug("File %s not found in %s or %s" % (fd, repr(ins), repr(outs))) 335 else : 336 maskval = [] 337 if mask & select.POLLIN : 338 maskval.append("POLLIN") 339 if mask & select.POLLOUT : 340 maskval.append("POLLOUT") 341 if mask & select.POLLPRI : 342 maskval.append("POLLPRI") 343 if mask & select.POLLERR : 344 maskval.append("POLLERR") 345 if mask & select.POLLHUP : 346 maskval.append("POLLHUP") 347 if mask & select.POLLNVAL : 348 maskval.append("POLLNVAL") 349 return "%s (%s)" % (name, " | ".join(maskval)) 331 maskval = [] 332 if mask & select.POLLIN : 333 maskval.append("POLLIN") 334 if mask & select.POLLOUT : 335 maskval.append("POLLOUT") 336 if mask & select.POLLPRI : 337 maskval.append("POLLPRI") 338 if mask & select.POLLERR : 339 maskval.append("POLLERR") 340 if mask & select.POLLHUP : 341 maskval.append("POLLHUP") 342 if mask & select.POLLNVAL : 343 maskval.append("POLLNVAL") 344 return "%s (%s)" % (fd, " | ".join(maskval)) 350 345 351 346 def handleData(self) : … … 378 373 if self.preserveinputfile is None : 379 374 # this is not a real file, we read the job's data 380 # from stdin375 # from our temporary file which is a copy of stdin 381 376 infno = self.jobdatastream.fileno() 382 377 self.jobdatastream.seek(0) … … 389 384 endinput = 1 390 385 391 self.logdebug("Ca tching SIGTERM.")386 self.logdebug("Capturing SIGTERM events.") 392 387 signal.signal(signal.SIGTERM, self.sigterm_handler) 393 388 389 self.logdebug("Entering streams polling loop...") 390 MEGABYTE = 1024*1024 394 391 killed = 0 395 self.logdebug("Entering streams polling loop...")396 392 status = -1 397 while status == -1:393 while (status == -1) and (not killed) and not (inputclosed and outputclosed) : 398 394 # First check if original backend is still alive 399 395 status = subprocess.poll() … … 404 400 try : 405 401 os.kill(subprocess.pid, signal.SIGTERM) 402 except OSError, msg : # ignore but logs if process was already killed. 403 self.logdebug("Error while sending signal to pid %s" % subprocess.pid) 404 else : 406 405 self.logger.log_message(_("SIGTERM was sent to real backend %s (pid: %s)") % (realbackend, subprocess.pid), "info") 407 406 killed = 1 408 except : # ignore if process was already killed.409 pass410 407 411 408 # In any case, deal with any remaining I/O 412 409 availablefds = pollster.poll(5000) 413 for (fd, mask) in availablefds : 414 # self.logdebug("file: %i mask: %04x" % (fd, mask)) 415 try : 416 if mask & select.POLLOUT : 417 # We can write 418 if fd == tocfno : 419 if indata : 420 os.write(fd, indata) 421 try : 422 os.fsync(fd) 423 except OSError : 424 pass 425 indata = "" 426 if endinput : 427 self.unregisterFileNo(pollster, tocfno) 428 self.logdebug("Closing real backend's stdin.") 429 os.close(tocfno) 430 inputclosed = 1 431 elif fd == stderrfno : 432 if outdata : 433 os.write(fd, outdata) 434 try : 435 os.fsync(fd) 436 except OSError : 437 pass 438 outdata = "" 439 if endoutput : 440 self.unregisterFileNo(pollster, stderrfno) 441 outputclosed = 1 442 if (mask & select.POLLIN) or (mask & select.POLLPRI) : 443 # We have something to read 444 try : 445 data = os.read(fd, 256 * 1024) 446 except OSError, msg : 447 self.logdebug("Error while reading file %s : %s" % (fd, msg)) 448 else : 410 if not availablefds : 411 self.logdebug("Nothing to do, sleeping a bit...") 412 time.sleep(0.01) # give some time to the system 413 else : 414 for (fd, mask) in availablefds : 415 # self.logdebug(self.formatFileEvent(fd, mask)) 416 try : 417 if mask & select.POLLOUT : 418 # We can write 419 if fd == tocfno : 420 if indata : 421 try : 422 os.write(fd, indata) 423 except IOError, msg : 424 self.logdebug("Error while writing to real backend's stdin %s : %s" % (fd, msg)) 425 else : 426 indata = "" 427 if endinput : 428 self.unregisterFileNo(pollster, tocfno) 429 self.logdebug("Closing real backend's stdin.") 430 os.close(tocfno) 431 inputclosed = 1 432 elif fd == stderrfno : 433 if outdata : 434 try : 435 os.write(fd, outdata) 436 except IOError, msg : 437 self.logdebug("Error while writing to CUPS back channel (stderr) %s : %s" % (fd, msg)) 438 else : 439 outdata = "" 440 if endoutput : 441 self.unregisterFileNo(pollster, stderrfno) 442 outputclosed = 1 443 if mask & (select.POLLIN | select.POLLPRI) : 444 # We have something to read 445 try : 446 data = os.read(fd, MEGABYTE) 447 except (IOError, OSError), msg : 448 self.logdebug("Error while reading file %s : %s" % (fd, msg)) 449 else : 450 if fd == infno : 451 indata += data 452 if not data : # If yes, then no more input data 453 self.unregisterFileNo(pollster, infno) 454 self.logdebug("Input data ends.") 455 endinput = 1 # this happens with real files. 456 elif fd == fromcfno : 457 outdata += data 458 if mask & (select.POLLHUP | select.POLLERR) : 459 # Treat POLLERR as an EOF. 460 # Some standard I/O stream has no more datas 461 self.unregisterFileNo(pollster, fd) 449 462 if fd == infno : 450 indata += data 451 if not data : # If yes, then no more input data 452 self.unregisterFileNo(pollster, infno) 453 self.logdebug("Input data ends.") 454 endinput = 1 # this happens with real files. 455 elif fd == fromcfno : 456 outdata += data 457 if (mask & select.POLLHUP) or (mask & select.POLLERR) : 458 # I've never seen POLLERR myself, but this probably 459 # can't hurt to treat an error condition just like 460 # an EOF. 461 # 462 # Some standard I/O stream has no more datas 463 self.unregisterFileNo(pollster, fd) 464 if fd == infno : 465 # Here we are in the case where the input file is stdin. 466 # which has no more data to be read. 467 self.logdebug("Input data ends.") 468 endinput = 1 469 elif fd == fromcfno : 470 # We are no more interested in this file descriptor 471 self.logdebug("Closing real backend's stdout+stderr.") 472 os.close(fromcfno) 473 endoutput = 1 474 except IOError : 475 pass # we got signalled during an I/O it seems 476 if killed or (inputclosed and outputclosed) : 477 break 463 # Here we are in the case where the input file is stdin. 464 # which has no more data to be read. 465 self.logdebug("Input data ends.") 466 endinput = 1 467 elif fd == fromcfno : 468 # We are no more interested in this file descriptor 469 self.logdebug("Closing real backend's stdout+stderr.") 470 os.close(fromcfno) 471 endoutput = 1 472 473 if mask & select.POLLNVAL : 474 self.logdebug("File %s was closed. Unregistering from polling object." % fd) 475 self.unregisterFileNo(pollster, fd) 476 except IOError, msg : 477 self.logdebug("Got an IOError : %s" % msg) # we got signalled during an I/O 478 478 479 479 # We must close the real backend's input stream … … 491 491 # we exited the loop before the real backend exited 492 492 # now we have to wait for it to finish and get its status 493 self.logdebug("Waiting for real backend to exit...") 493 494 try : 494 495 status = subprocess.wait() -
pykota/trunk/conf/pykota.conf.sample
r1483 r1495 425 425 # PYKOTAPHASE environment variable. 426 426 # Pre and Post Hooks can be defined either globally, per printer, 427 # or both if both are defined, the printer specific hook has427 # or both. If both are defined, the printer specific hook has 428 428 # priority. 429 429 # … … 476 476 #posthook: /usr/bin/printenv >/tmp/after 477 477 478 # How should enforcement be done for this printer ? 479 # 480 # "laxist" is the default if value is not set, and allows users 481 # to be over quota on their last job. 482 # 483 # "strict" tries to prevent users from ever being over quota. 484 # 485 # Enforcement can be defined either globally, per printer, 486 # or both. If both are defined, the printer specific enforcement 487 # setting has priority. 488 # 489 # valid values : "strict" or "laxist" 490 # 491 # default value 492 # enforcement : laxist 493 enforcement : strict -
pykota/trunk/NEWS
r1494 r1495 22 22 PyKota NEWS : 23 23 24 - 1.19alpha14 : 25 26 - New 'enforcement' directive which accepts either 27 STRICT or LAXIST. See sample configuration file 28 for details. 29 24 30 - 1.19alpha13 : 25 31 -
pykota/trunk/pykota/accounter.py
r1483 r1495 22 22 # 23 23 # $Log$ 24 # Revision 1.15 2004/05/24 22:45:49 jalet 25 # New 'enforcement' directive added 26 # Polling loop improvements 27 # 24 28 # Revision 1.14 2004/05/18 14:49:19 jalet 25 29 # Big code changes to completely remove the need for "requester" directives, … … 84 88 self.filter = kotafilter 85 89 self.arguments = arguments 86 self.isDelayed = 0 # Accounting is immediate by default87 90 self.firstPassSize = None 88 91 … … 138 141 return 0 139 142 140 def doAccounting(self, userpquota) :141 """Does accounting for current job."""142 self.beginJob(userpquota)143 144 # Is the current user allowed to print at all ?145 action = self.filter.warnUserPQuota(userpquota)146 147 # update the quota for the current user on this printer, if allowed to print148 if action == "DENY" :149 jobsize = 0150 else :151 # get the job size152 jobsize = self.getJobSize()153 userpquota.increasePagesUsage(jobsize)154 155 # adds the current job to history156 jobprice = userpquota.computeJobPrice(jobsize)157 userpquota.Printer.addJobToHistory(self.filter.jobid, userpquota.User, self.getLastPageCounter(), action, jobsize, jobprice, self.filter.preserveinputfile, self.filter.title, self.filter.copies, self.filter.options)158 self.endJob(userpquota)159 return action160 161 143 def computeJobSize(self) : 162 144 """Must be overriden in children classes.""" 163 145 raise RuntimeError, "AccounterBase.computeJobSize() must be overriden !" 164 165 146 166 147 def openAccounter(kotafilter) : -
pykota/trunk/pykota/accounters/hardware.py
r1494 r1495 22 22 # 23 23 # $Log$ 24 # Revision 1.4 2004/05/24 22:45:49 jalet 25 # New 'enforcement' directive added 26 # Polling loop improvements 27 # 24 28 # Revision 1.3 2004/05/24 14:36:40 jalet 25 29 # Revert to old polling loop. Will need optimisations … … 44 48 """Initializes querying accounter.""" 45 49 AccounterBase.__init__(self, kotabackend, arguments) 46 self.isDelayed = 1 # With the pykota filter, accounting is delayed by one job47 50 48 51 def getPrinterInternalPageCounter(self) : … … 92 95 return jobsize 93 96 94 def doAccounting(self, userpquota) :95 """Does print accounting and returns if the job status is ALLOW or DENY."""96 # Get the page counter directly from the printer itself97 counterbeforejob = self.getPrinterInternalPageCounter() or 098 99 # Is the current user allowed to print at all ?100 action = self.filter.warnUserPQuota(userpquota)101 102 # adds the current job to history103 userpquota.Printer.addJobToHistory(self.filter.jobid, userpquota.User, counterbeforejob, action, filename=self.filter.preserveinputfile, title=self.filter.title, copies=self.filter.copies, options=self.filter.options)104 return action105 106 97 def askPrinterPageCounter(self, printer) : 107 98 """Returns the page counter from the printer via an external command. -
pykota/trunk/pykota/accounters/software.py
r1483 r1495 22 22 # 23 23 # $Log$ 24 # Revision 1.3 2004/05/24 22:45:49 jalet 25 # New 'enforcement' directive added 26 # Polling loop improvements 27 # 24 28 # Revision 1.2 2004/05/18 14:49:23 jalet 25 29 # Big code changes to completely remove the need for "requester" directives, … … 30 34 # 31 35 # 32 #33 36 34 37 import sys 35 38 import os 36 39 import popen2 37 import tempfile38 40 from pykota.accounter import AccounterBase, PyKotaAccounterError 39 41 … … 41 43 def computeJobSize(self) : 42 44 """Feeds an external command with our datas to let it compute the job size, and return its value.""" 43 temporary = None 44 if self.filter.inputfile is None : 45 infile = sys.stdin 46 # we will have to duplicate our standard input 47 temporary = tempfile.TemporaryFile() 48 else : 49 infile = open(self.filter.inputfile, "rb") 50 51 # launches software accounter 52 # TODO : USE tempfile.mkstemp() instead ! Needs some work ! 53 infilename = tempfile.mktemp() 54 outfilename = tempfile.mktemp() 55 errfilename = tempfile.mktemp() 45 self.filter.logdebug("Launching software accounter %s" % self.arguments) 46 MEGABYTE = 1024*1024 47 self.filter.jobdatastream.seek(0) 48 child = popen2.Popen4(self.arguments) 49 try : 50 data = self.filter.jobdatastream.read(MEGABYTE) 51 while data : 52 child.tochild.write(data) 53 data = self.filter.jobdatastream.read(MEGABYTE) 54 child.tochild.flush() 55 child.tochild.close() 56 except (IOError, OSError), msg : 57 msg = "%s : %s" % (self.arguments, msg) 58 self.filter.logger.log_message(_("Unable to compute job size with accounter %s") % msg) 59 60 pagecount = 0 61 try : 62 pagecount = int(child.fromchild.readline().strip()) 63 except (AttributeError, ValueError) : 64 self.filter.logger.log_message(_("Unable to compute job size with accounter %s") % self.arguments) 65 except (IOError, OSError), msg : 66 msg = "%s : %s" % (self.arguments, msg) 67 self.filter.logger.log_message(_("Unable to compute job size with accounter %s") % msg) 68 child.fromchild.close() 56 69 57 70 try : 58 # feed it with our data 59 fakeinput = open(infilename, "wb") 60 data = infile.read(256*1024) 61 while data : 62 fakeinput.write(data) 63 if temporary is not None : 64 temporary.write(data) 65 data = infile.read(256*1024) 66 fakeinput.close() 67 68 # launches child process 69 command = "%s <%s >%s 2>%s" % (self.arguments, infilename, outfilename, errfilename) 70 retcode = os.system(command) 71 72 # check exit status 73 if (os.WIFEXITED(retcode) and not os.WEXITSTATUS(retcode)) or os.stat(errfilename) : 74 # tries to extract the job size from the software accounter's 75 # standard output 76 childoutput = open(outfilename, "r") 77 try : 78 pagecount = int(childoutput.readline().strip()) 79 except (AttributeError, ValueError) : 80 self.filter.logger.log_message(_("Unable to compute job size with accounter %s") % self.arguments) 81 pagecount = 0 82 childoutput.close() 83 else : 84 self.filter.logger.log_message(_("Unable to compute job size with accounter %s") % self.arguments) 85 pagecount = 0 86 os.remove(infilename) 87 os.remove(outfilename) 88 os.remove(errfilename) 89 except IOError, msg : 90 # TODO : temporary files may remain on the filesystem... 91 msg = "%s : %s" % (self.arguments, msg) 92 self.filter.logger.log_message(_("Unable to compute job size with accounter %s") % msg) 93 pagecount = 0 94 95 if temporary is not None : 96 # this is a copy of our previous standard input 97 # flush, then rewind 98 temporary.flush() 99 temporary.seek(0, 0) 100 # our temporary file will be used later if the 101 # job is allowed. 102 self.filter.inputfile = temporary 103 else : 104 infile.close() 71 retcode = child.wait() 72 except OSError, msg : 73 self.filter.logger.log_message(_("Problem while waiting for software accounter pid %s to exit") % child.pid) 74 else : 75 if os.WIFEXITED(retcode) : 76 status = os.WEXITSTATUS(retcode) 77 else : 78 status = retcode 79 self.filter.logger.log_message(_("Software accounter %s exit code is %s") % (self.arguments, repr(retcode))) 80 self.filter.logdebug("Software accounter %s said job is %s pages long." % (self.arguments, pagecount)) 105 81 return pagecount 106 82 -
pykota/trunk/pykota/config.py
r1483 r1495 22 22 # 23 23 # $Log$ 24 # Revision 1.48 2004/05/24 22:45:49 jalet 25 # New 'enforcement' directive added 26 # Polling loop improvements 27 # 24 28 # Revision 1.47 2004/05/18 14:49:20 jalet 25 29 # Big code changes to completely remove the need for "requester" directives, … … 343 347 return # No command to launch in the post-hook 344 348 349 def getPrinterEnforcement(self, printername) : 350 """Returns if quota enforcement should be strict or laxist for the current printer.""" 351 validenforcements = [ "STRICT", "LAXIST" ] 352 try : 353 enforcement = self.getPrinterOption(printername, "enforcement") 354 except PyKotaConfigError : 355 return "LAXIST" 356 else : 357 enforcement = enforcement.upper() 358 if enforcement not in validenforcements : 359 raise PyKotaConfigError, _("Option enforcement in section %s only supports values in %s") % (printername, str(validenforcements)) 360 return enforcement 361 345 362 def getPrinterPolicy(self, printername) : 346 363 """Returns the default policy for the current printer.""" -
pykota/trunk/pykota/tool.py
r1492 r1495 22 22 # 23 23 # $Log$ 24 # Revision 1.90 2004/05/24 22:45:49 jalet 25 # New 'enforcement' directive added 26 # Polling loop improvements 27 # 24 28 # Revision 1.89 2004/05/21 22:02:52 jalet 25 29 # Preliminary work on pre-accounting … … 379 383 self.smtpserver = self.config.getSMTPServer() 380 384 self.maildomain = self.config.getMailDomain() 385 self.softwareJobPrice = 0.0 381 386 382 387 def logdebug(self, message) : … … 513 518 user = userpquota.User 514 519 printer = userpquota.Printer 520 enforcement = self.config.getPrinterEnforcement(printer.Name) 515 521 self.logdebug("Checking user %s's quota on printer %s" % (user.Name, printer.Name)) 516 522 (policy, dummy) = self.config.getPrinterPolicy(userpquota.Printer.Name) … … 524 530 else : 525 531 pagecounter = int(userpquota.PageCounter or 0) 532 if enforcement == "STRICT" : 533 pagecounter += self.softwareJobSize 526 534 if userpquota.SoftLimit is not None : 527 535 softlimit = int(userpquota.SoftLimit) … … 564 572 group = grouppquota.Group 565 573 printer = grouppquota.Printer 574 enforcement = self.config.getPrinterEnforcement(printer.Name) 566 575 self.logdebug("Checking group %s's quota on printer %s" % (group.Name, printer.Name)) 567 576 if group.LimitBy and (group.LimitBy.lower() == "balance") : 568 if group.AccountBalance <= 0.0 : 577 val = group.AccountBalance 578 if enforcement == "STRICT" : 579 val -= self.softwareJobPrice # use precomputed size. 580 if val <= 0.0 : 569 581 action = "DENY" 570 elif group.AccountBalance<= self.config.getPoorMan() :582 elif val <= self.config.getPoorMan() : 571 583 action = "WARN" 572 584 else : 573 585 action = "ALLOW" 574 586 else : 587 val = grouppquota.PageCounter 588 if enforcement == "STRICT" : 589 val += self.softwareJobSize 575 590 if grouppquota.SoftLimit is not None : 576 591 softlimit = int(grouppquota.SoftLimit) 577 if grouppquota.PageCounter< softlimit :592 if val < softlimit : 578 593 action = "ALLOW" 579 594 else : … … 583 598 else : 584 599 hardlimit = int(grouppquota.HardLimit) 585 if softlimit <= grouppquota.PageCounter< hardlimit :600 if softlimit <= val < hardlimit : 586 601 now = DateTime.now() 587 602 if grouppquota.DateLimit is not None : … … 600 615 # no soft limit, only a hard one. 601 616 hardlimit = int(grouppquota.HardLimit) 602 if grouppquota.PageCounter< hardlimit :617 if val < hardlimit : 603 618 action = "ALLOW" 604 619 else : … … 643 658 else : 644 659 val = float(user.AccountBalance or 0.0) 660 if self.config.getPrinterEnforcement(printer.Name) == "STRICT" : 661 val -= self.softwareJobPrice # use precomputed size. 645 662 if val <= 0.0 : 646 663 return "DENY" … … 820 837 def precomputeJobSize(self) : 821 838 """Computes the job size with a software method.""" 839 self.logdebug("Precomputing job's size with generic PDL analyzer...") 822 840 try : 823 841 parser = pdlanalyzer.PDLAnalyzer(self.jobdatastream) 824 returnparser.getJobSize()842 jobsize = parser.getJobSize() 825 843 except pdlanalyzer.PDLAnalyzerError, msg : 826 844 # Here we just log the failure, but … … 830 848 self.logger.log_message(_("Unable to precompute the job's size with the generic PDL analyzer."), "warn") 831 849 return 0 850 else : 851 if ((self.printingsystem == "CUPS") \ 852 and (self.preserveinputfile is not None)) \ 853 or (self.printingsystem != "CUPS") : 854 return jobsize * self.copies 855 else : 856 return jobsize 832 857 833 858 def sigterm_handler(self, signum, frame) : -
pykota/trunk/pykota/version.py
r1494 r1495 22 22 # 23 23 24 __version__ = "1.19alpha1 3_unofficial"24 __version__ = "1.19alpha14_unofficial" 25 25 26 26 __doc__ = """PyKota : a complete Printing Quota Solution for CUPS and LPRng."""