• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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