common.py revision cad0bb9f621ff1ccfb584e18249b09768c30a0c0
1# Copyright (C) 2008 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15import copy 16import errno 17import getopt 18import getpass 19import imp 20import os 21import platform 22import re 23import shutil 24import subprocess 25import sys 26import tempfile 27import threading 28import time 29import zipfile 30 31try: 32 from hashlib import sha1 as sha1 33except ImportError: 34 from sha import sha as sha1 35 36# missing in Python 2.4 and before 37if not hasattr(os, "SEEK_SET"): 38 os.SEEK_SET = 0 39 40class Options(object): pass 41OPTIONS = Options() 42OPTIONS.search_path = "out/host/linux-x86" 43OPTIONS.verbose = False 44OPTIONS.tempfiles = [] 45OPTIONS.device_specific = None 46OPTIONS.extras = {} 47OPTIONS.info_dict = None 48 49 50# Values for "certificate" in apkcerts that mean special things. 51SPECIAL_CERT_STRINGS = ("PRESIGNED", "EXTERNAL") 52 53 54class ExternalError(RuntimeError): pass 55 56 57def Run(args, **kwargs): 58 """Create and return a subprocess.Popen object, printing the command 59 line on the terminal if -v was specified.""" 60 if OPTIONS.verbose: 61 print " running: ", " ".join(args) 62 return subprocess.Popen(args, **kwargs) 63 64 65def CloseInheritedPipes(): 66 """ Gmake in MAC OS has file descriptor (PIPE) leak. We close those fds 67 before doing other work.""" 68 if platform.system() != "Darwin": 69 return 70 for d in range(3, 1025): 71 try: 72 stat = os.fstat(d) 73 if stat is not None: 74 pipebit = stat[0] & 0x1000 75 if pipebit != 0: 76 os.close(d) 77 except OSError: 78 pass 79 80 81def LoadInfoDict(zip): 82 """Read and parse the META/misc_info.txt key/value pairs from the 83 input target files and return a dict.""" 84 85 d = {} 86 try: 87 for line in zip.read("META/misc_info.txt").split("\n"): 88 line = line.strip() 89 if not line or line.startswith("#"): continue 90 k, v = line.split("=", 1) 91 d[k] = v 92 except KeyError: 93 # ok if misc_info.txt doesn't exist 94 pass 95 96 # backwards compatibility: These values used to be in their own 97 # files. Look for them, in case we're processing an old 98 # target_files zip. 99 100 if "mkyaffs2_extra_flags" not in d: 101 try: 102 d["mkyaffs2_extra_flags"] = zip.read("META/mkyaffs2-extra-flags.txt").strip() 103 except KeyError: 104 # ok if flags don't exist 105 pass 106 107 if "recovery_api_version" not in d: 108 try: 109 d["recovery_api_version"] = zip.read("META/recovery-api-version.txt").strip() 110 except KeyError: 111 raise ValueError("can't find recovery API version in input target-files") 112 113 if "tool_extensions" not in d: 114 try: 115 d["tool_extensions"] = zip.read("META/tool-extensions.txt").strip() 116 except KeyError: 117 # ok if extensions don't exist 118 pass 119 120 try: 121 data = zip.read("META/imagesizes.txt") 122 for line in data.split("\n"): 123 if not line: continue 124 name, value = line.split(" ", 1) 125 if not value: continue 126 if name == "blocksize": 127 d[name] = value 128 else: 129 d[name + "_size"] = value 130 except KeyError: 131 pass 132 133 def makeint(key): 134 if key in d: 135 d[key] = int(d[key], 0) 136 137 makeint("recovery_api_version") 138 makeint("blocksize") 139 makeint("system_size") 140 makeint("userdata_size") 141 makeint("recovery_size") 142 makeint("boot_size") 143 144 d["fstab"] = LoadRecoveryFSTab(zip) 145 return d 146 147def LoadRecoveryFSTab(zip): 148 class Partition(object): 149 pass 150 151 try: 152 data = zip.read("RECOVERY/RAMDISK/etc/recovery.fstab") 153 except KeyError: 154 raise ValueError("Could not find RECOVERY/RAMDISK/etc/recovery.fstab") 155 156 d = {} 157 for line in data.split("\n"): 158 line = line.strip() 159 if not line or line.startswith("#"): continue 160 pieces = line.split() 161 if not (3 <= len(pieces) <= 4): 162 raise ValueError("malformed recovery.fstab line: \"%s\"" % (line,)) 163 164 p = Partition() 165 p.mount_point = pieces[0] 166 p.fs_type = pieces[1] 167 p.device = pieces[2] 168 if len(pieces) == 4: 169 p.device2 = pieces[3] 170 else: 171 p.device2 = None 172 173 d[p.mount_point] = p 174 return d 175 176 177def DumpInfoDict(d): 178 for k, v in sorted(d.items()): 179 print "%-25s = (%s) %s" % (k, type(v).__name__, v) 180 181def BuildAndAddBootableImage(sourcedir, targetname, output_zip, info_dict): 182 """Take a kernel, cmdline, and ramdisk directory from the input (in 183 'sourcedir'), and turn them into a boot image. Put the boot image 184 into the output zip file under the name 'targetname'. Returns 185 targetname on success or None on failure (if sourcedir does not 186 appear to contain files for the requested image).""" 187 188 print "creating %s..." % (targetname,) 189 190 img = BuildBootableImage(sourcedir) 191 if img is None: 192 return None 193 194 CheckSize(img, targetname, info_dict) 195 ZipWriteStr(output_zip, targetname, img) 196 return targetname 197 198def BuildBootableImage(sourcedir): 199 """Take a kernel, cmdline, and ramdisk directory from the input (in 200 'sourcedir'), and turn them into a boot image. Return the image 201 data, or None if sourcedir does not appear to contains files for 202 building the requested image.""" 203 204 if (not os.access(os.path.join(sourcedir, "RAMDISK"), os.F_OK) or 205 not os.access(os.path.join(sourcedir, "kernel"), os.F_OK)): 206 return None 207 208 ramdisk_img = tempfile.NamedTemporaryFile() 209 img = tempfile.NamedTemporaryFile() 210 211 p1 = Run(["mkbootfs", os.path.join(sourcedir, "RAMDISK")], 212 stdout=subprocess.PIPE) 213 p2 = Run(["minigzip"], 214 stdin=p1.stdout, stdout=ramdisk_img.file.fileno()) 215 216 p2.wait() 217 p1.wait() 218 assert p1.returncode == 0, "mkbootfs of %s ramdisk failed" % (targetname,) 219 assert p2.returncode == 0, "minigzip of %s ramdisk failed" % (targetname,) 220 221 cmd = ["mkbootimg", "--kernel", os.path.join(sourcedir, "kernel")] 222 223 fn = os.path.join(sourcedir, "cmdline") 224 if os.access(fn, os.F_OK): 225 cmd.append("--cmdline") 226 cmd.append(open(fn).read().rstrip("\n")) 227 228 fn = os.path.join(sourcedir, "base") 229 if os.access(fn, os.F_OK): 230 cmd.append("--base") 231 cmd.append(open(fn).read().rstrip("\n")) 232 233 fn = os.path.join(sourcedir, "pagesize") 234 if os.access(fn, os.F_OK): 235 cmd.append("--pagesize") 236 cmd.append(open(fn).read().rstrip("\n")) 237 238 cmd.extend(["--ramdisk", ramdisk_img.name, 239 "--output", img.name]) 240 241 p = Run(cmd, stdout=subprocess.PIPE) 242 p.communicate() 243 assert p.returncode == 0, "mkbootimg of %s image failed" % ( 244 os.path.basename(sourcedir),) 245 246 img.seek(os.SEEK_SET, 0) 247 data = img.read() 248 249 ramdisk_img.close() 250 img.close() 251 252 return data 253 254 255def AddRecovery(output_zip, info_dict): 256 BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "RECOVERY"), 257 "recovery.img", output_zip, info_dict) 258 259def AddBoot(output_zip, info_dict): 260 BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "BOOT"), 261 "boot.img", output_zip, info_dict) 262 263def UnzipTemp(filename, pattern=None): 264 """Unzip the given archive into a temporary directory and return the name.""" 265 266 tmp = tempfile.mkdtemp(prefix="targetfiles-") 267 OPTIONS.tempfiles.append(tmp) 268 cmd = ["unzip", "-o", "-q", filename, "-d", tmp] 269 if pattern is not None: 270 cmd.append(pattern) 271 p = Run(cmd, stdout=subprocess.PIPE) 272 p.communicate() 273 if p.returncode != 0: 274 raise ExternalError("failed to unzip input target-files \"%s\"" % 275 (filename,)) 276 return tmp 277 278 279def GetKeyPasswords(keylist): 280 """Given a list of keys, prompt the user to enter passwords for 281 those which require them. Return a {key: password} dict. password 282 will be None if the key has no password.""" 283 284 no_passwords = [] 285 need_passwords = [] 286 devnull = open("/dev/null", "w+b") 287 for k in sorted(keylist): 288 # We don't need a password for things that aren't really keys. 289 if k in SPECIAL_CERT_STRINGS: 290 no_passwords.append(k) 291 continue 292 293 p = Run(["openssl", "pkcs8", "-in", k+".pk8", 294 "-inform", "DER", "-nocrypt"], 295 stdin=devnull.fileno(), 296 stdout=devnull.fileno(), 297 stderr=subprocess.STDOUT) 298 p.communicate() 299 if p.returncode == 0: 300 no_passwords.append(k) 301 else: 302 need_passwords.append(k) 303 devnull.close() 304 305 key_passwords = PasswordManager().GetPasswords(need_passwords) 306 key_passwords.update(dict.fromkeys(no_passwords, None)) 307 return key_passwords 308 309 310def SignFile(input_name, output_name, key, password, align=None, 311 whole_file=False): 312 """Sign the input_name zip/jar/apk, producing output_name. Use the 313 given key and password (the latter may be None if the key does not 314 have a password. 315 316 If align is an integer > 1, zipalign is run to align stored files in 317 the output zip on 'align'-byte boundaries. 318 319 If whole_file is true, use the "-w" option to SignApk to embed a 320 signature that covers the whole file in the archive comment of the 321 zip file. 322 """ 323 324 if align == 0 or align == 1: 325 align = None 326 327 if align: 328 temp = tempfile.NamedTemporaryFile() 329 sign_name = temp.name 330 else: 331 sign_name = output_name 332 333 cmd = ["java", "-Xmx512m", "-jar", 334 os.path.join(OPTIONS.search_path, "framework", "signapk.jar")] 335 if whole_file: 336 cmd.append("-w") 337 cmd.extend([key + ".x509.pem", key + ".pk8", 338 input_name, sign_name]) 339 340 p = Run(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 341 if password is not None: 342 password += "\n" 343 p.communicate(password) 344 if p.returncode != 0: 345 raise ExternalError("signapk.jar failed: return code %s" % (p.returncode,)) 346 347 if align: 348 p = Run(["zipalign", "-f", str(align), sign_name, output_name]) 349 p.communicate() 350 if p.returncode != 0: 351 raise ExternalError("zipalign failed: return code %s" % (p.returncode,)) 352 temp.close() 353 354 355def CheckSize(data, target, info_dict): 356 """Check the data string passed against the max size limit, if 357 any, for the given target. Raise exception if the data is too big. 358 Print a warning if the data is nearing the maximum size.""" 359 360 if target.endswith(".img"): target = target[:-4] 361 mount_point = "/" + target 362 363 if info_dict["fstab"]: 364 if mount_point == "/userdata": mount_point = "/data" 365 p = info_dict["fstab"][mount_point] 366 fs_type = p.fs_type 367 limit = info_dict.get(p.device + "_size", None) 368 if not fs_type or not limit: return 369 370 if fs_type == "yaffs2": 371 # image size should be increased by 1/64th to account for the 372 # spare area (64 bytes per 2k page) 373 limit = limit / 2048 * (2048+64) 374 375 size = len(data) 376 pct = float(size) * 100.0 / limit 377 msg = "%s size (%d) is %.2f%% of limit (%d)" % (target, size, pct, limit) 378 if pct >= 99.0: 379 raise ExternalError(msg) 380 elif pct >= 95.0: 381 print 382 print " WARNING: ", msg 383 print 384 elif OPTIONS.verbose: 385 print " ", msg 386 387 388def ReadApkCerts(tf_zip): 389 """Given a target_files ZipFile, parse the META/apkcerts.txt file 390 and return a {package: cert} dict.""" 391 certmap = {} 392 for line in tf_zip.read("META/apkcerts.txt").split("\n"): 393 line = line.strip() 394 if not line: continue 395 m = re.match(r'^name="(.*)"\s+certificate="(.*)"\s+' 396 r'private_key="(.*)"$', line) 397 if m: 398 name, cert, privkey = m.groups() 399 if cert in SPECIAL_CERT_STRINGS and not privkey: 400 certmap[name] = cert 401 elif (cert.endswith(".x509.pem") and 402 privkey.endswith(".pk8") and 403 cert[:-9] == privkey[:-4]): 404 certmap[name] = cert[:-9] 405 else: 406 raise ValueError("failed to parse line from apkcerts.txt:\n" + line) 407 return certmap 408 409 410COMMON_DOCSTRING = """ 411 -p (--path) <dir> 412 Prepend <dir>/bin to the list of places to search for binaries 413 run by this script, and expect to find jars in <dir>/framework. 414 415 -s (--device_specific) <file> 416 Path to the python module containing device-specific 417 releasetools code. 418 419 -x (--extra) <key=value> 420 Add a key/value pair to the 'extras' dict, which device-specific 421 extension code may look at. 422 423 -v (--verbose) 424 Show command lines being executed. 425 426 -h (--help) 427 Display this usage message and exit. 428""" 429 430def Usage(docstring): 431 print docstring.rstrip("\n") 432 print COMMON_DOCSTRING 433 434 435def ParseOptions(argv, 436 docstring, 437 extra_opts="", extra_long_opts=(), 438 extra_option_handler=None): 439 """Parse the options in argv and return any arguments that aren't 440 flags. docstring is the calling module's docstring, to be displayed 441 for errors and -h. extra_opts and extra_long_opts are for flags 442 defined by the caller, which are processed by passing them to 443 extra_option_handler.""" 444 445 try: 446 opts, args = getopt.getopt( 447 argv, "hvp:s:x:" + extra_opts, 448 ["help", "verbose", "path=", "device_specific=", "extra="] + 449 list(extra_long_opts)) 450 except getopt.GetoptError, err: 451 Usage(docstring) 452 print "**", str(err), "**" 453 sys.exit(2) 454 455 path_specified = False 456 457 for o, a in opts: 458 if o in ("-h", "--help"): 459 Usage(docstring) 460 sys.exit() 461 elif o in ("-v", "--verbose"): 462 OPTIONS.verbose = True 463 elif o in ("-p", "--path"): 464 OPTIONS.search_path = a 465 elif o in ("-s", "--device_specific"): 466 OPTIONS.device_specific = a 467 elif o in ("-x", "--extra"): 468 key, value = a.split("=", 1) 469 OPTIONS.extras[key] = value 470 else: 471 if extra_option_handler is None or not extra_option_handler(o, a): 472 assert False, "unknown option \"%s\"" % (o,) 473 474 os.environ["PATH"] = (os.path.join(OPTIONS.search_path, "bin") + 475 os.pathsep + os.environ["PATH"]) 476 477 return args 478 479 480def Cleanup(): 481 for i in OPTIONS.tempfiles: 482 if os.path.isdir(i): 483 shutil.rmtree(i) 484 else: 485 os.remove(i) 486 487 488class PasswordManager(object): 489 def __init__(self): 490 self.editor = os.getenv("EDITOR", None) 491 self.pwfile = os.getenv("ANDROID_PW_FILE", None) 492 493 def GetPasswords(self, items): 494 """Get passwords corresponding to each string in 'items', 495 returning a dict. (The dict may have keys in addition to the 496 values in 'items'.) 497 498 Uses the passwords in $ANDROID_PW_FILE if available, letting the 499 user edit that file to add more needed passwords. If no editor is 500 available, or $ANDROID_PW_FILE isn't define, prompts the user 501 interactively in the ordinary way. 502 """ 503 504 current = self.ReadFile() 505 506 first = True 507 while True: 508 missing = [] 509 for i in items: 510 if i not in current or not current[i]: 511 missing.append(i) 512 # Are all the passwords already in the file? 513 if not missing: return current 514 515 for i in missing: 516 current[i] = "" 517 518 if not first: 519 print "key file %s still missing some passwords." % (self.pwfile,) 520 answer = raw_input("try to edit again? [y]> ").strip() 521 if answer and answer[0] not in 'yY': 522 raise RuntimeError("key passwords unavailable") 523 first = False 524 525 current = self.UpdateAndReadFile(current) 526 527 def PromptResult(self, current): 528 """Prompt the user to enter a value (password) for each key in 529 'current' whose value is fales. Returns a new dict with all the 530 values. 531 """ 532 result = {} 533 for k, v in sorted(current.iteritems()): 534 if v: 535 result[k] = v 536 else: 537 while True: 538 result[k] = getpass.getpass("Enter password for %s key> " 539 % (k,)).strip() 540 if result[k]: break 541 return result 542 543 def UpdateAndReadFile(self, current): 544 if not self.editor or not self.pwfile: 545 return self.PromptResult(current) 546 547 f = open(self.pwfile, "w") 548 os.chmod(self.pwfile, 0600) 549 f.write("# Enter key passwords between the [[[ ]]] brackets.\n") 550 f.write("# (Additional spaces are harmless.)\n\n") 551 552 first_line = None 553 sorted = [(not v, k, v) for (k, v) in current.iteritems()] 554 sorted.sort() 555 for i, (_, k, v) in enumerate(sorted): 556 f.write("[[[ %s ]]] %s\n" % (v, k)) 557 if not v and first_line is None: 558 # position cursor on first line with no password. 559 first_line = i + 4 560 f.close() 561 562 p = Run([self.editor, "+%d" % (first_line,), self.pwfile]) 563 _, _ = p.communicate() 564 565 return self.ReadFile() 566 567 def ReadFile(self): 568 result = {} 569 if self.pwfile is None: return result 570 try: 571 f = open(self.pwfile, "r") 572 for line in f: 573 line = line.strip() 574 if not line or line[0] == '#': continue 575 m = re.match(r"^\[\[\[\s*(.*?)\s*\]\]\]\s*(\S+)$", line) 576 if not m: 577 print "failed to parse password file: ", line 578 else: 579 result[m.group(2)] = m.group(1) 580 f.close() 581 except IOError, e: 582 if e.errno != errno.ENOENT: 583 print "error reading password file: ", str(e) 584 return result 585 586 587def ZipWriteStr(zip, filename, data, perms=0644): 588 # use a fixed timestamp so the output is repeatable. 589 zinfo = zipfile.ZipInfo(filename=filename, 590 date_time=(2009, 1, 1, 0, 0, 0)) 591 zinfo.compress_type = zip.compression 592 zinfo.external_attr = perms << 16 593 zip.writestr(zinfo, data) 594 595 596class DeviceSpecificParams(object): 597 module = None 598 def __init__(self, **kwargs): 599 """Keyword arguments to the constructor become attributes of this 600 object, which is passed to all functions in the device-specific 601 module.""" 602 for k, v in kwargs.iteritems(): 603 setattr(self, k, v) 604 self.extras = OPTIONS.extras 605 606 if self.module is None: 607 path = OPTIONS.device_specific 608 if not path: return 609 try: 610 if os.path.isdir(path): 611 info = imp.find_module("releasetools", [path]) 612 else: 613 d, f = os.path.split(path) 614 b, x = os.path.splitext(f) 615 if x == ".py": 616 f = b 617 info = imp.find_module(f, [d]) 618 self.module = imp.load_module("device_specific", *info) 619 except ImportError: 620 print "unable to load device-specific module; assuming none" 621 622 def _DoCall(self, function_name, *args, **kwargs): 623 """Call the named function in the device-specific module, passing 624 the given args and kwargs. The first argument to the call will be 625 the DeviceSpecific object itself. If there is no module, or the 626 module does not define the function, return the value of the 627 'default' kwarg (which itself defaults to None).""" 628 if self.module is None or not hasattr(self.module, function_name): 629 return kwargs.get("default", None) 630 return getattr(self.module, function_name)(*((self,) + args), **kwargs) 631 632 def FullOTA_Assertions(self): 633 """Called after emitting the block of assertions at the top of a 634 full OTA package. Implementations can add whatever additional 635 assertions they like.""" 636 return self._DoCall("FullOTA_Assertions") 637 638 def FullOTA_InstallEnd(self): 639 """Called at the end of full OTA installation; typically this is 640 used to install the image for the device's baseband processor.""" 641 return self._DoCall("FullOTA_InstallEnd") 642 643 def IncrementalOTA_Assertions(self): 644 """Called after emitting the block of assertions at the top of an 645 incremental OTA package. Implementations can add whatever 646 additional assertions they like.""" 647 return self._DoCall("IncrementalOTA_Assertions") 648 649 def IncrementalOTA_VerifyEnd(self): 650 """Called at the end of the verification phase of incremental OTA 651 installation; additional checks can be placed here to abort the 652 script before any changes are made.""" 653 return self._DoCall("IncrementalOTA_VerifyEnd") 654 655 def IncrementalOTA_InstallEnd(self): 656 """Called at the end of incremental OTA installation; typically 657 this is used to install the image for the device's baseband 658 processor.""" 659 return self._DoCall("IncrementalOTA_InstallEnd") 660 661class File(object): 662 def __init__(self, name, data): 663 self.name = name 664 self.data = data 665 self.size = len(data) 666 self.sha1 = sha1(data).hexdigest() 667 668 def WriteToTemp(self): 669 t = tempfile.NamedTemporaryFile() 670 t.write(self.data) 671 t.flush() 672 return t 673 674 def AddToZip(self, z): 675 ZipWriteStr(z, self.name, self.data) 676 677DIFF_PROGRAM_BY_EXT = { 678 ".gz" : "imgdiff", 679 ".zip" : ["imgdiff", "-z"], 680 ".jar" : ["imgdiff", "-z"], 681 ".apk" : ["imgdiff", "-z"], 682 ".img" : "imgdiff", 683 } 684 685class Difference(object): 686 def __init__(self, tf, sf): 687 self.tf = tf 688 self.sf = sf 689 self.patch = None 690 691 def ComputePatch(self): 692 """Compute the patch (as a string of data) needed to turn sf into 693 tf. Returns the same tuple as GetPatch().""" 694 695 tf = self.tf 696 sf = self.sf 697 698 ext = os.path.splitext(tf.name)[1] 699 diff_program = DIFF_PROGRAM_BY_EXT.get(ext, "bsdiff") 700 701 ttemp = tf.WriteToTemp() 702 stemp = sf.WriteToTemp() 703 704 ext = os.path.splitext(tf.name)[1] 705 706 try: 707 ptemp = tempfile.NamedTemporaryFile() 708 if isinstance(diff_program, list): 709 cmd = copy.copy(diff_program) 710 else: 711 cmd = [diff_program] 712 cmd.append(stemp.name) 713 cmd.append(ttemp.name) 714 cmd.append(ptemp.name) 715 p = Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 716 _, err = p.communicate() 717 if err or p.returncode != 0: 718 print "WARNING: failure running %s:\n%s\n" % (diff_program, err) 719 return None 720 diff = ptemp.read() 721 finally: 722 ptemp.close() 723 stemp.close() 724 ttemp.close() 725 726 self.patch = diff 727 return self.tf, self.sf, self.patch 728 729 730 def GetPatch(self): 731 """Return a tuple (target_file, source_file, patch_data). 732 patch_data may be None if ComputePatch hasn't been called, or if 733 computing the patch failed.""" 734 return self.tf, self.sf, self.patch 735 736 737def ComputeDifferences(diffs): 738 """Call ComputePatch on all the Difference objects in 'diffs'.""" 739 print len(diffs), "diffs to compute" 740 741 # Do the largest files first, to try and reduce the long-pole effect. 742 by_size = [(i.tf.size, i) for i in diffs] 743 by_size.sort(reverse=True) 744 by_size = [i[1] for i in by_size] 745 746 lock = threading.Lock() 747 diff_iter = iter(by_size) # accessed under lock 748 749 def worker(): 750 try: 751 lock.acquire() 752 for d in diff_iter: 753 lock.release() 754 start = time.time() 755 d.ComputePatch() 756 dur = time.time() - start 757 lock.acquire() 758 759 tf, sf, patch = d.GetPatch() 760 if sf.name == tf.name: 761 name = tf.name 762 else: 763 name = "%s (%s)" % (tf.name, sf.name) 764 if patch is None: 765 print "patching failed! %s" % (name,) 766 else: 767 print "%8.2f sec %8d / %8d bytes (%6.2f%%) %s" % ( 768 dur, len(patch), tf.size, 100.0 * len(patch) / tf.size, name) 769 lock.release() 770 except Exception, e: 771 print e 772 raise 773 774 # start worker threads; wait for them all to finish. 775 threads = [threading.Thread(target=worker) 776 for i in range(OPTIONS.worker_threads)] 777 for th in threads: 778 th.start() 779 while threads: 780 threads.pop().join() 781 782 783# map recovery.fstab's fs_types to mount/format "partition types" 784PARTITION_TYPES = { "yaffs2": "MTD", "mtd": "MTD", 785 "ext4": "EMMC", "emmc": "EMMC" } 786 787def GetTypeAndDevice(mount_point, info): 788 fstab = info["fstab"] 789 if fstab: 790 return PARTITION_TYPES[fstab[mount_point].fs_type], fstab[mount_point].device 791 else: 792 return None 793