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