Changeset 3438 for tea4cups/trunk/tea4cups
- Timestamp:
- 10/06/08 00:26:54 (16 years ago)
- Files:
-
- 1 modified
Legend:
- Unmodified
- Added
- Removed
-
tea4cups/trunk/tea4cups
r693 r3438 1 1 #! /usr/bin/env python 2 # -*- coding: ISO-8859-15-*-2 # -*- coding: utf-8 -*- 3 3 4 4 # Tea4CUPS : Tee for CUPS … … 35 35 the Free Software Foundation; either version 2 of the License, or 36 36 (at your option) any later version. 37 37 38 38 This program is distributed in the hope that it will be useful, 39 39 but WITHOUT ANY WARRANTY; without even the implied warranty of 40 40 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 41 41 GNU General Public License for more details. 42 42 43 43 You should have received a copy of the GNU General Public License 44 44 along with this program; if not, write to the Free Software … … 49 49 50 50 Copy the different files where they belong : 51 51 52 52 $ cp tea4cups /usr/lib/cups/backend/ 53 53 $ chown root.root /usr/lib/cups/backend/tea4cups 54 54 $ chmod 700 /usr/lib/cups/backend/tea4cups 55 55 $ cp tea4cups.conf /etc/cupsd/ 56 56 57 57 Now edit the configuration file to suit your needs : 58 58 59 59 $ vi /etc/cupsd/tea4cups.conf 60 60 61 61 NB : you can use emacs as well :-) 62 62 63 63 Finally restart CUPS : 64 64 65 65 $ /etc/init.d/cupsys restart 66 66 67 67 You can now create "Tea4CUPS Managed" print queues from 68 68 CUPS' web interface, or using lpadmin. 69 70 Send bug reports to : alet@librelogiciel.com 71 """ 69 70 Send bug reports to : alet@librelogiciel.com 71 """ 72 72 73 73 import sys … … 105 105 """IPP related exceptions.""" 106 106 pass 107 107 108 108 IPP_VERSION = "1.1" # default version number 109 109 … … 314 314 IPP_MULTIPLE_JOBS_NOT_SUPPORTED = 0x0509 315 315 IPP_PRINTER_IS_DEACTIVATED = 0x50a 316 316 317 317 CUPS_PRINTER_LOCAL = 0x0000 318 318 CUPS_PRINTER_CLASS = 0x0001 … … 341 341 CUPS_PRINTER_COMMANDS = 0x8000 342 342 CUPS_PRINTER_OPTIONS = 0xe6ff 343 343 344 344 class FakeAttribute : 345 345 """Fakes an IPPRequest attribute to simplify usage syntax.""" … … 348 348 self.request = request 349 349 self.name = name 350 350 351 351 def __setitem__(self, key, value) : 352 352 """Appends the value to the real attribute.""" … … 359 359 attribute[j][1].append(value) 360 360 return 361 attribute.append((key, [value])) 362 361 attribute.append((key, [value])) 362 363 363 def __getitem__(self, key) : 364 364 """Returns an attribute's value.""" … … 373 373 if answer : 374 374 return answer 375 raise KeyError, key 376 375 raise KeyError, key 376 377 377 class IPPRequest : 378 378 """A class for IPP requests.""" 379 379 attributes_types = ("operation", "job", "printer", "unsupported", \ 380 380 "subscription", "event_notification") 381 def __init__(self, data="", version=IPP_VERSION, 381 def __init__(self, data="", version=IPP_VERSION, 382 382 operation_id=None, \ 383 383 request_id=None, \ 384 384 debug=False) : 385 385 """Initializes an IPP Message object. 386 386 387 387 Parameters : 388 388 389 389 data : the complete IPP Message's content. 390 390 debug : a boolean value to output debug info on stderr. … … 393 393 self._data = data 394 394 self.parsed = False 395 395 396 396 # Initializes message 397 self.setVersion(version) 397 self.setVersion(version) 398 398 self.setOperationId(operation_id) 399 399 self.setRequestId(request_id) 400 400 self.data = "" 401 401 402 402 for attrtype in self.attributes_types : 403 403 setattr(self, "_%s_attributes" % attrtype, [[]]) 404 405 # Initialize tags 404 405 # Initialize tags 406 406 self.tags = [ None ] * 256 # by default all tags reserved 407 407 408 408 # Delimiter tags 409 409 self.tags[0x01] = "operation-attributes-tag" … … 414 414 self.tags[0x06] = "subscription-attributes-tag" 415 415 self.tags[0x07] = "event_notification-attributes-tag" 416 416 417 417 # out of band values 418 418 self.tags[0x10] = "unsupported" … … 423 423 self.tags[0x16] = "delete-attribute" 424 424 self.tags[0x17] = "admin-define" 425 425 426 426 # integer values 427 427 self.tags[0x20] = "generic-integer" … … 429 429 self.tags[0x22] = "boolean" 430 430 self.tags[0x23] = "enum" 431 431 432 432 # octetString 433 433 self.tags[0x30] = "octetString-with-an-unspecified-format" … … 439 439 self.tags[0x36] = "nameWithLanguage" 440 440 self.tags[0x37] = "endCollection" 441 441 442 442 # character strings 443 443 self.tags[0x40] = "generic-character-string" … … 451 451 self.tags[0x49] = "mimeMediaType" 452 452 self.tags[0x4a] = "memberAttrName" 453 453 454 454 # Reverse mapping to generate IPP messages 455 455 self.tagvalues = {} … … 458 458 if value is not None : 459 459 self.tagvalues[value] = i 460 461 def __getattr__(self, name) : 460 461 def __getattr__(self, name) : 462 462 """Fakes attribute access.""" 463 463 if name in self.attributes_types : … … 465 465 else : 466 466 raise AttributeError, name 467 468 def __str__(self) : 467 468 def __str__(self) : 469 469 """Returns the parsed IPP message in a readable form.""" 470 470 if not self.parsed : … … 480 480 for (name, value) in attribute : 481 481 mybuffer.append(" %s : %s" % (name, value)) 482 if self.data : 482 if self.data : 483 483 mybuffer.append("IPP datas : %s" % repr(self.data)) 484 484 return "\n".join(mybuffer) 485 486 def logDebug(self, msg) : 485 486 def logDebug(self, msg) : 487 487 """Prints a debug message.""" 488 488 if self.debug : 489 489 sys.stderr.write("%s\n" % msg) 490 490 sys.stderr.flush() 491 491 492 492 def setVersion(self, version) : 493 493 """Sets the request's operation id.""" … … 498 498 if len(version) == 2 : # 2-tuple 499 499 self.version = version 500 else : 500 else : 501 501 try : 502 502 self.version = [int(p) for p in str(float(version)).split(".")] 503 503 except : 504 504 self.version = [int(p) for p in IPP_VERSION.split(".")] 505 506 def setOperationId(self, opid) : 505 506 def setOperationId(self, opid) : 507 507 """Sets the request's operation id.""" 508 508 self.operation_id = opid 509 510 def setRequestId(self, reqid) : 509 510 def setRequestId(self, reqid) : 511 511 """Sets the request's request id.""" 512 512 self.request_id = reqid 513 514 def dump(self) : 513 514 def dump(self) : 515 515 """Generates an IPP Message. 516 516 517 517 Returns the message as a string of text. 518 """ 518 """ 519 519 mybuffer = [] 520 520 if None not in (self.version, self.operation_id) : … … 534 534 mybuffer.append(attrname) 535 535 nameprinted = 1 536 else : 536 else : 537 537 mybuffer.append(pack(">H", 0)) 538 538 if vtype in ("integer", "enum") : … … 542 542 mybuffer.append(pack(">H", 1)) 543 543 mybuffer.append(chr(val)) 544 else : 544 else : 545 545 mybuffer.append(pack(">H", len(val))) 546 546 mybuffer.append(val) 547 547 mybuffer.append(chr(self.tagvalues["end-of-attributes-tag"])) 548 mybuffer.append(self.data) 548 mybuffer.append(self.data) 549 549 return "".join(mybuffer) 550 550 551 551 def parse(self) : 552 552 """Parses an IPP Request. 553 553 554 554 NB : Only a subset of RFC2910 is implemented. 555 555 """ 556 556 self._curname = None 557 557 self._curattributes = None 558 558 559 559 self.setVersion((ord(self._data[0]), ord(self._data[1]))) 560 560 self.setOperationId(unpack(">H", self._data[2:4])[0]) … … 575 575 self.position -= 1 576 576 continue 577 oldtag = tag 577 oldtag = tag 578 578 tag = ord(self._data[self.position]) 579 579 if tag == oldtag : … … 581 581 except IndexError : 582 582 raise IPPError, "Unexpected end of IPP message." 583 584 self.data = self._data[self.position+1:] 583 584 self.data = self._data[self.position+1:] 585 585 self.parsed = True 586 587 def parseTag(self) : 586 587 def parseTag(self) : 588 588 """Extracts information from an IPP tag.""" 589 589 pos = self.position … … 594 594 if not namelength : 595 595 name = self._curname 596 else : 596 else : 597 597 posend += namelength 598 598 self._curname = name = self._data[pos2:posend] … … 603 603 if tagtype in ("integer", "enum") : 604 604 value = unpack(">I", value)[0] 605 elif tagtype == "boolean" : 605 elif tagtype == "boolean" : 606 606 value = ord(value) 607 try : 607 try : 608 608 (oldname, oldval) = self._curattributes[-1][-1] 609 609 if oldname == name : 610 610 oldval.append((tagtype, value)) 611 else : 611 else : 612 612 raise IndexError 613 except IndexError : 613 except IndexError : 614 614 self._curattributes[-1].append((name, [(tagtype, value)])) 615 615 self.logDebug("%s(%s) : %s" % (name, tagtype, value)) 616 616 return posend - self.position 617 618 def operation_attributes_tag(self) : 617 618 def operation_attributes_tag(self) : 619 619 """Indicates that the parser enters into an operation-attributes-tag group.""" 620 620 self._curattributes = self._operation_attributes 621 621 return self.parseTag() 622 623 def job_attributes_tag(self) : 622 623 def job_attributes_tag(self) : 624 624 """Indicates that the parser enters into a job-attributes-tag group.""" 625 625 self._curattributes = self._job_attributes 626 626 return self.parseTag() 627 628 def printer_attributes_tag(self) : 627 628 def printer_attributes_tag(self) : 629 629 """Indicates that the parser enters into a printer-attributes-tag group.""" 630 630 self._curattributes = self._printer_attributes 631 631 return self.parseTag() 632 633 def unsupported_attributes_tag(self) : 632 633 def unsupported_attributes_tag(self) : 634 634 """Indicates that the parser enters into an unsupported-attributes-tag group.""" 635 635 self._curattributes = self._unsupported_attributes 636 636 return self.parseTag() 637 638 def subscription_attributes_tag(self) : 637 638 def subscription_attributes_tag(self) : 639 639 """Indicates that the parser enters into a subscription-attributes-tag group.""" 640 640 self._curattributes = self._subscription_attributes 641 641 return self.parseTag() 642 643 def event_notification_attributes_tag(self) : 642 643 def event_notification_attributes_tag(self) : 644 644 """Indicates that the parser enters into an event-notification-attributes-tag group.""" 645 645 self._curattributes = self._event_notification_attributes 646 646 return self.parseTag() 647 648 647 648 649 649 class CUPS : 650 650 """A class for a CUPS instance.""" … … 655 655 if self.url.endswith("/") : 656 656 self.url = self.url[:-1] 657 else : 657 else : 658 658 self.url = self.getDefaultURL() 659 659 self.username = username … … 665 665 self.lastErrorMessage = None 666 666 self.requestId = None 667 668 def getDefaultURL(self) : 667 668 def getDefaultURL(self) : 669 669 """Builds a default URL.""" 670 670 # TODO : encryption methods. … … 675 675 # we can't handle this right now, so we use the default instead. 676 676 return "http://localhost:%s" % port 677 else : 677 else : 678 678 return "http://%s:%s" % (server, port) 679 679 680 680 def identifierToURI(self, service, ident) : 681 681 """Transforms an identifier into a particular URI depending on requested service.""" … … 683 683 service, 684 684 ident) 685 686 def nextRequestId(self) : 685 686 def nextRequestId(self) : 687 687 """Increments the current request id and returns the new value.""" 688 688 try : 689 689 self.requestId += 1 690 except TypeError : 690 except TypeError : 691 691 self.requestId = 1 692 692 return self.requestId 693 693 694 694 def newRequest(self, operationid=None) : 695 695 """Generates a new empty request.""" … … 701 701 req.operation["attributes-natural-language"] = ("naturalLanguage", self.language) 702 702 return req 703 703 704 704 def doRequest(self, req, url=None) : 705 705 """Sends a request to the CUPS server. 706 706 returns a new IPPRequest object, containing the parsed answer. 707 """ 707 """ 708 708 connexion = urllib2.Request(url=url or self.url, \ 709 709 data=req.dump()) … … 715 715 self.username, \ 716 716 self.password or "") 717 authhandler = urllib2.HTTPBasicAuthHandler(pwmanager) 717 authhandler = urllib2.HTTPBasicAuthHandler(pwmanager) 718 718 opener = urllib2.build_opener(authhandler) 719 719 urllib2.install_opener(opener) 720 self.lastError = None 720 self.lastError = None 721 721 self.lastErrorMessage = None 722 try : 722 try : 723 723 response = urllib2.urlopen(connexion) 724 except (urllib2.URLError, urllib2.HTTPError, socket.error), error : 724 except (urllib2.URLError, urllib2.HTTPError, socket.error), error : 725 725 self.lastError = error 726 726 self.lastErrorMessage = str(error) 727 727 return None 728 else : 728 else : 729 729 datas = response.read() 730 730 ippresponse = IPPRequest(datas) 731 731 ippresponse.parse() 732 732 return ippresponse 733 734 def getPPD(self, queuename) : 733 734 def getPPD(self, queuename) : 735 735 """Retrieves the PPD for a particular queuename.""" 736 736 req = self.newRequest(IPP_GET_PRINTER_ATTRIBUTES) … … 739 739 req.operation["requested-attributes"] = ("nameWithoutLanguage", attrib) 740 740 return self.doRequest(req) # TODO : get the PPD from the actual print server 741 741 742 742 def getDefault(self) : 743 743 """Retrieves CUPS' default printer.""" 744 744 return self.doRequest(self.newRequest(CUPS_GET_DEFAULT)) 745 746 def getJobAttributes(self, jobid) : 745 746 def getJobAttributes(self, jobid) : 747 747 """Retrieves a print job's attributes.""" 748 748 req = self.newRequest(IPP_GET_JOB_ATTRIBUTES) 749 749 req.operation["job-uri"] = ("uri", self.identifierToURI("jobs", jobid)) 750 750 return self.doRequest(req) 751 752 def getPrinters(self) : 751 752 def getPrinters(self) : 753 753 """Returns the list of print queues names.""" 754 754 req = self.newRequest(CUPS_GET_PRINTERS) … … 757 757 req.operation["printer-type-mask"] = ("enum", CUPS_PRINTER_CLASS) 758 758 return [printer[1] for printer in self.doRequest(req).printer["printer-name"]] 759 760 def getDevices(self) : 759 760 def getDevices(self) : 761 761 """Returns a list of devices as (deviceclass, deviceinfo, devicemakeandmodel, deviceuri) tuples.""" 762 762 answer = self.doRequest(self.newRequest(CUPS_GET_DEVICES)) … … 765 765 [d[1] for d in answer.printer["device-make-and-model"]], \ 766 766 [d[1] for d in answer.printer["device-uri"]]) 767 768 def getPPDs(self) : 767 768 def getPPDs(self) : 769 769 """Returns a list of PPDs as (ppdnaturallanguage, ppdmake, ppdmakeandmodel, ppdname) tuples.""" 770 770 answer = self.doRequest(self.newRequest(CUPS_GET_PPDS)) … … 773 773 [d[1] for d in answer.printer["ppd-make-and-model"]], \ 774 774 [d[1] for d in answer.printer["ppd-name"]]) 775 775 776 776 def createSubscription(self, uri, events=["all"], 777 777 userdata=None, … … 784 784 jobid=None) : 785 785 """Creates a job, printer or server subscription. 786 786 787 787 uri : the subscription's uri, e.g. ipp://server 788 788 events : a list of events to subscribe to, e.g. ["printer-added", "printer-deleted"] … … 794 794 timeinterval : the interval of time during notifications 795 795 jobid : the optional job id in case of a job subscription 796 """ 796 """ 797 797 if jobid is not None : 798 798 opid = IPP_CREATE_JOB_SUBSCRIPTION … … 805 805 for event in events : 806 806 req.subscription["notify-events"] = ("keyword", event) 807 if userdata is not None : 807 if userdata is not None : 808 808 req.subscription["notify-user-data"] = ("octetString-with-an-unspecified-format", userdata) 809 if recipient is not None : 809 if recipient is not None : 810 810 req.subscription["notify-recipient"] = ("uri", recipient) 811 811 if pullmethod is not None : … … 822 822 req.subscription["notify-job-id"] = ("integer", jobid) 823 823 return self.doRequest(req) 824 825 def cancelSubscription(self, uri, subscriptionid, jobid=None) : 824 825 def cancelSubscription(self, uri, subscriptionid, jobid=None) : 826 826 """Cancels a subscription. 827 827 828 828 uri : the subscription's uri. 829 829 subscriptionid : the subscription's id. … … 838 838 req.event_notification["notify-subscription-id"] = ("integer", subscriptionid) 839 839 return self.doRequest(req) 840 840 841 841 842 842 class FakeConfig : … … 879 879 conffile.close() 880 880 return dirvalues 881 881 882 882 class CupsBackend : 883 883 """Base class for tools with no database access.""" … … 912 912 self.LockFile = None 913 913 914 def waitForLock(self) : 914 def waitForLock(self) : 915 915 """Waits until we can acquire the lock file.""" 916 916 lockfilename = self.DeviceURI.replace("/", ".") … … 926 926 # open the lock file, optionally creating it if needed. 927 927 self.LockFile = open(lockfilename, "a+") 928 928 929 929 # we wait indefinitely for the lock to become available. 930 930 # works over NFS too. 931 931 fcntl.lockf(self.LockFile, fcntl.LOCK_EX) 932 932 haslock = True 933 933 934 934 self.logDebug("Lock %s acquired." % lockfilename) 935 935 936 936 # Here we save the PID in the lock file, but we don't use 937 937 # it, because the lock file may be in a directory shared … … 942 942 self.LockFile.write(str(self.pid)) 943 943 self.LockFile.flush() 944 except IOError : 944 except IOError : 945 945 self.logDebug("I/O Error while waiting for lock %s" % lockfilename) 946 946 time.sleep(0.25) 947 947 948 948 def readConfig(self) : 949 949 """Reads the configuration file.""" … … 1104 1104 self.Directory = self.getPrintQueueOption(self.PrinterName, "directory", ignore=1) or tempfile.gettempdir() 1105 1105 self.DataFile = os.path.join(self.Directory, "%s-%s-%s-%s" % (self.myname, self.PrinterName, self.UserName, self.JobId)) 1106 1106 1107 1107 # check that the DEVICE_URI environment variable's value is 1108 1108 # prefixed with self.myname otherwise don't touch it. … … 1128 1128 self.RealBackend = backend 1129 1129 self.DeviceURI = device_uri 1130 1130 1131 1131 try : 1132 1132 cupsserver = CUPS() # TODO : username and password and/or encryption … … 1135 1135 raise ValueError # don't hande unix domain sockets yet. 1136 1136 self.ControlFile = "NotUsedAnymore" 1137 except : 1137 except : 1138 1138 (ippfilename, answer) = self.parseIPPRequestFile() 1139 1139 self.ControlFile = ippfilename 1140 1140 1141 1141 try : 1142 1142 john = answer.job["job-originating-host-name"] 1143 except (KeyError, AttributeError) : 1143 except (KeyError, AttributeError) : 1144 1144 try : 1145 1145 john = answer.operation["job-originating-host-name"] 1146 except (KeyError, AttributeError) : 1146 except (KeyError, AttributeError) : 1147 1147 john = (None, None) 1148 if type(john) == type([]) : 1148 if type(john) == type([]) : 1149 1149 john = john[-1] 1150 (dummy, self.ClientHost) = john 1151 try : 1150 (dummy, self.ClientHost) = john 1151 try : 1152 1152 jbing = answer.job["job-billing"] 1153 except (KeyError, AttributeError) : 1153 except (KeyError, AttributeError) : 1154 1154 jbing = (None, None) 1155 if type(jbing) == type([]) : 1155 if type(jbing) == type([]) : 1156 1156 jbing = jbing[-1] 1157 1157 (dummy, self.JobBilling) = jbing 1158 1158 1159 1159 def parseIPPRequestFile(self) : 1160 1160 """Parses the IPP message file and returns a tuple (filename, parsedvalue).""" … … 1212 1212 else : 1213 1213 infile = sys.stdin 1214 1214 1215 1215 filtercommand = self.getPrintQueueOption(self.PrinterName, "filter", \ 1216 1216 ignore=1) 1217 if filtercommand : 1217 if filtercommand : 1218 1218 self.logDebug("Data stream will be filtered through [%s]" % filtercommand) 1219 1219 filteroutput = "%s.filteroutput" % self.DataFile … … 1226 1226 infile = open(filteroutput, "rb") 1227 1227 mustclose = 1 1228 else : 1228 else : 1229 1229 self.logDebug("Data stream will be used as-is (no filter defined)") 1230 1230 1231 1231 CHUNK = 64*1024 # read 64 Kb at a time 1232 1232 dummy = 0 … … 1245 1245 dummy += 1 1246 1246 outfile.close() 1247 1247 1248 1248 if filtercommand : 1249 1249 self.logDebug("Removing filter's output file %s" % filteroutput) 1250 1250 try : 1251 1251 os.remove(filteroutput) 1252 except : 1252 except : 1253 1253 pass 1254 1254 1255 1255 if mustclose : 1256 1256 infile.close() 1257 1257 1258 1258 self.logDebug("%s bytes saved..." % sizeread) 1259 1259 self.JobSize = sizeread … … 1269 1269 try : 1270 1270 os.remove(self.DataFile) 1271 except OSError, msg : 1271 except OSError, msg : 1272 1272 self.logInfo("Problem when removing %s : %s" % (self.DataFile, msg), "error") 1273 1273 1274 1274 if self.LockFile is not None : 1275 1275 self.logDebug("Removing lock...") … … 1277 1277 fcntl.lockf(self.LockFile, fcntl.LOCK_UN) 1278 1278 self.LockFile.close() 1279 except : 1279 except : 1280 1280 self.logInfo("Problem while unlocking.", "error") 1281 else : 1281 else : 1282 1282 self.logDebug("Lock removed.") 1283 1283 self.logDebug("Clean.") … … 1303 1303 self.logDebug("Launching onfail script %s" % onfail) 1304 1304 os.system(onfail) 1305 1305 1306 1306 os.environ["TEASTATUS"] = str(retcode) 1307 1307 branches = self.enumBranches(self.PrinterName, "posthook") 1308 1308 if self.runCommands("posthook", branches, serialize) : 1309 1309 self.logInfo("An error occured during the execution of posthooks.", "warn") 1310 1310 1311 1311 for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] : 1312 1312 os.close(p[1][0]) … … 1404 1404 try : 1405 1405 (number, delay) = [int(p) for p in retry.strip().split(",")] 1406 except (ValueError, AttributeError, TypeError) : 1406 except (ValueError, AttributeError, TypeError) : 1407 1407 self.logInfo("Invalid value '%s' for the 'retry' directive for printer %s in %s." % (retry, self.PrinterName, self.conffile), "error") 1408 1408 number = 1 1409 1409 delay = 0 1410 1411 loopcount = 1 1412 while 1 : 1410 1411 loopcount = 1 1412 while 1 : 1413 1413 retcode = self.runOriginalBackend() 1414 1414 if not retcode : … … 1419 1419 time.sleep(delay) 1420 1420 loopcount += 1 1421 else : 1421 else : 1422 1422 break 1423 return retcode 1424 1423 return retcode 1424 1425 1425 def runOriginalBackend(self) : 1426 1426 """Launches the original backend.""" … … 1435 1435 os.dup2(f.fileno(), 0) 1436 1436 f.close() 1437 else : 1437 else : 1438 1438 arguments[6] = self.DataFile # in case a tea4cups filter was applied 1439 1439 try : … … 1485 1485 except SystemExit, e : 1486 1486 returncode = e.code 1487 except KeyboardInterrupt : 1487 except KeyboardInterrupt : 1488 1488 wrapper.logInfo("Job %s interrupted by the administrator !" % wrapper.JobId, "warn") 1489 1489 except : … … 1498 1498 sys.stderr.flush() 1499 1499 returncode = 1 1500 finally : 1500 finally : 1501 1501 wrapper.cleanUp() 1502 1502 sys.exit(returncode)