1#!/usr/bin/env python3 2# Copyright 2023 The Chromium Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5"""Helpers to reliably reboot the device via serial and fastboot. 6 7Note, this file will be executed in docker instance without vpython3, so we use 8python3 instead. 9""" 10 11import json 12import logging 13import os 14import shutil 15import subprocess 16import sys 17import time 18 19from typing import List 20from boot_device import BootMode 21 22# pylint: disable=too-many-return-statements, too-many-branches 23 24 25def boot_device(node_id: str, 26 serial_num: str, 27 mode: BootMode, 28 must_boot: bool = False) -> bool: 29 """Boots device into desired mode via serial and fastboot. 30 This function waits for at most 10 minutes for the transition. 31 32 Args: 33 node_id: The fuchsia node id of the device. 34 serial_num: The fastboot serial number of the device. 35 mode: Desired boot mode. 36 must_boot: Forces device to reboot regardless the current state. 37 38 Returns: 39 a boolean value to indicate if the operation succeeded; missing 40 dependencies like serialio (for serial access) and fastboot, or the 41 device cannot be found may also introduce the error. 42 """ 43 #TODO(crbug.com/1490434): Remove the default values once the use in 44 # flash_device has been migrated. 45 if node_id is None: 46 node_id = os.getenv('FUCHSIA_NODENAME') 47 if serial_num is None: 48 serial_num = os.getenv('FUCHSIA_FASTBOOT_SERNUM') 49 assert node_id is not None 50 assert serial_num is not None 51 52 if not mode in [BootMode.REGULAR, BootMode.BOOTLOADER]: 53 logging.warning('Unsupported BootMode %s for serial_boot_device.', 54 mode) 55 return False 56 if shutil.which('fastboot') is None: 57 logging.warning('fastboot is not accessible') 58 return False 59 if shutil.which('serialio') is None: 60 logging.warning('serialio is not accessible') 61 return False 62 63 if is_in_fuchsia(node_id): 64 if not must_boot and mode == BootMode.REGULAR: 65 return True 66 # pylint: disable=subprocess-run-check 67 if subprocess.run([ 68 'serialio', node_id, 'send', 'dm', 'reboot' + 69 ('' if mode == BootMode.REGULAR else '-bootloader') 70 ]).returncode != 0: 71 logging.error('Failed to send dm reboot[-bootloader] via serialio') 72 return False 73 elif is_in_fastboot(serial_num): 74 # fastboot is stateless and there isn't a reason to reboot the device 75 # again to go to the fastboot. 76 if mode == BootMode.BOOTLOADER: 77 return True 78 if not _run_fastboot(['reboot'], serial_num): 79 # Shouldn't return None here, unless the device was rebooting. In 80 # the case, it would be safer to return false. 81 return False 82 else: 83 logging.error('Cannot find node id %s or fastboot serial number %s', 84 node_id, serial_num) 85 return False 86 87 start_sec = time.time() 88 while time.time() - start_sec < 600: 89 assert mode in [BootMode.REGULAR, BootMode.BOOTLOADER] 90 if mode == BootMode.REGULAR and is_in_fuchsia(node_id): 91 return True 92 if mode == BootMode.BOOTLOADER and is_in_fastboot(serial_num): 93 return True 94 logging.error( 95 'Failed to transite node id %s or fastboot serial number %s ' 96 'to expected state %s', node_id, serial_num, mode) 97 return False 98 99 100def _serialio_send_and_wait(node_id: str, command: List[str], 101 waitfor: str) -> bool: 102 """Continously sends the command to the device and waits for the waitfor 103 string via serialio. 104 This function asserts the existence of serialio and waits at most ~30 105 seconds.""" 106 assert shutil.which('serialio') is not None 107 start_sec = time.time() 108 with subprocess.Popen(['serialio', node_id, 'wait', waitfor], 109 stdout=subprocess.DEVNULL, 110 stderr=subprocess.DEVNULL) as proc: 111 while time.time() - start_sec < 28: 112 send_command = ['serialio', node_id, 'send'] 113 send_command.extend(command) 114 # pylint: disable=subprocess-run-check 115 if subprocess.run(send_command).returncode != 0: 116 logging.error('Failed to send %s via serialio to %s', command, 117 node_id) 118 return False 119 result = proc.poll() 120 if result is not None: 121 if result == 0: 122 return True 123 logging.error( 124 'Failed to wait %s via serial to %s, ' 125 'return code %s', waitfor, node_id, result) 126 return False 127 time.sleep(2) 128 proc.kill() 129 logging.error('Have not found %s via serialio to %s', waitfor, node_id) 130 return False 131 132 133def is_in_fuchsia(node_id: str) -> bool: 134 """Checks if the device is running in fuchsia through serial. 135 Note, this check goes through serial and does not guarantee the fuchsia os 136 has a workable network or ssh connection. 137 This function asserts the existence of serialio and waits at most ~60 138 seconds.""" 139 if not _serialio_send_and_wait( 140 node_id, ['echo', 'yes-i-am-healthy', '|', 'sha1sum'], 141 '89d517b7db104aada669a83bc3c3a906e00671f7'): 142 logging.error( 143 'Device %s did not respond echo, ' 144 'it may not be running fuchsia', node_id) 145 return False 146 if not _serialio_send_and_wait(node_id, ['ps'], 'sshd'): 147 logging.warning( 148 'Cannot find sshd from ps on %s, the ssh ' 149 'connection may not be available.', node_id) 150 return True 151 152 153def is_in_fastboot(serial_num: str) -> bool: 154 """Checks if the device is running in fastboot through fastboot command. 155 Note, the fastboot may be impacted by the usb congestion and causes this 156 function to return false. 157 This function asserts the existence of fastboot and waits at most ~30 158 seconds.""" 159 start_sec = time.time() 160 while time.time() - start_sec < 28: 161 result = _run_fastboot(['getvar', 'product'], serial_num) 162 if result is None: 163 return False 164 if result: 165 return True 166 time.sleep(2) 167 logging.error('Failed to wait for fastboot state of %s', serial_num) 168 return False 169 170 171def _run_fastboot(args: List[str], serial_num: str) -> bool: 172 """Executes the fastboot command and kills the hanging process. 173 The fastboot may be impacted by the usb congestion and causes the process to 174 hang forever. So this command waits for 30 seconds before killing the 175 process, and it's not good for flashing. 176 Note, if this function detects the fastboot is waiting for the device, i.e. 177 the device is not in the fastboot, it returns None instead, e.g. unknown. 178 This function asserts the existence of fastboot.""" 179 assert shutil.which('fastboot') is not None 180 args.insert(0, 'fastboot') 181 args.extend(('-s', serial_num)) 182 try: 183 # Capture output to ensure we can get '< waiting for serial-num >' 184 # output. 185 # pylint: disable=subprocess-run-check 186 if subprocess.run(args, capture_output=True, 187 timeout=30).returncode == 0: 188 return True 189 except subprocess.TimeoutExpired as timeout: 190 if timeout.stderr is not None and serial_num.lower( 191 ) in timeout.stderr.decode().lower(): 192 logging.warning('fastboot is still waiting for %s', serial_num) 193 return None 194 logging.error('Failed to run %s against fastboot %s', args, serial_num) 195 return False 196 197 198def main(action: str) -> int: 199 """Main entry of serial_boot_device.""" 200 node_id = os.getenv('FUCHSIA_NODENAME') 201 serial_num = os.getenv('FUCHSIA_FASTBOOT_SERNUM') 202 assert node_id is not None 203 assert serial_num is not None 204 if action == 'health-check': 205 if is_in_fuchsia(node_id) or is_in_fastboot(serial_num): 206 # Print out the json result without using logging to avoid any 207 # potential formatting issue. 208 print( 209 json.dumps([{ 210 'nodename': node_id, 211 'state': 'healthy', 212 'status_message': '', 213 'dms_state': '' 214 }])) 215 return 0 216 logging.error('Cannot find node id %s or fastboot serial number %s', 217 node_id, serial_num) 218 return 1 219 if action in ['reboot', 'after-task']: 220 if boot_device(node_id, serial_num, BootMode.REGULAR, must_boot=True): 221 return 0 222 logging.error( 223 'Cannot reboot the device with node id %s and fastboot ' 224 'serial number %s', node_id, serial_num) 225 return 1 226 if action == 'reboot-fastboot': 227 if boot_device(node_id, 228 serial_num, 229 BootMode.BOOTLOADER, 230 must_boot=True): 231 return 0 232 logging.error( 233 'Cannot reboot the device with node id %s and fastboot ' 234 'serial number %s into fastboot', node_id, serial_num) 235 return 1 236 if action == 'is-in-fuchsia': 237 if is_in_fuchsia(node_id): 238 return 0 239 logging.error('Cannot find node id %s', node_id) 240 return 1 241 if action == 'is-in-fastboot': 242 if is_in_fastboot(serial_num): 243 return 0 244 logging.error('Cannot find fastboot serial number %s', serial_num) 245 return 1 246 if action == 'server-version': 247 # TODO(crbug.com/1490434): Implement the server-version. 248 print('chromium') 249 return 0 250 if action == 'before-task': 251 # Do nothing 252 return 0 253 if action == 'set-power-state': 254 # Do nothing 255 return 0 256 logging.error('Unknown command %s', action) 257 return 2 258 259 260if __name__ == '__main__': 261 sys.exit(main(sys.argv[1])) 262