Changeset 3413 for pykota/trunk/bin/cupspykota
- Timestamp:
- 09/27/08 22:02:37 (16 years ago)
- Files:
-
- 1 modified
Legend:
- Unmodified
- Added
- Removed
-
pykota/trunk/bin/cupspykota
r3411 r3413 9 9 # the Free Software Foundation, either version 3 of the License, or 10 10 # (at your option) any later version. 11 # 11 # 12 12 # This program is distributed in the hope that it will be useful, 13 13 # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 15 # GNU General Public License for more details. 16 # 16 # 17 17 # You should have received a copy of the GNU General Public License 18 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. … … 51 51 from pykota.accounter import openAccounter 52 52 from pykota import cups 53 54 class FakeObject : 53 54 class FakeObject : 55 55 """Fake object.""" 56 56 def __init__(self, name) : 57 57 """Fake init.""" 58 58 self.Name = name 59 60 class FakePrinter(FakeObject) : 59 60 class FakePrinter(FakeObject) : 61 61 """Fake printer instance.""" 62 62 pass 63 64 class FakeUser(FakeObject) : 63 64 class FakeUser(FakeObject) : 65 65 """Fake user instance.""" 66 66 def __init__(self, name) : … … 68 68 self.Email = name 69 69 FakeObject.__init__(self, name) 70 70 71 71 class CUPSBackend(PyKotaTool) : 72 72 """Base class for tools with no database access.""" … … 83 83 self.lockfilename = None 84 84 self.lockfile = None 85 86 def deferredInit(self) : 85 86 def deferredInit(self) : 87 87 """Deferred initialization.""" 88 88 PyKotaTool.deferredInit(self) … … 91 91 username = self.effectiveUserName 92 92 raise config.PyKotaConfigError, _("User %(username)s is not allowed to read ~pykota/pykotadmin.conf, you must check the permissions.") % locals() 93 94 def enableSigInt(self) : 93 94 def enableSigInt(self) : 95 95 """Enables the SIGINT signal (which raises KeyboardInterrupt).""" 96 96 signal.signal(signal.SIGINT, self.oldSigIntHandler) 97 98 def waitForLock(self) : 97 98 def waitForLock(self) : 99 99 """Waits until we can acquire the lock file.""" 100 100 self.logdebug("Waiting for lock %s to become available..." % self.lockfilename) … … 104 104 # open the lock file, optionally creating it if needed. 105 105 self.lockfile = open(self.lockfilename, "a+") 106 106 107 107 # we wait indefinitely for the lock to become available. 108 108 # works over NFS too. 109 109 fcntl.lockf(self.lockfile, fcntl.LOCK_EX) 110 110 haslock = True 111 111 112 112 self.logdebug("Lock %s acquired." % self.lockfilename) 113 113 114 114 # Here we save the PID in the lock file, but we don't use 115 115 # it, because the lock file may be in a directory shared … … 120 120 self.lockfile.write(str(self.pid)) 121 121 self.lockfile.flush() 122 except IOError, msg : 122 except IOError, msg : 123 123 self.logdebug("I/O Error while waiting for lock %s : %s" % (self.lockfilename, msg)) 124 124 time.sleep(0.25) 125 126 def discoverOtherBackends(self) : 125 126 def discoverOtherBackends(self) : 127 127 """Discovers the other CUPS backends. 128 128 129 129 Executes each existing backend in turn in device enumeration mode. 130 130 Returns the list of available backends. 131 131 132 132 Unfortunately this method can't output any debug information 133 133 to stdout or stderr, else CUPS considers that the device is … … 151 151 # process doesn't exist anymore 152 152 os.remove(lockfilename) 153 153 154 154 if not os.path.exists(lockfilename) : 155 155 lockfile = open(lockfilename, "w") … … 159 159 for b in os.listdir(directory) \ 160 160 if os.access(os.path.join(directory, b), os.X_OK) \ 161 and (b != myname)] 162 for backend in allbackends : 161 and (b != myname)] 162 for backend in allbackends : 163 163 answer = os.popen(backend, "r") 164 164 try : 165 165 devices = [line.strip() for line in answer.readlines()] 166 except : 166 except : 167 167 devices = [] 168 168 status = answer.close() 169 169 if status is None : 170 170 for d in devices : 171 # each line is of the form : 171 # each line is of the form : 172 172 # 'xxxx xxxx "xxxx xxx" "xxxx xxx"' 173 173 # so we have to decompose it carefully … … 186 186 try : 187 187 (devicetype, device, name, fullname) = arguments 188 except ValueError : 188 except ValueError : 189 189 pass # ignore this 'bizarre' device 190 else : 190 else : 191 191 if name.startswith('"') and name.endswith('"') : 192 192 name = name[1:-1] … … 202 202 % (self.myname, self.MyName, self.MyName)) 203 203 return available 204 205 def checkCUPSVersion(self) : 204 205 def checkCUPSVersion(self) : 206 206 """Checks if CUPS is not v1.3.4 or higher.""" 207 207 fullversion = os.environ.get("SOFTWARE", "") … … 210 210 try : 211 211 (major, minor, release) = [int(p) for p in vnum.split(".")] 212 except ValueError : 212 except ValueError : 213 213 pass 214 else : 214 else : 215 215 return (major > 1) \ 216 216 or ((major == 1) and (minor > 3)) \ 217 217 or ((major == 1) and (minor == 3) and (release >= 4)) 218 218 return False 219 220 def initBackendParameters(self) : 219 220 def initBackendParameters(self) : 221 221 """Initializes the backend's attributes.""" 222 # check that the DEVICE_URI environment variable's value is 222 # check that the DEVICE_URI environment variable's value is 223 223 # prefixed with self.myname otherwise don't touch it. 224 # If this is the case, we have to remove the prefix from 225 # the environment before launching the real backend 224 # If this is the case, we have to remove the prefix from 225 # the environment before launching the real backend 226 226 self.logdebug("Initializing backend...") 227 227 228 228 if not self.checkCUPSVersion() : 229 229 self.printInfo("BEWARE : CUPS is too old. You should use CUPS v1.3.4 or higher.", "error") 230 230 231 231 self.PrinterName = os.environ.get("PRINTER", "") 232 232 directories = [ self.config.getPrinterDirectory(self.PrinterName), … … 240 240 else : 241 241 self.printInfo("Insufficient permissions to access to temporary directory %s" % direc, "warn") 242 242 243 243 self.Action = "ALLOW" # job allowed by default 244 244 self.Reason = None … … 247 247 if copies < 1 : 248 248 raise ValueError 249 except (ValueError, TypeError) : 249 except (ValueError, TypeError) : 250 250 self.logdebug("Invalid number of copies '%s', using 1 instead." % sys.argv[4]) 251 251 copies = 1 252 252 if len(sys.argv) == 7 : 253 253 fname = sys.argv[6] # read job's datas from file 254 else : 255 fname = None # read job's datas from stdin 256 254 else : 255 fname = None # read job's datas from stdin 256 257 257 self.Ticket = cups.JobTicket(sys.argv[1].strip(), self.PrinterName, \ 258 258 copies, fname, sys.argv[5].strip()) 259 self.UserName = self.Ticket.OriginatingUserName 260 259 self.UserName = self.Ticket.OriginatingUserName 260 261 261 self.DataFile = (os.path.join(self.Directory, "%s-%s-%s-%s" % \ 262 262 (self.myname, self.PrinterName, self.UserName, self.Ticket.JobId))).encode(sys.getfilesystemencoding(), "replace") 263 263 264 264 muststartwith = "%s:" % self.myname 265 265 device_uri = os.environ.get("DEVICE_URI", "") … … 268 268 device_uri = fulldevice_uri[len(muststartwith):] 269 269 for i in range(2) : 270 if device_uri.startswith("/") : 270 if device_uri.startswith("/") : 271 271 device_uri = device_uri[1:] 272 272 try : 273 (backend, destination) = device_uri.split(":", 1) 274 except ValueError : 273 (backend, destination) = device_uri.split(":", 1) 274 except ValueError : 275 275 if not device_uri : 276 276 self.logdebug("Not attached to an existing print queue.") 277 277 backend = "" 278 278 printerhostname = "" 279 else : 279 else : 280 280 raise PyKotaToolError, "Invalid DEVICE_URI : %s\n" % device_uri 281 else : 281 else : 282 282 if backend == "hp" : 283 283 try : 284 284 printerhostname = destination.split("=")[1] # hp:/net/HP_LaserJet_8000_Series?ip=192.168.100.100 285 except IndexError : 285 except IndexError : 286 286 self.logdebug("Unsupported hplip URI %s" % device_uri) 287 printerhostname = "" 288 else : 287 printerhostname = "" 288 else : 289 289 while destination.startswith("/") : 290 290 destination = destination[1:] 291 checkauth = destination.split("@", 1) 291 checkauth = destination.split("@", 1) 292 292 if len(checkauth) == 2 : 293 293 destination = checkauth[1] 294 294 printerhostname = destination.split("/")[0].split(":")[0] 295 296 self.PrinterHostName = printerhostname 295 296 self.PrinterHostName = printerhostname 297 297 self.RealBackend = backend 298 298 self.DeviceURI = device_uri 299 299 300 300 if self.Ticket.BillingCode is None : 301 301 self.OriginalJobBillingCode = None 302 else : 302 else : 303 303 self.OriginalJobBillingCode = self.Ticket.BillingCode[:] 304 304 305 305 baselockfilename = self.DeviceURI.replace("/", ".") 306 306 baselockfilename = baselockfilename.replace(":", ".") … … 309 309 baselockfilename = baselockfilename.replace("@", ".") 310 310 self.lockfilename = os.path.join(self.Directory, "%s-%s..LCK" % (self.myname, baselockfilename)) 311 311 312 312 self.logdebug("Backend : %s" % self.RealBackend) 313 313 self.logdebug("DeviceURI : %s" % self.DeviceURI) … … 319 319 self.logdebug("Copies : %s" % self.Ticket.Copies) 320 320 self.logdebug("Options : %s" % self.Ticket.Options) 321 self.logdebug("Directory : %s" % self.Directory) 321 self.logdebug("Directory : %s" % self.Directory) 322 322 self.logdebug("DataFile : %s" % self.DataFile) 323 323 self.logdebug("JobBillingCode : %s" % self.Ticket.BillingCode) 324 324 self.logdebug("JobOriginatingHostName : %s" % self.Ticket.OriginatingHostName) 325 325 326 326 # fakes some entries to allow for external mailto 327 327 # before real entries are extracted from the database. 328 328 self.User = FakeUser(self.UserName) 329 329 self.Printer = FakePrinter(self.PrinterName) 330 330 331 331 self.enableSigInt() 332 332 self.logdebug("Backend initialized.") 333 333 334 334 def overwriteJobAttributes(self) : 335 335 """Overwrites some of the job's attributes if needed.""" … … 337 337 # First overwrite the job ticket 338 338 self.overwriteJobTicket() 339 339 340 340 # do we want to strip out the Samba/Winbind domain name ? 341 341 separator = self.config.getWinbindSeparator() 342 342 if separator is not None : 343 343 self.UserName = self.UserName.split(separator)[-1] 344 345 # this option is deprecated, and we want to tell people 344 345 # this option is deprecated, and we want to tell people 346 346 # this is the case. 347 347 tolower = self.config.getUserNameToLower() … … 352 352 if self.config.isTrue(tolower) : 353 353 self.UserName = self.UserName.lower() 354 355 # Now use the newer and more complete 'usernamecase' directive. 356 casechange = self.config.getUserNameCase() 354 355 # Now use the newer and more complete 'usernamecase' directive. 356 casechange = self.config.getUserNameCase() 357 357 if casechange != "native" : 358 358 self.UserName = getattr(self.UserName, casechange)() 359 360 # do we want to strip some prefix off of titles ? 359 360 # do we want to strip some prefix off of titles ? 361 361 stripprefix = self.config.getStripTitle(self.PrinterName) 362 362 if stripprefix : … … 365 365 % (stripprefix, self.Ticket.Title)) 366 366 self.Ticket.Title = self.Ticket.Title[len(stripprefix):] 367 367 368 368 self.logdebug("Username : %s" % self.UserName) 369 369 self.logdebug("BillingCode : %s" % self.Ticket.BillingCode) 370 370 self.logdebug("Title : %s" % self.Ticket.Title) 371 371 self.logdebug("Job's attributes sanitizing done.") 372 372 373 373 def didUserConfirm(self) : 374 374 """Asks for user confirmation through an external script. 375 375 376 376 returns False if the end user wants to cancel the job, else True. 377 377 """ 378 378 self.logdebug("Checking if we have to ask for user's confirmation...") 379 answer = None 379 answer = None 380 380 confirmationcommand = self.config.getAskConfirmation(self.PrinterName) 381 381 if confirmationcommand : … … 388 388 if answer == "CANCEL" : 389 389 break 390 except IOError, msg : 390 except IOError, msg : 391 391 self.logdebug("IOError while reading subprocess' output : %s" % msg) 392 inputfile.close() 392 inputfile.close() 393 393 self.logdebug("User's confirmation received : %s" % (((answer == "CANCEL") and "CANCEL") or "CONTINUE")) 394 else : 394 else : 395 395 self.logdebug("No need to ask for user's confirmation, job processing will continue.") 396 return (answer != "CANCEL") 397 398 def overwriteJobTicket(self) : 396 return (answer != "CANCEL") 397 398 def overwriteJobTicket(self) : 399 399 """Should we overwrite the job's ticket (username and billingcode) ?""" 400 400 self.logdebug("Checking if we need to overwrite the job ticket...") … … 414 414 self.logdebug("Seen CANCEL command.") 415 415 action = "CANCEL" 416 elif line.startswith("USERNAME=") : 416 elif line.startswith("USERNAME=") : 417 417 username = line.split("=", 1)[1].strip().decode(self.charset, "replace") 418 418 self.logdebug("Seen new username [%s]" % username) 419 elif line.startswith("BILLINGCODE=") : 419 elif line.startswith("BILLINGCODE=") : 420 420 billingcode = line.split("=", 1)[1].strip().decode(self.charset, "replace") 421 421 self.logdebug("Seen new billing code [%s]" % billingcode) … … 423 423 reason = line.split("=", 1)[1].strip().decode(self.charset, "replace") 424 424 self.logdebug("Seen new reason [%s]" % reason) 425 except IOError, msg : 425 except IOError, msg : 426 426 self.logdebug("IOError while reading subprocess' output : %s" % msg) 427 inputfile.close() 428 427 inputfile.close() 428 429 429 # now overwrite the job's ticket if new data was supplied 430 430 if action == "DENY" : … … 441 441 self.UserName = username 442 442 if billingcode is not None : 443 self.Ticket.BillingCode = billingcode 443 self.Ticket.BillingCode = billingcode 444 444 self.logdebug("Job ticket overwriting done.") 445 445 446 446 def saveDatasAndCheckSum(self) : 447 447 """Saves the input datas into a static file.""" 448 448 self.logdebug("Duplicating data stream into %s" % self.DataFile) 449 449 mustclose = 0 450 outfile = open(self.DataFile, "wb") 450 outfile = open(self.DataFile, "wb") 451 451 if self.Ticket.FileName is not None : 452 452 infile = open(self.Ticket.FileName, "rb") 453 453 self.logdebug("Reading input datas from %s" % self.Ticket.FileName) 454 454 mustclose = 1 455 else : 455 else : 456 456 infile = sys.stdin 457 457 self.logdebug("Reading input datas from stdin") … … 461 461 checksum = md5.new() 462 462 while 1 : 463 data = infile.read(CHUNK) 463 data = infile.read(CHUNK) 464 464 if not data : 465 465 break 466 sizeread += len(data) 466 sizeread += len(data) 467 467 outfile.write(data) 468 checksum.update(data) 468 checksum.update(data) 469 469 if not (dummy % 32) : # Only display every 2 Mb 470 470 self.logdebug("%s bytes saved..." % sizeread) 471 dummy += 1 472 if mustclose : 471 dummy += 1 472 if mustclose : 473 473 infile.close() 474 474 475 475 outfile.close() 476 self.JobSizeBytes = sizeread 476 self.JobSizeBytes = sizeread 477 477 self.JobMD5Sum = checksum.hexdigest() 478 478 479 479 self.logdebug("JobSizeBytes : %s" % self.JobSizeBytes) 480 480 self.logdebug("JobMD5Sum : %s" % self.JobMD5Sum) 481 481 self.logdebug("Data stream duplicated into %s" % self.DataFile) 482 482 483 483 def clean(self) : 484 484 """Cleans up the place.""" … … 487 487 try : 488 488 keep = self.config.getPrinterKeepFiles(self.PrinterName) 489 except AttributeError : 489 except AttributeError : 490 490 keep = False 491 491 if not keep : … … 497 497 else : 498 498 self.logdebug("Work file %s has been deleted." % self.DataFile) 499 else : 499 else : 500 500 self.logdebug("Work file %s will be kept." % self.DataFile) 501 PyKotaTool.clean(self) 501 PyKotaTool.clean(self) 502 502 if self.lockfile is not None : 503 503 self.logdebug("Unlocking %s..." % self.lockfilename) … … 505 505 fcntl.lockf(self.lockfile, fcntl.LOCK_UN) 506 506 self.lockfile.close() 507 except : 507 except : 508 508 self.printInfo("Problem while unlocking %s" % self.lockfilename, "error") 509 else : 509 else : 510 510 self.logdebug("%s unlocked." % self.lockfilename) 511 511 self.logdebug("Clean.") 512 513 def precomputeJobSize(self) : 512 513 def precomputeJobSize(self) : 514 514 """Computes the job size with a software method.""" 515 515 self.logdebug("Precomputing job's size...") … … 518 518 self.softwareJobSize = self.preaccounter.getJobSize(None) 519 519 self.logdebug("Precomputed job's size is %s pages." % self.softwareJobSize) 520 521 def precomputeJobPrice(self) : 520 521 def precomputeJobPrice(self) : 522 522 """Precomputes the job price with a software method.""" 523 523 self.logdebug("Precomputing job's price...") … … 525 525 self.logdebug("Precomputed job's price is %.3f credits." \ 526 526 % self.softwareJobPrice) 527 528 def exportJobInfo(self) : 527 528 def exportJobInfo(self) : 529 529 """Exports the actual job's attributes to the environment.""" 530 530 self.logdebug("Exporting job information to the environment...") … … 550 550 setenv("PYKOTAPRECOMPUTEDJOBSIZE", str(self.softwareJobSize), self.charset) 551 551 self.logdebug("Environment updated.") 552 552 553 553 def exportUserInfo(self) : 554 554 """Exports user information to the environment.""" … … 559 559 setenv("PYKOTALIFETIMEPAID", str(self.User.LifeTimePaid or 0.0), self.charset) 560 560 setenv("PYKOTAUSERDESCRIPTION", self.User.Description or "", self.charset) 561 561 562 562 setenv("PYKOTAPAGECOUNTER", str(self.UserPQuota.PageCounter or 0), self.charset) 563 563 setenv("PYKOTALIFEPAGECOUNTER", str(self.UserPQuota.LifePageCounter or 0), self.charset) … … 566 566 setenv("PYKOTADATELIMIT", str(self.UserPQuota.DateLimit), self.charset) 567 567 setenv("PYKOTAWARNCOUNT", str(self.UserPQuota.WarnCount), self.charset) 568 568 569 569 # TODO : move this elsewhere once software accounting is done only once. 570 570 setenv("PYKOTAPRECOMPUTEDJOBPRICE", str(self.softwareJobPrice), self.charset) 571 571 572 572 self.logdebug("Environment updated.") 573 573 574 574 def exportPrinterInfo(self) : 575 575 """Exports printer information to the environment.""" … … 584 584 setenv("PYKOTAPRICEPERJOB", str(self.Printer.PricePerJob or 0), self.charset) 585 585 self.logdebug("Environment updated.") 586 586 587 587 def exportPhaseInfo(self, phase) : 588 588 """Exports phase information to the environment.""" … … 590 590 setenv("PYKOTAPHASE", phase, self.charset) 591 591 self.logdebug("Environment updated.") 592 592 593 593 def exportJobSizeAndPrice(self) : 594 594 """Exports job's size and price information to the environment.""" … … 597 597 setenv("PYKOTAJOBPRICE", str(self.JobPrice), self.charset) 598 598 self.logdebug("Environment updated.") 599 599 600 600 def exportReason(self) : 601 601 """Exports the job's action status and optional reason.""" … … 605 605 setenv("PYKOTAREASON", self.Reason or "", self.charset) 606 606 self.logdebug("Environment updated.") 607 608 def acceptJob(self) : 607 608 def acceptJob(self) : 609 609 """Returns the appropriate exit code to tell CUPS all is OK.""" 610 610 return 0 611 612 def removeJob(self) : 611 612 def removeJob(self) : 613 613 """Returns the appropriate exit code to let CUPS think all is OK. 614 614 615 615 Returning 0 (success) prevents CUPS from stopping the print queue. 616 """ 616 """ 617 617 return 0 618 618 619 619 def launchPreHook(self) : 620 620 """Allows plugging of an external hook before the job gets printed.""" … … 624 624 retcode = os.system(prehook) 625 625 self.logdebug("pre-hook exited with status %s." % retcode) 626 626 627 627 def launchPostHook(self) : 628 628 """Allows plugging of an external hook after the job gets printed and/or denied.""" … … 632 632 retcode = os.system(posthook) 633 633 self.logdebug("post-hook exited with status %s." % retcode) 634 635 def improveMessage(self, message) : 634 635 def improveMessage(self, message) : 636 636 """Improves a message by adding more informations in it if possible.""" 637 637 try : … … 640 640 self.Ticket.JobId, \ 641 641 message) 642 except : 642 except : 643 643 return message 644 645 def logdebug(self, message) : 644 645 def logdebug(self, message) : 646 646 """Improves the debug message before outputting it.""" 647 647 PyKotaTool.logdebug(self, self.improveMessage(message)) 648 649 def printInfo(self, message, level="info") : 648 649 def printInfo(self, message, level="info") : 650 650 """Improves the informational message before outputting it.""" 651 651 self.logger.log_message(self.improveMessage(message), level) 652 652 653 653 def startingBanner(self, withaccounting) : 654 654 """Retrieves a starting banner for current printer and returns its content.""" … … 656 656 self.printBanner(self.config.getStartingBanner(self.PrinterName), withaccounting) 657 657 self.logdebug("Starting banner retrieved.") 658 658 659 659 def endingBanner(self, withaccounting) : 660 660 """Retrieves an ending banner for current printer and returns its content.""" … … 662 662 self.printBanner(self.config.getEndingBanner(self.PrinterName), withaccounting) 663 663 self.logdebug("Ending banner retrieved.") 664 664 665 665 def printBanner(self, bannerfileorcommand, withaccounting) : 666 666 """Reads a banner or generates one through an external command. 667 667 668 668 Returns the banner's content in a format which MUST be accepted 669 669 by the printer. … … 691 691 try : 692 692 fh = open(bannerfileorcommand, 'rb') 693 except IOError, msg : 693 except IOError, msg : 694 694 self.printInfo("Impossible to open %s : %s" \ 695 695 % (bannerfileorcommand, msg), "error") 696 else : 696 else : 697 697 self.runOriginalBackend(fh, isBanner=1) 698 698 fh.close() … … 701 701 self.BannerSize += 1 # TODO : fix this by passing the banner's content through software accounting 702 702 self.logdebug("Banner printed...") 703 703 704 704 def handleBanner(self, bannertype, withaccounting) : 705 705 """Handles the banner with or without accounting.""" 706 706 if withaccounting : 707 707 acc = "with" 708 else : 708 else : 709 709 acc = "without" 710 710 self.logdebug("Handling %s banner %s accounting..." % (bannertype, acc)) … … 735 735 if (avoidduplicatebanners == "YES") : 736 736 printbanner = False 737 else : 738 # avoidduplicatebanners is an integer, since NO, 737 else : 738 # avoidduplicatebanners is an integer, since NO, 739 739 # YES and 0 are already handled 740 740 now = DateTime.now() … … 746 746 self.logdebug("Difference with previous job : %.2f seconds. Try to avoid banners for : %.2f seconds." % (difference, avoidduplicatebanners)) 747 747 if difference < avoidduplicatebanners : 748 self.logdebug("Duplicate banner avoided because previous banner is less than %.2f seconds old." % avoidduplicatebanners) 748 self.logdebug("Duplicate banner avoided because previous banner is less than %.2f seconds old." % avoidduplicatebanners) 749 749 printbanner = False 750 750 else : … … 753 753 getattr(self, "%sBanner" % bannertype)(withaccounting) 754 754 self.logdebug("%s banner done." % bannertype.title()) 755 756 def sanitizeJobSize(self) : 755 756 def sanitizeJobSize(self) : 757 757 """Sanitizes the job's size if needed.""" 758 758 # TODO : there's a difficult to see bug here when banner accounting is activated and hardware accounting is used. … … 772 772 if replacement == "PRECOMPUTED" : 773 773 self.JobSize = self.softwareJobSize 774 else : 774 else : 775 775 self.JobSize = replacement 776 776 self.logdebug("Job's size sanitized.") 777 778 def getPrinterUserAndUserPQuota(self) : 777 778 def getPrinterUserAndUserPQuota(self) : 779 779 """Returns a tuple (policy, printer, user, and user print quota) on this printer. 780 780 781 781 "OK" is returned in the policy if both printer, user and user print quota 782 782 exist in the Quota Storage. 783 783 Otherwise, the policy as defined for this printer in pykota.conf is returned. 784 784 785 785 If policy was set to "EXTERNAL" and one of printer, user, or user print quota 786 786 doesn't exist in the Quota Storage, then an external command is launched, as … … 789 789 or users, for example, and finally extracting printer, user and user print 790 790 quota from the Quota Storage is tried a second time. 791 791 792 792 "EXTERNALERROR" is returned in case policy was "EXTERNAL" and an error status 793 793 was returned by the external command. … … 802 802 break 803 803 (policy, args) = self.config.getPrinterPolicy(self.PrinterName) 804 if policy == "EXTERNAL" : 804 if policy == "EXTERNAL" : 805 805 commandline = self.formatCommandLine(args, user, printer) 806 806 if not printer.Exists : … … 814 814 policy = "EXTERNALERROR" 815 815 break 816 else : 816 else : 817 817 if not printer.Exists : 818 818 self.printInfo(_("Printer %s not registered in the PyKota system, applying default policy (%s)") % (self.PrinterName, policy)) … … 822 822 self.printInfo(_("User %s doesn't have quota on printer %s in the PyKota system, applying default policy (%s)") % (self.UserName, self.PrinterName, policy)) 823 823 break 824 825 if policy == "EXTERNAL" : 824 825 if policy == "EXTERNAL" : 826 826 if not printer.Exists : 827 827 self.printInfo(_("Printer %s still not registered in the PyKota system, job will be rejected") % self.PrinterName) … … 830 830 if not userpquota.Exists : 831 831 self.printInfo(_("User %s still doesn't have quota on printer %s in the PyKota system, job will be rejected") % (self.UserName, self.PrinterName)) 832 self.Policy = policy 832 self.Policy = policy 833 833 self.Printer = printer 834 834 self.User = user 835 835 self.UserPQuota = userpquota 836 836 self.logdebug("Retrieval of printer, user and user print quota entry done.") 837 838 def getBillingCode(self) : 837 838 def getBillingCode(self) : 839 839 """Extracts the billing code from the database. 840 840 841 841 An optional script is launched to notify the user when 842 842 the billing code is unknown and PyKota was configured to … … 858 858 if self.BillingCode.Exists : 859 859 self.logdebug(msg + "has been created.") 860 else : 860 else : 861 861 self.printInfo(msg + "couldn't be created.", "error") 862 else : 862 else : 863 863 self.logdebug(msg + "job will be denied.") 864 864 self.Action = newaction 865 if script is not None : 865 if script is not None : 866 866 self.logdebug(msg + "launching subprocess [%s] to notify user." % script) 867 867 os.system(script) 868 868 self.logdebug("Retrieval of billing code information done.") 869 870 def checkIfDupe(self) : 869 870 def checkIfDupe(self) : 871 871 """Checks if the job is a duplicate, and handles the situation.""" 872 872 self.logdebug("Checking if the job is a duplicate...") … … 889 889 self.logdebug("Duplicate job allowed because previous one is more than %.2f seconds old." % duplicatesdelay) 890 890 else : 891 # TODO : use the current user's last job instead of 891 # TODO : use the current user's last job instead of 892 892 # TODO : the current printer's last job. This would be 893 893 # TODO : better but requires an additional database query 894 # TODO : with SQL, and is much more complex with the 894 # TODO : with SQL, and is much more complex with the 895 895 # TODO : actual LDAP schema. Maybe this is not very 896 896 # TODO : important, because usually duplicate jobs are sucessive. … … 900 900 self.Action = "DENY" 901 901 self.Reason = _("Duplicate print jobs are not allowed on printer %s.") % self.PrinterName 902 else : 902 else : 903 903 self.logdebug("Launching subprocess [%s] to see if duplicate jobs should be allowed or not." % denyduplicates) 904 904 fanswer = os.popen(denyduplicates, "r") 905 905 self.Action = fanswer.read().strip().upper() 906 906 fanswer.close() 907 if self.Action == "DENY" : 907 if self.Action == "DENY" : 908 908 self.printInfo("%s : %s." % (msg, _("Subprocess denied printing of a dupe")), "warn") 909 909 self.Reason = _("Duplicate print jobs are not allowed on printer %s at this time.") % self.PrinterName 910 else : 910 else : 911 911 self.printInfo("%s : %s." % (msg, _("Subprocess allowed printing of a dupe")), "warn") 912 else : 912 else : 913 913 self.logdebug("Job doesn't seem to be a duplicate.") 914 914 self.logdebug("Checking if the job is a duplicate done.") 915 915 916 916 def tellUser(self) : 917 917 """Sends a message to an user.""" 918 self.logdebug("Sending some feedback to user %s..." % self.UserName) 918 self.logdebug("Sending some feedback to user %s..." % self.UserName) 919 919 if not self.Reason : 920 920 self.logdebug("No feedback to send to user %s." % self.UserName) 921 else : 921 else : 922 922 (mailto, arguments) = self.config.getMailTo(self.PrinterName) 923 923 if mailto == "EXTERNAL" : 924 924 # TODO : clean this again 925 925 self.externalMailTo(arguments, self.Action, self.User, self.Printer, self.Reason) 926 else : 926 else : 927 927 # TODO : clean this again 928 928 admin = self.config.getAdmin(self.PrinterName) … … 934 934 if mailto in ("BOTH", "ADMIN") : 935 935 destination.append(adminmail) 936 if mailto in ("BOTH", "USER") : 936 if mailto in ("BOTH", "USER") : 937 937 destination.append(usermail) 938 938 939 939 fullmessage = self.Reason + (_("\n\nYour system administrator :\n\n\t%s - <%s>\n") % (admin, adminmail)) 940 try : 940 try : 941 941 server = smtplib.SMTP(self.smtpserver) 942 except socket.error, msg : 942 except socket.error, msg : 943 943 self.printInfo(_("Impossible to connect to SMTP server : %s") % msg, "error") 944 944 else : … … 951 951 if mailto == "BOTH" : 952 952 msg["Cc"] = adminmail 953 else : 953 else : 954 954 msg["To"] = adminmail 955 955 msg["Date"] = email.Utils.formatdate(localtime=True) 956 956 server.sendmail(adminmail, destination, msg.as_string()) 957 except smtplib.SMTPException, answer : 957 except smtplib.SMTPException, answer : 958 958 try : 959 959 for (k, v) in answer.recipients.items() : … … 963 963 server.quit() 964 964 self.logdebug("Feedback sent to user %s." % self.UserName) 965 966 def mainWork(self) : 965 966 def mainWork(self) : 967 967 """Main work is done here.""" 968 968 if not self.JobSizeBytes : … … 974 974 self.tellUser() 975 975 return self.removeJob() 976 976 977 977 self.getPrinterUserAndUserPQuota() 978 978 if self.Policy == "EXTERNALERROR" : … … 989 989 self.tellUser() 990 990 return self.removeJob() 991 elif self.Policy == "DENY" : 991 elif self.Policy == "DENY" : 992 992 # Either printer, user or user print quota doesn't exist, 993 993 # and the job should be rejected. … … 1007 1007 # be allowed if current user is allowed to print on this printer 1008 1008 return self.doWork() 1009 else : 1009 else : 1010 1010 self.Reason = _("Invalid policy %s for printer %s") % (self.Policy, self.PrinterName) 1011 1011 self.printInfo(self.Reason, "error") 1012 1012 self.tellUser() 1013 1013 return self.removeJob() 1014 1015 def doWork(self) : 1014 1015 def doWork(self) : 1016 1016 """The accounting work is done here.""" 1017 1017 self.precomputeJobPrice() … … 1019 1019 self.exportPrinterInfo() 1020 1020 self.exportPhaseInfo("BEFORE") 1021 1022 if self.Action not in ("DENY", "CANCEL") : 1021 1022 if self.Action not in ("DENY", "CANCEL") : 1023 1023 if self.Printer.MaxJobSize and (self.softwareJobSize > self.Printer.MaxJobSize) : 1024 1024 # This printer was set to refuse jobs this large. … … 1028 1028 # because in case of error the user could complain :-) 1029 1029 self.Reason = _("You are not allowed to print so many pages on printer %s at this time.") % self.PrinterName 1030 1030 1031 1031 if self.Action not in ("DENY", "CANCEL") : 1032 1032 if self.User.LimitBy == "noprint" : … … 1034 1034 self.Action = "DENY" 1035 1035 self.Reason = _("Your account settings forbid you to print at this time.") 1036 1036 1037 1037 if self.Action not in ("DENY", "CANCEL") : 1038 1038 # If printing is still allowed at this time, we … … 1041 1041 # save some database queries. 1042 1042 self.getBillingCode() 1043 1043 1044 1044 if self.Action not in ("DENY", "CANCEL") : 1045 1045 # If printing is still allowed at this time, we … … 1048 1048 # save some database queries. 1049 1049 self.checkIfDupe() 1050 1050 1051 1051 if self.Action not in ("DENY", "CANCEL") : 1052 1052 # If printing is still allowed at this time, we … … 1056 1056 if self.User.LimitBy in ('noquota', 'nochange') : 1057 1057 self.logdebug("User %s is allowed to print with no limit, no need to check quota." % self.UserName) 1058 elif self.Printer.PassThrough : 1058 elif self.Printer.PassThrough : 1059 1059 self.logdebug("Printer %s is in PassThrough mode, no need to check quota." % self.PrinterName) 1060 1060 else : … … 1067 1067 self.printInfo(_("Print Quota exceeded for user %s on printer %s") % (self.UserName, self.PrinterName)) 1068 1068 self.Reason = self.config.getHardWarn(self.PrinterName) 1069 elif self.Action == "WARN" : 1069 elif self.Action == "WARN" : 1070 1070 self.printInfo(_("Print Quota low for user %s on printer %s") % (self.UserName, self.PrinterName)) 1071 if self.User.LimitBy and (self.User.LimitBy.lower() == "balance") : 1071 if self.User.LimitBy and (self.User.LimitBy.lower() == "balance") : 1072 1072 self.Reason = self.config.getPoorWarn() 1073 else : 1073 else : 1074 1074 self.Reason = self.config.getSoftWarn(self.PrinterName) 1075 1076 # If job still allowed to print, should we ask for confirmation ? 1077 if self.Action not in ("DENY", "CANCEL") : 1075 1076 # If job still allowed to print, should we ask for confirmation ? 1077 if self.Action not in ("DENY", "CANCEL") : 1078 1078 if not self.didUserConfirm() : 1079 1079 self.Action = "CANCEL" 1080 1080 self.Reason = _("Print job cancelled.") 1081 1081 setenv("PYKOTASTATUS", "CANCELLED", self.charset) 1082 1082 1083 1083 # exports some new environment variables 1084 1084 self.exportReason() 1085 1085 1086 1086 # now tell the user if he needs to know something 1087 1087 self.tellUser() 1088 1088 1089 1089 # launches the pre hook 1090 1090 self.launchPreHook() 1091 1091 1092 1092 # handle starting banner pages without accounting 1093 1093 self.BannerSize = 0 … … 1095 1095 if (self.Action != "CANCEL") and accountbanner in ["ENDING", "NONE"] : 1096 1096 self.handleBanner("starting", 0) 1097 1097 1098 1098 if self.Action == "DENY" : 1099 1099 self.printInfo(_("Job denied, no accounting will be done.")) 1100 elif self.Action == "CANCEL" : 1100 elif self.Action == "CANCEL" : 1101 1101 self.printInfo(_("Job cancelled, no accounting will be done.")) 1102 1102 else : 1103 1103 self.printInfo(_("Job accounting begins.")) 1104 1104 self.accounter.beginJob(self.Printer) 1105 1105 1106 1106 # handle starting banner pages with accounting 1107 1107 if (self.Action != "CANCEL") and accountbanner in ["STARTING", "BOTH"] : 1108 1108 self.handleBanner("starting", 1) 1109 1110 # pass the job's data to the real backend if needed 1109 1110 # pass the job's data to the real backend if needed 1111 1111 if self.Action in ("ALLOW", "WARN") : 1112 1112 retcode = self.printJobDatas() 1113 else : 1113 else : 1114 1114 retcode = self.removeJob() 1115 1115 1116 1116 # indicate phase change 1117 1117 self.exportPhaseInfo("AFTER") 1118 1118 1119 1119 # handle ending banner pages with accounting 1120 1120 if (self.Action != "CANCEL") and accountbanner in ["ENDING", "BOTH"] : 1121 1121 self.handleBanner("ending", 1) 1122 1122 1123 1123 # stops accounting 1124 1124 if self.Action == "DENY" : 1125 1125 self.printInfo(_("Job denied, no accounting has been done.")) 1126 elif self.Action == "CANCEL" : 1126 elif self.Action == "CANCEL" : 1127 1127 self.printInfo(_("Job cancelled, no accounting has been done.")) 1128 1128 else : 1129 1129 self.accounter.endJob(self.Printer) 1130 1130 self.printInfo(_("Job accounting ends.")) 1131 1132 # Do all these database changes within a single transaction 1131 1132 # Do all these database changes within a single transaction 1133 1133 # NB : we don't enclose ALL the changes within a single transaction 1134 1134 # because while waiting for the printer to answer its internal page … … 1147 1147 self.JobSize = 0 1148 1148 self.printInfo(_("Job size forced to 0 because the real CUPS backend failed. No accounting will be done."), "warn") 1149 else : 1149 else : 1150 1150 self.printInfo(_("The real CUPS backend failed, but the job will be accounted for anyway."), "warn") 1151 1152 # retrieve the job size 1151 1152 # retrieve the job size 1153 1153 if self.Action == "DENY" : 1154 1154 self.JobSize = 0 1155 1155 self.printInfo(_("Job size forced to 0 because printing is denied.")) 1156 elif self.Action == "CANCEL" : 1156 elif self.Action == "CANCEL" : 1157 1157 self.JobSize = 0 1158 1158 self.printInfo(_("Job size forced to 0 because printing was cancelled.")) 1159 else : 1159 else : 1160 1160 self.UserPQuota.resetDenyBannerCounter() 1161 if (self.Action != "PROBLEM") or ("CHARGE" in onbackenderror) : 1161 if (self.Action != "PROBLEM") or ("CHARGE" in onbackenderror) : 1162 1162 self.JobSize = self.accounter.getJobSize(self.Printer) 1163 1163 self.sanitizeJobSize() 1164 1164 self.JobSize += self.BannerSize 1165 1165 self.printInfo(_("Job size : %i") % self.JobSize) 1166 1166 1167 1167 if ((self.Action == "PROBLEM") and ("NOCHARGE" in onbackenderror)) or \ 1168 1168 (self.Action in ("DENY", "CANCEL")) : … … 1173 1173 self.JobPrice = 0.0 1174 1174 else : 1175 # update the quota for the current user on this printer 1175 # update the quota for the current user on this printer 1176 1176 self.printInfo(_("Updating user %s's quota on printer %s") % (self.UserName, self.PrinterName)) 1177 1177 self.JobPrice = self.UserPQuota.increasePagesUsage(self.JobSize, self.accounter.inkUsage) 1178 1179 # adds the current job to history 1178 1179 # adds the current job to history 1180 1180 self.Printer.addJobToHistory(self.Ticket.JobId, self.User, self.accounter.getLastPageCounter(), \ 1181 1181 self.Action, self.JobSize, self.JobPrice, self.Ticket.FileName, \ … … 1184 1184 self.softwareJobSize, self.softwareJobPrice) 1185 1185 self.printInfo(_("Job added to history.")) 1186 1186 1187 1187 if hasattr(self, "BillingCode") and self.BillingCode and self.BillingCode.Exists : 1188 1188 if (self.Action in ("ALLOW", "WARN")) or \ … … 1190 1190 self.BillingCode.consume(self.JobSize, self.JobPrice) 1191 1191 self.printInfo(_("Billing code %s was updated.") % self.BillingCode.BillingCode) 1192 except : 1192 except : 1193 1193 self.storage.rollbackTransaction() 1194 1194 raise 1195 else : 1195 else : 1196 1196 self.storage.commitTransaction() 1197 1197 1198 1198 # exports some new environment variables 1199 1199 self.exportJobSizeAndPrice() 1200 1200 1201 1201 # then re-export user information with new values 1202 1202 self.exportUserInfo() 1203 1203 1204 1204 # handle ending banner pages without accounting 1205 1205 if (self.Action != "CANCEL") and accountbanner in ["STARTING", "NONE"] : 1206 1206 self.handleBanner("ending", 0) 1207 1207 1208 1208 self.launchPostHook() 1209 1210 return retcode 1211 1212 def printJobDatas(self) : 1209 1210 return retcode 1211 1212 def printJobDatas(self) : 1213 1213 """Sends the job's datas to the real backend.""" 1214 1214 self.logdebug("Sending job's datas to real backend...") 1215 1215 1216 1216 delay = 0 1217 1217 number = 1 … … 1222 1222 if (number < 0) or (delay < 0) : 1223 1223 raise ValueError 1224 except ValueError : 1224 except ValueError : 1225 1225 self.printInfo(_("Incorrect value for the 'onbackenderror' directive in section [%s]") % self.PrinterName, "error") 1226 1226 delay = 0 1227 1227 number = 1 1228 else : 1228 else : 1229 1229 break 1230 loopcnt = 1 1231 while True : 1230 loopcnt = 1 1231 while True : 1232 1232 if self.Ticket.FileName is None : 1233 1233 infile = open(self.DataFile, "rb") 1234 else : 1234 else : 1235 1235 infile = None 1236 1236 retcode = self.runOriginalBackend(infile) … … 1244 1244 time.sleep(delay) 1245 1245 loopcnt += 1 1246 else : 1246 else : 1247 1247 break 1248 1248 1249 1249 self.logdebug("Job's datas sent to real backend.") 1250 1250 return retcode 1251 1251 1252 1252 def runOriginalBackend(self, filehandle=None, isBanner=0) : 1253 1253 """Launches the original backend.""" … … 1255 1255 if not isBanner : 1256 1256 arguments = [os.environ["DEVICE_URI"]] + [a.encode("UTF-8") for a in sys.argv[1:]] 1257 else : 1257 else : 1258 1258 # For banners, we absolutely WANT 1259 1259 # to remove any filename from the command line ! … … 1264 1264 # TODO : do something about the job title : if we are printing a banner and the backend 1265 1265 # TODO : uses the job's title to name an output file (cups-pdf:// for example), we're stuck ! 1266 1266 1267 1267 self.logdebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a.decode("UTF-8") for a in arguments]))) 1268 1268 pid = os.fork() … … 1277 1277 except OSError, msg : 1278 1278 self.logdebug("execve() failed: %s" % msg) 1279 self.logdebug("We shouldn't be there !!!") 1279 self.logdebug("We shouldn't be there !!!") 1280 1280 os._exit(-1) 1281 1282 self.logdebug("Waiting for original backend to exit...") 1281 1282 self.logdebug("Waiting for original backend to exit...") 1283 1283 killed = False 1284 1284 status = -1 … … 1289 1289 if err == 4 : 1290 1290 killed = True 1291 1291 1292 1292 if os.WIFEXITED(status) : 1293 1293 status = os.WEXITSTATUS(status) … … 1297 1297 level = "error" 1298 1298 self.Reason = message 1299 else : 1299 else : 1300 1300 level = "info" 1301 1301 self.printInfo(message, level) … … 1309 1309 self.printInfo(self.Reason, "warn") 1310 1310 return 1 1311 1312 if __name__ == "__main__" : 1311 1312 if __name__ == "__main__" : 1313 1313 # This is a CUPS backend, we should act and die like a CUPS backend 1314 1314 wrapper = CUPSBackend() 1315 1315 if len(sys.argv) == 1 : 1316 1316 print "\n".join(wrapper.discoverOtherBackends()) 1317 sys.exit(0) 1318 elif len(sys.argv) not in (6, 7) : 1317 sys.exit(0) 1318 elif len(sys.argv) not in (6, 7) : 1319 1319 logerr("ERROR: %s job-id user title copies options [file]\n"\ 1320 1320 % sys.argv[0]) 1321 1321 sys.exit(1) 1322 else : 1322 else : 1323 1323 os.environ["PATH"] = "%s:/bin:/usr/bin:/usr/local/bin:/opt/bin:/sbin:/usr/sbin" % os.environ.get("PATH", "") 1324 1324 try : … … 1333 1333 wrapper.accounter = openAccounter(wrapper) 1334 1334 wrapper.precomputeJobSize() 1335 wrapper.exportJobInfo() # exports a first time to give hints to external scripts 1335 wrapper.exportJobInfo() # exports a first time to give hints to external scripts 1336 1336 wrapper.overwriteJobAttributes() 1337 1337 wrapper.exportJobInfo() # re-exports in case it was overwritten 1338 1338 retcode = wrapper.mainWork() 1339 except KeyboardInterrupt : 1339 except KeyboardInterrupt : 1340 1340 wrapper.printInfo(_("Job %s interrupted by the administrator !") % wrapper.Ticket.JobId, "warn") 1341 1341 retcode = 0 1342 except SystemExit, err : 1342 except SystemExit, err : 1343 1343 retcode = err.code 1344 except : 1344 except : 1345 1345 try : 1346 1346 wrapper.crashed("cupspykota backend failed") 1347 except : 1347 except : 1348 1348 crashed("cupspykota backend failed") 1349 1349 retcode = 1 1350 finally : 1350 finally : 1351 1351 wrapper.clean() 1352 1352 sys.exit(retcode)