| Kelvin Zhang | f708dda | 2021-04-13 10:26:30 -0400 | [diff] [blame] | 1 | #!/usr/bin/env python3 | 
|  | 2 | # | 
|  | 3 | # Copyright (C) 2021 The Android Open Source Project | 
|  | 4 | # | 
|  | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | 6 | # you may not use this file except in compliance with the License. | 
|  | 7 | # You may obtain a copy of the License at | 
|  | 8 | # | 
|  | 9 | #      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | 10 | # | 
|  | 11 | # Unless required by applicable law or agreed to in writing, software | 
|  | 12 | # distributed under the License is distributed on an "AS IS" BASIS, | 
|  | 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | 14 | # See the License for the specific language governing permissions and | 
|  | 15 | # limitations under the License. | 
|  | 16 | # | 
|  | 17 |  | 
|  | 18 | """Repeatedly install an A/B update to an Android device over adb.""" | 
|  | 19 |  | 
|  | 20 | import argparse | 
|  | 21 | import sys | 
|  | 22 | from pathlib import Path | 
|  | 23 | import subprocess | 
|  | 24 | import signal | 
|  | 25 |  | 
|  | 26 |  | 
|  | 27 | def CleanupLoopDevices(): | 
|  | 28 | # b/184716804 clean up unused loop devices | 
|  | 29 | subprocess.check_call(["adb", "shell", "su", "0", "losetup", '-D']) | 
|  | 30 |  | 
|  | 31 |  | 
|  | 32 | def CancelOTA(): | 
|  | 33 | subprocess.call(["adb", "shell", "su", "0", | 
|  | 34 | "update_engine_client", "--cancel"]) | 
|  | 35 |  | 
|  | 36 |  | 
|  | 37 | def PerformOTAThenPause(otafile: Path, update_device_script: Path): | 
|  | 38 | python = sys.executable | 
|  | 39 | ota_cmd = [python, str(update_device_script), str(otafile), | 
|  | 40 | "--no-postinstall", "--no-slot-switch"] | 
|  | 41 | p = subprocess.Popen(ota_cmd) | 
|  | 42 | pid = p.pid | 
|  | 43 | try: | 
|  | 44 | ret = p.wait(10) | 
|  | 45 | if ret is not None and ret != 0: | 
|  | 46 | raise RuntimeError("OTA failed to apply") | 
|  | 47 | if ret == 0: | 
|  | 48 | print("OTA finished early? Surprise.") | 
|  | 49 | return | 
|  | 50 | except subprocess.TimeoutExpired: | 
|  | 51 | pass | 
|  | 52 | print(f"Killing {pid}") | 
|  | 53 | subprocess.check_call(["pkill", "-INT", "-P", str(pid)]) | 
|  | 54 | p.send_signal(signal.SIGINT) | 
|  | 55 | p.wait() | 
|  | 56 |  | 
|  | 57 |  | 
|  | 58 | def PerformTest(otafile: Path, resumes: int, timeout: int): | 
|  | 59 | """Install an OTA to device, raising exceptions on failure | 
|  | 60 |  | 
|  | 61 | Args: | 
|  | 62 | otafile: Path to the ota.zip to install | 
|  | 63 |  | 
|  | 64 | Return: | 
|  | 65 | None if no error, if there's an error exception will be thrown | 
|  | 66 | """ | 
|  | 67 | assert otafile.exists() | 
|  | 68 | print("Applying", otafile) | 
|  | 69 | script_dir = Path(__file__).parent.absolute() | 
|  | 70 | update_device_script = script_dir / "update_device.py" | 
|  | 71 | assert update_device_script.exists() | 
|  | 72 | print(update_device_script) | 
|  | 73 | python = sys.executable | 
|  | 74 |  | 
|  | 75 | for i in range(resumes): | 
|  | 76 | print("Pause/Resume for the", i+1, "th time") | 
|  | 77 | PerformOTAThenPause(otafile, update_device_script) | 
|  | 78 | CancelOTA() | 
|  | 79 | CleanupLoopDevices() | 
|  | 80 |  | 
|  | 81 | ota_cmd = [python, str(update_device_script), | 
|  | 82 | str(otafile), "--no-postinstall"] | 
|  | 83 | print("Finishing OTA Update", ota_cmd) | 
|  | 84 | output = subprocess.check_output( | 
|  | 85 | ota_cmd, stderr=subprocess.STDOUT, timeout=timeout).decode() | 
|  | 86 | print(output) | 
|  | 87 | if "onPayloadApplicationComplete(ErrorCode::kSuccess" not in output: | 
|  | 88 | raise RuntimeError("Failed to finish OTA") | 
|  | 89 | subprocess.call( | 
|  | 90 | ["adb", "shell", "su", "0", "update_engine_client", "--cancel"]) | 
|  | 91 | subprocess.check_call( | 
|  | 92 | ["adb", "shell", "su", "0", "update_engine_client", "--reset_status"]) | 
|  | 93 | CleanupLoopDevices() | 
|  | 94 |  | 
|  | 95 |  | 
|  | 96 | def main(): | 
|  | 97 | parser = argparse.ArgumentParser( | 
|  | 98 | description='Android A/B OTA stress test helper.') | 
|  | 99 | parser.add_argument('otafile', metavar='PAYLOAD', type=Path, | 
|  | 100 | help='the OTA package file (a .zip file) or raw payload \ | 
|  | 101 | if device uses Omaha.') | 
|  | 102 | parser.add_argument('-n', "--iterations", type=int, default=10, | 
|  | 103 | metavar='ITERATIONS', | 
|  | 104 | help='The number of iterations to run the stress test, or\ | 
|  | 105 | -1 to keep running until CTRL+C') | 
|  | 106 | parser.add_argument('-r', "--resumes", type=int, default=5, metavar='RESUMES', | 
|  | 107 | help='The number of iterations to pause the update when \ | 
|  | 108 | installing') | 
|  | 109 | parser.add_argument('-t', "--timeout", type=int, default=60*60, | 
|  | 110 | metavar='TIMEOUTS', | 
|  | 111 | help='Timeout, in seconds, when waiting for OTA to \ | 
|  | 112 | finish') | 
|  | 113 | args = parser.parse_args() | 
|  | 114 | print(args) | 
|  | 115 | n = args.iterations | 
|  | 116 | while n != 0: | 
|  | 117 | PerformTest(args.otafile, args.resumes, args.timeout) | 
|  | 118 | n -= 1 | 
|  | 119 |  | 
|  | 120 |  | 
|  | 121 | if __name__ == "__main__": | 
|  | 122 | main() |