| Yifan Hong | 0c71550 | 2021-04-19 13:48:21 -0700 | [diff] [blame] | 1 | #!/usr/bin/env python3 | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 2 | # | 
|  | 3 | # Copyright (C) 2017 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 | """Send an A/B update to an Android device over adb.""" | 
|  | 19 |  | 
| Kelvin Zhang | 4b883ea | 2020-10-08 13:26:44 -0400 | [diff] [blame] | 20 | from __future__ import print_function | 
| Andrew Lassalle | 165843c | 2019-11-05 13:30:34 -0800 | [diff] [blame] | 21 | from __future__ import absolute_import | 
|  | 22 |  | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 23 | import argparse | 
| Kelvin Zhang | aba70ab | 2020-08-04 10:32:59 -0400 | [diff] [blame] | 24 | import binascii | 
| Sen Jiang | 3b15b59 | 2017-09-26 18:21:04 -0700 | [diff] [blame] | 25 | import hashlib | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 26 | import logging | 
|  | 27 | import os | 
| Kelvin Zhang | 46a860c | 2022-09-28 19:42:39 -0700 | [diff] [blame] | 28 | import re | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 29 | import socket | 
|  | 30 | import subprocess | 
|  | 31 | import sys | 
| Kelvin Zhang | aba70ab | 2020-08-04 10:32:59 -0400 | [diff] [blame] | 32 | import struct | 
| Kelvin Zhang | 51aad99 | 2021-02-19 14:46:28 -0500 | [diff] [blame] | 33 | import tempfile | 
| Nikita Ioffe | 3a327e6 | 2021-06-30 16:05:09 +0100 | [diff] [blame] | 34 | import time | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 35 | import threading | 
| Sen Jiang | 144f9f8 | 2017-09-26 15:49:45 -0700 | [diff] [blame] | 36 | import xml.etree.ElementTree | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 37 | import zipfile | 
|  | 38 |  | 
| Andrew Lassalle | 165843c | 2019-11-05 13:30:34 -0800 | [diff] [blame] | 39 | from six.moves import BaseHTTPServer | 
|  | 40 |  | 
| Sen Jiang | a1784b7 | 2017-08-09 17:42:36 -0700 | [diff] [blame] | 41 | import update_payload.payload | 
|  | 42 |  | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 43 |  | 
|  | 44 | # The path used to store the OTA package when applying the package from a file. | 
|  | 45 | OTA_PACKAGE_PATH = '/data/ota_package' | 
|  | 46 |  | 
| Sen Jiang | a1784b7 | 2017-08-09 17:42:36 -0700 | [diff] [blame] | 47 | # The path to the payload public key on the device. | 
|  | 48 | PAYLOAD_KEY_PATH = '/etc/update_engine/update-payload-key.pub.pem' | 
|  | 49 |  | 
|  | 50 | # The port on the device that update_engine should connect to. | 
|  | 51 | DEVICE_PORT = 1234 | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 52 |  | 
| Andrew Lassalle | 165843c | 2019-11-05 13:30:34 -0800 | [diff] [blame] | 53 |  | 
| Kelvin Zhang | 46a860c | 2022-09-28 19:42:39 -0700 | [diff] [blame] | 54 | def CopyFileObjLength(fsrc, fdst, buffer_size=128 * 1024, copy_length=None, speed_limit=None): | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 55 | """Copy from a file object to another. | 
|  | 56 |  | 
|  | 57 | This function is similar to shutil.copyfileobj except that it allows to copy | 
|  | 58 | less than the full source file. | 
|  | 59 |  | 
|  | 60 | Args: | 
|  | 61 | fsrc: source file object where to read from. | 
|  | 62 | fdst: destination file object where to write to. | 
|  | 63 | buffer_size: size of the copy buffer in memory. | 
|  | 64 | copy_length: maximum number of bytes to copy, or None to copy everything. | 
| Kelvin Zhang | 46a860c | 2022-09-28 19:42:39 -0700 | [diff] [blame] | 65 | speed_limit: upper limit for copying speed, in bytes per second. | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 66 |  | 
|  | 67 | Returns: | 
|  | 68 | the number of bytes copied. | 
|  | 69 | """ | 
| Kelvin Zhang | 46a860c | 2022-09-28 19:42:39 -0700 | [diff] [blame] | 70 | # If buffer size significantly bigger than speed limit | 
|  | 71 | # traffic would seem extremely spiky to the client. | 
|  | 72 | if speed_limit: | 
|  | 73 | print(f"Applying speed limit: {speed_limit}") | 
|  | 74 | buffer_size = min(speed_limit//32, buffer_size) | 
|  | 75 |  | 
|  | 76 | start_time = time.time() | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 77 | copied = 0 | 
|  | 78 | while True: | 
|  | 79 | chunk_size = buffer_size | 
|  | 80 | if copy_length is not None: | 
|  | 81 | chunk_size = min(chunk_size, copy_length - copied) | 
|  | 82 | if not chunk_size: | 
|  | 83 | break | 
|  | 84 | buf = fsrc.read(chunk_size) | 
|  | 85 | if not buf: | 
|  | 86 | break | 
| Kelvin Zhang | 46a860c | 2022-09-28 19:42:39 -0700 | [diff] [blame] | 87 | if speed_limit: | 
|  | 88 | expected_duration = copied/speed_limit | 
|  | 89 | actual_duration = time.time() - start_time | 
|  | 90 | if actual_duration < expected_duration: | 
|  | 91 | time.sleep(expected_duration-actual_duration) | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 92 | fdst.write(buf) | 
|  | 93 | copied += len(buf) | 
|  | 94 | return copied | 
|  | 95 |  | 
|  | 96 |  | 
|  | 97 | class AndroidOTAPackage(object): | 
|  | 98 | """Android update payload using the .zip format. | 
|  | 99 |  | 
|  | 100 | Android OTA packages traditionally used a .zip file to store the payload. When | 
|  | 101 | applying A/B updates over the network, a payload binary is stored RAW inside | 
|  | 102 | this .zip file which is used by update_engine to apply the payload. To do | 
|  | 103 | this, an offset and size inside the .zip file are provided. | 
|  | 104 | """ | 
|  | 105 |  | 
|  | 106 | # Android OTA package file paths. | 
|  | 107 | OTA_PAYLOAD_BIN = 'payload.bin' | 
|  | 108 | OTA_PAYLOAD_PROPERTIES_TXT = 'payload_properties.txt' | 
| Tianjie Xu | 3f9be77 | 2019-11-02 18:31:50 -0700 | [diff] [blame] | 109 | SECONDARY_OTA_PAYLOAD_BIN = 'secondary/payload.bin' | 
|  | 110 | SECONDARY_OTA_PAYLOAD_PROPERTIES_TXT = 'secondary/payload_properties.txt' | 
| Kelvin Zhang | aba70ab | 2020-08-04 10:32:59 -0400 | [diff] [blame] | 111 | PAYLOAD_MAGIC_HEADER = b'CrAU' | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 112 |  | 
| Tianjie Xu | 3f9be77 | 2019-11-02 18:31:50 -0700 | [diff] [blame] | 113 | def __init__(self, otafilename, secondary_payload=False): | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 114 | self.otafilename = otafilename | 
|  | 115 |  | 
|  | 116 | otazip = zipfile.ZipFile(otafilename, 'r') | 
| Tianjie Xu | 3f9be77 | 2019-11-02 18:31:50 -0700 | [diff] [blame] | 117 | payload_entry = (self.SECONDARY_OTA_PAYLOAD_BIN if secondary_payload else | 
|  | 118 | self.OTA_PAYLOAD_BIN) | 
|  | 119 | payload_info = otazip.getinfo(payload_entry) | 
| Kelvin Zhang | aba70ab | 2020-08-04 10:32:59 -0400 | [diff] [blame] | 120 |  | 
|  | 121 | if payload_info.compress_type != 0: | 
|  | 122 | logging.error( | 
| Kelvin Zhang | 07676f5 | 2020-12-01 10:45:09 -0500 | [diff] [blame] | 123 | "Expected payload to be uncompressed, got compression method %d", | 
| Kelvin Zhang | aba70ab | 2020-08-04 10:32:59 -0400 | [diff] [blame] | 124 | payload_info.compress_type) | 
|  | 125 | # Don't use len(payload_info.extra). Because that returns size of extra | 
|  | 126 | # fields in central directory. We need to look at local file directory, | 
|  | 127 | # as these two might have different sizes. | 
|  | 128 | with open(otafilename, "rb") as fp: | 
|  | 129 | fp.seek(payload_info.header_offset) | 
|  | 130 | data = fp.read(zipfile.sizeFileHeader) | 
|  | 131 | fheader = struct.unpack(zipfile.structFileHeader, data) | 
|  | 132 | # Last two fields of local file header are filename length and | 
|  | 133 | # extra length | 
|  | 134 | filename_len = fheader[-2] | 
|  | 135 | extra_len = fheader[-1] | 
|  | 136 | self.offset = payload_info.header_offset | 
|  | 137 | self.offset += zipfile.sizeFileHeader | 
|  | 138 | self.offset += filename_len + extra_len | 
|  | 139 | self.size = payload_info.file_size | 
|  | 140 | fp.seek(self.offset) | 
|  | 141 | payload_header = fp.read(4) | 
|  | 142 | if payload_header != self.PAYLOAD_MAGIC_HEADER: | 
|  | 143 | logging.warning( | 
| Kelvin Zhang | 07676f5 | 2020-12-01 10:45:09 -0500 | [diff] [blame] | 144 | "Invalid header, expected %s, got %s." | 
| Kelvin Zhang | aba70ab | 2020-08-04 10:32:59 -0400 | [diff] [blame] | 145 | "Either the offset is not correct, or payload is corrupted", | 
|  | 146 | binascii.hexlify(self.PAYLOAD_MAGIC_HEADER), | 
| Kelvin Zhang | 07676f5 | 2020-12-01 10:45:09 -0500 | [diff] [blame] | 147 | binascii.hexlify(payload_header)) | 
| Tianjie Xu | 3f9be77 | 2019-11-02 18:31:50 -0700 | [diff] [blame] | 148 |  | 
|  | 149 | property_entry = (self.SECONDARY_OTA_PAYLOAD_PROPERTIES_TXT if | 
|  | 150 | secondary_payload else self.OTA_PAYLOAD_PROPERTIES_TXT) | 
|  | 151 | self.properties = otazip.read(property_entry) | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 152 |  | 
|  | 153 |  | 
|  | 154 | class UpdateHandler(BaseHTTPServer.BaseHTTPRequestHandler): | 
|  | 155 | """A HTTPServer that supports single-range requests. | 
|  | 156 |  | 
|  | 157 | Attributes: | 
|  | 158 | serving_payload: path to the only payload file we are serving. | 
| Sen Jiang | 3b15b59 | 2017-09-26 18:21:04 -0700 | [diff] [blame] | 159 | serving_range: the start offset and size tuple of the payload. | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 160 | """ | 
|  | 161 |  | 
|  | 162 | @staticmethod | 
| Sen Jiang | 1048559 | 2017-08-15 18:20:24 -0700 | [diff] [blame] | 163 | def _parse_range(range_str, file_size): | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 164 | """Parse an HTTP range string. | 
|  | 165 |  | 
|  | 166 | Args: | 
|  | 167 | range_str: HTTP Range header in the request, not including "Header:". | 
|  | 168 | file_size: total size of the serving file. | 
|  | 169 |  | 
|  | 170 | Returns: | 
|  | 171 | A tuple (start_range, end_range) with the range of bytes requested. | 
|  | 172 | """ | 
|  | 173 | start_range = 0 | 
|  | 174 | end_range = file_size | 
|  | 175 |  | 
|  | 176 | if range_str: | 
|  | 177 | range_str = range_str.split('=', 1)[1] | 
|  | 178 | s, e = range_str.split('-', 1) | 
|  | 179 | if s: | 
|  | 180 | start_range = int(s) | 
|  | 181 | if e: | 
|  | 182 | end_range = int(e) + 1 | 
|  | 183 | elif e: | 
|  | 184 | if int(e) < file_size: | 
|  | 185 | start_range = file_size - int(e) | 
|  | 186 | return start_range, end_range | 
|  | 187 |  | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 188 | def do_GET(self):  # pylint: disable=invalid-name | 
|  | 189 | """Reply with the requested payload file.""" | 
|  | 190 | if self.path != '/payload': | 
|  | 191 | self.send_error(404, 'Unknown request') | 
|  | 192 | return | 
|  | 193 |  | 
|  | 194 | if not self.serving_payload: | 
|  | 195 | self.send_error(500, 'No serving payload set') | 
|  | 196 | return | 
|  | 197 |  | 
|  | 198 | try: | 
|  | 199 | f = open(self.serving_payload, 'rb') | 
|  | 200 | except IOError: | 
|  | 201 | self.send_error(404, 'File not found') | 
|  | 202 | return | 
|  | 203 | # Handle the range request. | 
|  | 204 | if 'Range' in self.headers: | 
|  | 205 | self.send_response(206) | 
|  | 206 | else: | 
|  | 207 | self.send_response(200) | 
|  | 208 |  | 
| Sen Jiang | 3b15b59 | 2017-09-26 18:21:04 -0700 | [diff] [blame] | 209 | serving_start, serving_size = self.serving_range | 
| Sen Jiang | 1048559 | 2017-08-15 18:20:24 -0700 | [diff] [blame] | 210 | start_range, end_range = self._parse_range(self.headers.get('range'), | 
| Sen Jiang | 3b15b59 | 2017-09-26 18:21:04 -0700 | [diff] [blame] | 211 | serving_size) | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 212 | logging.info('Serving request for %s from %s [%d, %d) length: %d', | 
| Sen Jiang | 3b15b59 | 2017-09-26 18:21:04 -0700 | [diff] [blame] | 213 | self.path, self.serving_payload, serving_start + start_range, | 
|  | 214 | serving_start + end_range, end_range - start_range) | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 215 |  | 
|  | 216 | self.send_header('Accept-Ranges', 'bytes') | 
|  | 217 | self.send_header('Content-Range', | 
|  | 218 | 'bytes ' + str(start_range) + '-' + str(end_range - 1) + | 
|  | 219 | '/' + str(end_range - start_range)) | 
|  | 220 | self.send_header('Content-Length', end_range - start_range) | 
|  | 221 |  | 
| Sen Jiang | 3b15b59 | 2017-09-26 18:21:04 -0700 | [diff] [blame] | 222 | stat = os.fstat(f.fileno()) | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 223 | self.send_header('Last-Modified', self.date_time_string(stat.st_mtime)) | 
|  | 224 | self.send_header('Content-type', 'application/octet-stream') | 
|  | 225 | self.end_headers() | 
|  | 226 |  | 
| Sen Jiang | 3b15b59 | 2017-09-26 18:21:04 -0700 | [diff] [blame] | 227 | f.seek(serving_start + start_range) | 
| Kelvin Zhang | 46a860c | 2022-09-28 19:42:39 -0700 | [diff] [blame] | 228 | CopyFileObjLength(f, self.wfile, copy_length=end_range - | 
|  | 229 | start_range, speed_limit=self.speed_limit) | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 230 |  | 
| Sen Jiang | a1784b7 | 2017-08-09 17:42:36 -0700 | [diff] [blame] | 231 |  | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 232 | class ServerThread(threading.Thread): | 
|  | 233 | """A thread for serving HTTP requests.""" | 
|  | 234 |  | 
| Kelvin Zhang | 46a860c | 2022-09-28 19:42:39 -0700 | [diff] [blame] | 235 | def __init__(self, ota_filename, serving_range, speed_limit): | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 236 | threading.Thread.__init__(self) | 
| Sen Jiang | 3b15b59 | 2017-09-26 18:21:04 -0700 | [diff] [blame] | 237 | # serving_payload and serving_range are class attributes and the | 
|  | 238 | # UpdateHandler class is instantiated with every request. | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 239 | UpdateHandler.serving_payload = ota_filename | 
| Sen Jiang | 3b15b59 | 2017-09-26 18:21:04 -0700 | [diff] [blame] | 240 | UpdateHandler.serving_range = serving_range | 
| Kelvin Zhang | 46a860c | 2022-09-28 19:42:39 -0700 | [diff] [blame] | 241 | UpdateHandler.speed_limit = speed_limit | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 242 | self._httpd = BaseHTTPServer.HTTPServer(('127.0.0.1', 0), UpdateHandler) | 
|  | 243 | self.port = self._httpd.server_port | 
|  | 244 |  | 
|  | 245 | def run(self): | 
|  | 246 | try: | 
|  | 247 | self._httpd.serve_forever() | 
|  | 248 | except (KeyboardInterrupt, socket.error): | 
|  | 249 | pass | 
|  | 250 | logging.info('Server Terminated') | 
|  | 251 |  | 
|  | 252 | def StopServer(self): | 
| Kelvin Zhang | 4b883ea | 2020-10-08 13:26:44 -0400 | [diff] [blame] | 253 | self._httpd.shutdown() | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 254 | self._httpd.socket.close() | 
|  | 255 |  | 
|  | 256 |  | 
| Kelvin Zhang | 46a860c | 2022-09-28 19:42:39 -0700 | [diff] [blame] | 257 | def StartServer(ota_filename, serving_range, speed_limit): | 
|  | 258 | t = ServerThread(ota_filename, serving_range, speed_limit) | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 259 | t.start() | 
|  | 260 | return t | 
|  | 261 |  | 
|  | 262 |  | 
| Tianjie Xu | 3f9be77 | 2019-11-02 18:31:50 -0700 | [diff] [blame] | 263 | def AndroidUpdateCommand(ota_filename, secondary, payload_url, extra_headers): | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 264 | """Return the command to run to start the update in the Android device.""" | 
| Tianjie Xu | 3f9be77 | 2019-11-02 18:31:50 -0700 | [diff] [blame] | 265 | ota = AndroidOTAPackage(ota_filename, secondary) | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 266 | headers = ota.properties | 
| Kelvin Zhang | 4b883ea | 2020-10-08 13:26:44 -0400 | [diff] [blame] | 267 | headers += b'USER_AGENT=Dalvik (something, something)\n' | 
|  | 268 | headers += b'NETWORK_ID=0\n' | 
|  | 269 | headers += extra_headers.encode() | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 270 |  | 
|  | 271 | return ['update_engine_client', '--update', '--follow', | 
|  | 272 | '--payload=%s' % payload_url, '--offset=%d' % ota.offset, | 
| Kelvin Zhang | 4b883ea | 2020-10-08 13:26:44 -0400 | [diff] [blame] | 273 | '--size=%d' % ota.size, '--headers="%s"' % headers.decode()] | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 274 |  | 
|  | 275 |  | 
|  | 276 | class AdbHost(object): | 
|  | 277 | """Represents a device connected via ADB.""" | 
|  | 278 |  | 
|  | 279 | def __init__(self, device_serial=None): | 
|  | 280 | """Construct an instance. | 
|  | 281 |  | 
|  | 282 | Args: | 
|  | 283 | device_serial: options string serial number of attached device. | 
|  | 284 | """ | 
|  | 285 | self._device_serial = device_serial | 
|  | 286 | self._command_prefix = ['adb'] | 
|  | 287 | if self._device_serial: | 
|  | 288 | self._command_prefix += ['-s', self._device_serial] | 
|  | 289 |  | 
| Kelvin Zhang | 3a18895 | 2021-04-13 12:44:45 -0400 | [diff] [blame] | 290 | def adb(self, command, timeout_seconds: float = None): | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 291 | """Run an ADB command like "adb push". | 
|  | 292 |  | 
|  | 293 | Args: | 
|  | 294 | command: list of strings containing command and arguments to run | 
|  | 295 |  | 
|  | 296 | Returns: | 
|  | 297 | the program's return code. | 
|  | 298 |  | 
|  | 299 | Raises: | 
|  | 300 | subprocess.CalledProcessError on command exit != 0. | 
|  | 301 | """ | 
|  | 302 | command = self._command_prefix + command | 
|  | 303 | logging.info('Running: %s', ' '.join(str(x) for x in command)) | 
|  | 304 | p = subprocess.Popen(command, universal_newlines=True) | 
| Kelvin Zhang | 3a18895 | 2021-04-13 12:44:45 -0400 | [diff] [blame] | 305 | p.wait(timeout_seconds) | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 306 | return p.returncode | 
|  | 307 |  | 
| Sen Jiang | a1784b7 | 2017-08-09 17:42:36 -0700 | [diff] [blame] | 308 | def adb_output(self, command): | 
|  | 309 | """Run an ADB command like "adb push" and return the output. | 
|  | 310 |  | 
|  | 311 | Args: | 
|  | 312 | command: list of strings containing command and arguments to run | 
|  | 313 |  | 
|  | 314 | Returns: | 
|  | 315 | the program's output as a string. | 
|  | 316 |  | 
|  | 317 | Raises: | 
|  | 318 | subprocess.CalledProcessError on command exit != 0. | 
|  | 319 | """ | 
|  | 320 | command = self._command_prefix + command | 
|  | 321 | logging.info('Running: %s', ' '.join(str(x) for x in command)) | 
|  | 322 | return subprocess.check_output(command, universal_newlines=True) | 
|  | 323 |  | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 324 |  | 
| Kelvin Zhang | 63b3911 | 2021-03-05 12:31:38 -0500 | [diff] [blame] | 325 | def PushMetadata(dut, otafile, metadata_path): | 
|  | 326 | payload = update_payload.Payload(otafile) | 
|  | 327 | payload.Init() | 
|  | 328 | with tempfile.TemporaryDirectory() as tmpdir: | 
|  | 329 | with zipfile.ZipFile(otafile, "r") as zfp: | 
|  | 330 | extracted_path = os.path.join(tmpdir, "payload.bin") | 
|  | 331 | with zfp.open("payload.bin") as payload_fp, \ | 
|  | 332 | open(extracted_path, "wb") as output_fp: | 
|  | 333 | # Only extract the first |data_offset| bytes from the payload. | 
|  | 334 | # This is because allocateSpaceForPayload only needs to see | 
|  | 335 | # the manifest, not the entire payload. | 
|  | 336 | # Extracting the entire payload works, but is slow for full | 
|  | 337 | # OTA. | 
|  | 338 | output_fp.write(payload_fp.read(payload.data_offset)) | 
|  | 339 |  | 
|  | 340 | return dut.adb([ | 
|  | 341 | "push", | 
|  | 342 | extracted_path, | 
|  | 343 | metadata_path | 
|  | 344 | ]) == 0 | 
|  | 345 |  | 
|  | 346 |  | 
| Kelvin Zhang | 46a860c | 2022-09-28 19:42:39 -0700 | [diff] [blame] | 347 | def ParseSpeedLimit(arg: str) -> int: | 
|  | 348 | arg = arg.strip().upper() | 
|  | 349 | if not re.match(r"\d+[KkMmGgTt]?", arg): | 
|  | 350 | raise argparse.ArgumentError( | 
|  | 351 | "Wrong speed limit format, expected format is number followed by unit, such as 10K, 5m, 3G (case insensitive)") | 
|  | 352 | unit = 1 | 
|  | 353 | if arg[-1].isalpha(): | 
|  | 354 | if arg[-1] == "K": | 
|  | 355 | unit = 1024 | 
|  | 356 | elif arg[-1] == "M": | 
|  | 357 | unit = 1024 * 1024 | 
|  | 358 | elif arg[-1] == "G": | 
|  | 359 | unit = 1024 * 1024 * 1024 | 
|  | 360 | elif arg[-1] == "T": | 
|  | 361 | unit = 1024 * 1024 * 1024 * 1024 | 
|  | 362 | else: | 
|  | 363 | raise argparse.ArgumentError( | 
|  | 364 | f"Unsupported unit for download speed: {arg[-1]}, supported units are K,M,G,T (case insensitive)") | 
|  | 365 | return int(float(arg[:-1]) * unit) | 
|  | 366 |  | 
|  | 367 |  | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 368 | def main(): | 
|  | 369 | parser = argparse.ArgumentParser(description='Android A/B OTA helper.') | 
| Sen Jiang | 3b15b59 | 2017-09-26 18:21:04 -0700 | [diff] [blame] | 370 | parser.add_argument('otafile', metavar='PAYLOAD', type=str, | 
|  | 371 | help='the OTA package file (a .zip file) or raw payload \ | 
|  | 372 | if device uses Omaha.') | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 373 | parser.add_argument('--file', action='store_true', | 
|  | 374 | help='Push the file to the device before updating.') | 
|  | 375 | parser.add_argument('--no-push', action='store_true', | 
|  | 376 | help='Skip the "push" command when using --file') | 
|  | 377 | parser.add_argument('-s', type=str, default='', metavar='DEVICE', | 
|  | 378 | help='The specific device to use.') | 
|  | 379 | parser.add_argument('--no-verbose', action='store_true', | 
|  | 380 | help='Less verbose output') | 
| Sen Jiang | a1784b7 | 2017-08-09 17:42:36 -0700 | [diff] [blame] | 381 | parser.add_argument('--public-key', type=str, default='', | 
|  | 382 | help='Override the public key used to verify payload.') | 
| Sen Jiang | 6fbfd7d | 2017-10-31 16:16:56 -0700 | [diff] [blame] | 383 | parser.add_argument('--extra-headers', type=str, default='', | 
|  | 384 | help='Extra headers to pass to the device.') | 
| Tianjie Xu | 3f9be77 | 2019-11-02 18:31:50 -0700 | [diff] [blame] | 385 | parser.add_argument('--secondary', action='store_true', | 
|  | 386 | help='Update with the secondary payload in the package.') | 
| Kelvin Zhang | 8212f53 | 2020-11-13 16:00:00 -0500 | [diff] [blame] | 387 | parser.add_argument('--no-slot-switch', action='store_true', | 
|  | 388 | help='Do not perform slot switch after the update.') | 
| Kelvin Zhang | bec0f07 | 2021-03-31 16:09:00 -0400 | [diff] [blame] | 389 | parser.add_argument('--no-postinstall', action='store_true', | 
|  | 390 | help='Do not execute postinstall scripts after the update.') | 
| Kelvin Zhang | ffd2144 | 2021-04-14 09:09:41 -0400 | [diff] [blame] | 391 | parser.add_argument('--allocate-only', action='store_true', | 
| Kelvin Zhang | 51aad99 | 2021-02-19 14:46:28 -0500 | [diff] [blame] | 392 | help='Allocate space for this OTA, instead of actually \ | 
|  | 393 | applying the OTA.') | 
| Kelvin Zhang | ffd2144 | 2021-04-14 09:09:41 -0400 | [diff] [blame] | 394 | parser.add_argument('--verify-only', action='store_true', | 
| Kelvin Zhang | 63b3911 | 2021-03-05 12:31:38 -0500 | [diff] [blame] | 395 | help='Verify metadata then exit, instead of applying the OTA.') | 
| Kelvin Zhang | ffd2144 | 2021-04-14 09:09:41 -0400 | [diff] [blame] | 396 | parser.add_argument('--no-care-map', action='store_true', | 
| Kelvin Zhang | 5bd4622 | 2021-03-02 12:36:14 -0500 | [diff] [blame] | 397 | help='Do not push care_map.pb to device.') | 
| Kelvin Zhang | c56afa3 | 2021-08-13 12:32:31 -0700 | [diff] [blame] | 398 | parser.add_argument('--perform-slot-switch', action='store_true', | 
|  | 399 | help='Perform slot switch for this OTA package') | 
|  | 400 | parser.add_argument('--perform-reset-slot-switch', action='store_true', | 
|  | 401 | help='Perform reset slot switch for this OTA package') | 
| Kelvin Zhang | 2451a30 | 2022-03-23 19:18:35 -0700 | [diff] [blame] | 402 | parser.add_argument('--wipe-user-data', action='store_true', | 
|  | 403 | help='Wipe userdata after installing OTA') | 
| Kelvin Zhang | a7407b5 | 2023-03-13 15:05:14 -0700 | [diff] [blame] | 404 | parser.add_argument('--vabc-none', action='store_true', | 
|  | 405 | help='Set Virtual AB Compression algorithm to none, but still use Android COW format') | 
| Daniel Zheng | 9fc62b8 | 2023-03-24 22:57:20 +0000 | [diff] [blame] | 406 | parser.add_argument('--disable-vabc', action='store_true', | 
|  | 407 | help='Option to enable or disable vabc. If set to false, will fall back on A/B') | 
| Kelvin Zhang | 6bef490 | 2023-02-22 12:43:27 -0800 | [diff] [blame] | 408 | parser.add_argument('--enable-threading', action='store_true', | 
|  | 409 | help='Enable multi-threaded compression for VABC') | 
|  | 410 | parser.add_argument('--batched-writes', action='store_true', | 
|  | 411 | help='Enable batched writes for VABC') | 
| Kelvin Zhang | 46a860c | 2022-09-28 19:42:39 -0700 | [diff] [blame] | 412 | parser.add_argument('--speed-limit', type=str, | 
|  | 413 | help='Speed limit for serving payloads over HTTP. For ' | 
|  | 414 | 'example: 10K, 5m, 1G, input is case insensitive') | 
|  | 415 |  | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 416 | args = parser.parse_args() | 
| Kelvin Zhang | 46a860c | 2022-09-28 19:42:39 -0700 | [diff] [blame] | 417 | if args.speed_limit: | 
|  | 418 | args.speed_limit = ParseSpeedLimit(args.speed_limit) | 
|  | 419 |  | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 420 | logging.basicConfig( | 
|  | 421 | level=logging.WARNING if args.no_verbose else logging.INFO) | 
|  | 422 |  | 
| Nikita Ioffe | 3a327e6 | 2021-06-30 16:05:09 +0100 | [diff] [blame] | 423 | start_time = time.perf_counter() | 
|  | 424 |  | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 425 | dut = AdbHost(args.s) | 
|  | 426 |  | 
|  | 427 | server_thread = None | 
|  | 428 | # List of commands to execute on exit. | 
|  | 429 | finalize_cmds = [] | 
|  | 430 | # Commands to execute when canceling an update. | 
|  | 431 | cancel_cmd = ['shell', 'su', '0', 'update_engine_client', '--cancel'] | 
|  | 432 | # List of commands to perform the update. | 
|  | 433 | cmds = [] | 
|  | 434 |  | 
| Sen Jiang | a1784b7 | 2017-08-09 17:42:36 -0700 | [diff] [blame] | 435 | help_cmd = ['shell', 'su', '0', 'update_engine_client', '--help'] | 
| Sen Jiang | a1784b7 | 2017-08-09 17:42:36 -0700 | [diff] [blame] | 436 |  | 
| Kelvin Zhang | 63b3911 | 2021-03-05 12:31:38 -0500 | [diff] [blame] | 437 | metadata_path = "/data/ota_package/metadata" | 
| Kelvin Zhang | 51aad99 | 2021-02-19 14:46:28 -0500 | [diff] [blame] | 438 | if args.allocate_only: | 
| Kelvin Zhang | 027eb38 | 2023-04-27 20:44:45 -0700 | [diff] [blame] | 439 | with zipfile.ZipFile(args.otafile, "r") as zfp: | 
|  | 440 | headers = zfp.read("payload_properties.txt").decode() | 
| Kelvin Zhang | 63b3911 | 2021-03-05 12:31:38 -0500 | [diff] [blame] | 441 | if PushMetadata(dut, args.otafile, metadata_path): | 
|  | 442 | dut.adb([ | 
|  | 443 | "shell", "update_engine_client", "--allocate", | 
| Kelvin Zhang | 027eb38 | 2023-04-27 20:44:45 -0700 | [diff] [blame] | 444 | "--metadata={} --headers='{}'".format(metadata_path, headers)]) | 
| Kelvin Zhang | 63b3911 | 2021-03-05 12:31:38 -0500 | [diff] [blame] | 445 | # Return 0, as we are executing ADB commands here, no work needed after | 
|  | 446 | # this point | 
|  | 447 | return 0 | 
|  | 448 | if args.verify_only: | 
|  | 449 | if PushMetadata(dut, args.otafile, metadata_path): | 
|  | 450 | dut.adb([ | 
|  | 451 | "shell", "update_engine_client", "--verify", | 
|  | 452 | "--metadata={}".format(metadata_path)]) | 
| Kelvin Zhang | 51aad99 | 2021-02-19 14:46:28 -0500 | [diff] [blame] | 453 | # Return 0, as we are executing ADB commands here, no work needed after | 
|  | 454 | # this point | 
|  | 455 | return 0 | 
| Kelvin Zhang | c56afa3 | 2021-08-13 12:32:31 -0700 | [diff] [blame] | 456 | if args.perform_slot_switch: | 
|  | 457 | assert PushMetadata(dut, args.otafile, metadata_path) | 
|  | 458 | dut.adb(["shell", "update_engine_client", | 
|  | 459 | "--switch_slot=true", "--metadata={}".format(metadata_path), "--follow"]) | 
|  | 460 | return 0 | 
|  | 461 | if args.perform_reset_slot_switch: | 
|  | 462 | assert PushMetadata(dut, args.otafile, metadata_path) | 
|  | 463 | dut.adb(["shell", "update_engine_client", | 
|  | 464 | "--switch_slot=false", "--metadata={}".format(metadata_path)]) | 
|  | 465 | return 0 | 
| Kelvin Zhang | 51aad99 | 2021-02-19 14:46:28 -0500 | [diff] [blame] | 466 |  | 
| Kelvin Zhang | 8212f53 | 2020-11-13 16:00:00 -0500 | [diff] [blame] | 467 | if args.no_slot_switch: | 
|  | 468 | args.extra_headers += "\nSWITCH_SLOT_ON_REBOOT=0" | 
| Kelvin Zhang | bec0f07 | 2021-03-31 16:09:00 -0400 | [diff] [blame] | 469 | if args.no_postinstall: | 
|  | 470 | args.extra_headers += "\nRUN_POST_INSTALL=0" | 
| Kelvin Zhang | 2451a30 | 2022-03-23 19:18:35 -0700 | [diff] [blame] | 471 | if args.wipe_user_data: | 
|  | 472 | args.extra_headers += "\nPOWERWASH=1" | 
| Kelvin Zhang | a7407b5 | 2023-03-13 15:05:14 -0700 | [diff] [blame] | 473 | if args.vabc_none: | 
|  | 474 | args.extra_headers += "\nVABC_NONE=1" | 
| Daniel Zheng | 9fc62b8 | 2023-03-24 22:57:20 +0000 | [diff] [blame] | 475 | if args.disable_vabc: | 
|  | 476 | args.extra_headers += "\nDISABLE_VABC=1" | 
| Kelvin Zhang | 6bef490 | 2023-02-22 12:43:27 -0800 | [diff] [blame] | 477 | if args.enable_threading: | 
|  | 478 | args.extra_headers += "\nENABLE_THREADING=1" | 
|  | 479 | if args.batched_writes: | 
|  | 480 | args.extra_headers += "\nBATCHED_WRITES=1" | 
| Kelvin Zhang | 8212f53 | 2020-11-13 16:00:00 -0500 | [diff] [blame] | 481 |  | 
| Kelvin Zhang | 5bd4622 | 2021-03-02 12:36:14 -0500 | [diff] [blame] | 482 | with zipfile.ZipFile(args.otafile) as zfp: | 
|  | 483 | CARE_MAP_ENTRY_NAME = "care_map.pb" | 
|  | 484 | if CARE_MAP_ENTRY_NAME in zfp.namelist() and not args.no_care_map: | 
|  | 485 | # Need root permission to push to /data | 
|  | 486 | dut.adb(["root"]) | 
| Kelvin Zhang | 472d561 | 2021-03-05 12:32:19 -0500 | [diff] [blame] | 487 | with tempfile.NamedTemporaryFile() as care_map_fp: | 
| Kelvin Zhang | 5bd4622 | 2021-03-02 12:36:14 -0500 | [diff] [blame] | 488 | care_map_fp.write(zfp.read(CARE_MAP_ENTRY_NAME)) | 
|  | 489 | care_map_fp.flush() | 
|  | 490 | dut.adb(["push", care_map_fp.name, | 
| Kelvin Zhang | ffd2144 | 2021-04-14 09:09:41 -0400 | [diff] [blame] | 491 | "/data/ota_package/" + CARE_MAP_ENTRY_NAME]) | 
| Kelvin Zhang | 5bd4622 | 2021-03-02 12:36:14 -0500 | [diff] [blame] | 492 |  | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 493 | if args.file: | 
|  | 494 | # Update via pushing a file to /data. | 
|  | 495 | device_ota_file = os.path.join(OTA_PACKAGE_PATH, 'debug.zip') | 
|  | 496 | payload_url = 'file://' + device_ota_file | 
|  | 497 | if not args.no_push: | 
| Tao Bao | abb45a5 | 2017-10-25 11:13:03 -0700 | [diff] [blame] | 498 | data_local_tmp_file = '/data/local/tmp/debug.zip' | 
|  | 499 | cmds.append(['push', args.otafile, data_local_tmp_file]) | 
|  | 500 | cmds.append(['shell', 'su', '0', 'mv', data_local_tmp_file, | 
|  | 501 | device_ota_file]) | 
|  | 502 | cmds.append(['shell', 'su', '0', 'chcon', | 
|  | 503 | 'u:object_r:ota_package_file:s0', device_ota_file]) | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 504 | cmds.append(['shell', 'su', '0', 'chown', 'system:cache', device_ota_file]) | 
|  | 505 | cmds.append(['shell', 'su', '0', 'chmod', '0660', device_ota_file]) | 
|  | 506 | else: | 
|  | 507 | # Update via sending the payload over the network with an "adb reverse" | 
|  | 508 | # command. | 
| Sen Jiang | a1784b7 | 2017-08-09 17:42:36 -0700 | [diff] [blame] | 509 | payload_url = 'http://127.0.0.1:%d/payload' % DEVICE_PORT | 
| Kelvin Zhang | 033e173 | 2023-09-11 16:32:11 -0700 | [diff] [blame] | 510 | serving_range = (0, os.stat(args.otafile).st_size) | 
| Kelvin Zhang | 46a860c | 2022-09-28 19:42:39 -0700 | [diff] [blame] | 511 | server_thread = StartServer(args.otafile, serving_range, args.speed_limit) | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 512 | cmds.append( | 
| Sen Jiang | a1784b7 | 2017-08-09 17:42:36 -0700 | [diff] [blame] | 513 | ['reverse', 'tcp:%d' % DEVICE_PORT, 'tcp:%d' % server_thread.port]) | 
|  | 514 | finalize_cmds.append(['reverse', '--remove', 'tcp:%d' % DEVICE_PORT]) | 
|  | 515 |  | 
|  | 516 | if args.public_key: | 
|  | 517 | payload_key_dir = os.path.dirname(PAYLOAD_KEY_PATH) | 
|  | 518 | cmds.append( | 
|  | 519 | ['shell', 'su', '0', 'mount', '-t', 'tmpfs', 'tmpfs', payload_key_dir]) | 
|  | 520 | # Allow adb push to payload_key_dir | 
|  | 521 | cmds.append(['shell', 'su', '0', 'chcon', 'u:object_r:shell_data_file:s0', | 
|  | 522 | payload_key_dir]) | 
|  | 523 | cmds.append(['push', args.public_key, PAYLOAD_KEY_PATH]) | 
|  | 524 | # Allow update_engine to read it. | 
|  | 525 | cmds.append(['shell', 'su', '0', 'chcon', '-R', 'u:object_r:system_file:s0', | 
|  | 526 | payload_key_dir]) | 
|  | 527 | finalize_cmds.append(['shell', 'su', '0', 'umount', payload_key_dir]) | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 528 |  | 
|  | 529 | try: | 
|  | 530 | # The main update command using the configured payload_url. | 
| Kelvin Zhang | 033e173 | 2023-09-11 16:32:11 -0700 | [diff] [blame] | 531 | update_cmd = AndroidUpdateCommand(args.otafile, args.secondary, | 
| Tianjie Xu | 3f9be77 | 2019-11-02 18:31:50 -0700 | [diff] [blame] | 532 | payload_url, args.extra_headers) | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 533 | cmds.append(['shell', 'su', '0'] + update_cmd) | 
|  | 534 |  | 
|  | 535 | for cmd in cmds: | 
|  | 536 | dut.adb(cmd) | 
|  | 537 | except KeyboardInterrupt: | 
|  | 538 | dut.adb(cancel_cmd) | 
|  | 539 | finally: | 
|  | 540 | if server_thread: | 
|  | 541 | server_thread.StopServer() | 
|  | 542 | for cmd in finalize_cmds: | 
| Kelvin Zhang | 3a18895 | 2021-04-13 12:44:45 -0400 | [diff] [blame] | 543 | dut.adb(cmd, 5) | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 544 |  | 
| Nikita Ioffe | 3a327e6 | 2021-06-30 16:05:09 +0100 | [diff] [blame] | 545 | logging.info('Update took %.3f seconds', (time.perf_counter() - start_time)) | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 546 | return 0 | 
|  | 547 |  | 
| Andrew Lassalle | 165843c | 2019-11-05 13:30:34 -0800 | [diff] [blame] | 548 |  | 
| Alex Deymo | 6751bbe | 2017-03-21 11:20:02 -0700 | [diff] [blame] | 549 | if __name__ == '__main__': | 
|  | 550 | sys.exit(main()) |