From a9f18ce67bfce9ef18091180ea7921b854157dd4 Mon Sep 17 00:00:00 2001 From: Alec Delaney Date: Mon, 18 Mar 2024 00:37:18 -0400 Subject: [PATCH] Add timeout option to install and update commands --- circfirm/cli/__init__.py | 17 +++++++++++++++-- circfirm/cli/install.py | 15 +++++++++++++-- circfirm/cli/update.py | 16 ++++++++++++++-- docs/commands/install.rst | 8 ++++++++ docs/commands/update.rst | 8 ++++++++ tests/cli/test_cli_install.py | 35 +++++++++++++++++++++++++++++++++++ tests/cli/test_cli_update.py | 15 +++++++++++++++ 7 files changed, 108 insertions(+), 6 deletions(-) diff --git a/circfirm/cli/__init__.py b/circfirm/cli/__init__.py index dc9fae0..1f2708a 100644 --- a/circfirm/cli/__init__.py +++ b/circfirm/cli/__init__.py @@ -43,7 +43,10 @@ def maybe_support(msg: str) -> None: def get_board_id( - circuitpy: Optional[str], bootloader: Optional[str], board: Optional[str] + circuitpy: Optional[str], + bootloader: Optional[str], + board: Optional[str], + timeout: int = -1, ) -> Tuple[str, str]: """Get the board ID of a device via CLI.""" if not board: @@ -56,8 +59,18 @@ def get_board_id( board = circfirm.backend.device.get_board_info(circuitpy)[0] click.echo("Board ID detected, please switch the device to bootloader mode.") + if timeout == -1: + skip_timeout = True + else: + skip_timeout = False + start_time = time.time() + while not (bootloader := circfirm.backend.device.find_bootloader()): - time.sleep(1) + if not skip_timeout and time.time() >= start_time + timeout: + raise OSError( + "Bootloader mode device not found within the timeout period" + ) + time.sleep(0.05) return bootloader, board diff --git a/circfirm/cli/install.py b/circfirm/cli/install.py index 4a307cc..26483d4 100644 --- a/circfirm/cli/install.py +++ b/circfirm/cli/install.py @@ -23,10 +23,21 @@ default=None, help="Assume the given board ID (and connect in bootloader mode)", ) -def cli(version: str, language: str, board_id: Optional[str]) -> None: +@click.option( + "-t", + "--timeout", + default=-1, + help="Set a timeout in seconds for the switch to bootloader mode", +) +def cli(version: str, language: str, board_id: Optional[str], timeout: int) -> None: """Install the specified version of CircuitPython.""" circuitpy, bootloader = circfirm.cli.get_connection_status() - bootloader, board_id = circfirm.cli.get_board_id(circuitpy, bootloader, board_id) + try: + bootloader, board_id = circfirm.cli.get_board_id( + circuitpy, bootloader, board_id, timeout + ) + except OSError as err: + raise click.ClickException(err.args[0]) circfirm.cli.ensure_bootloader_mode(bootloader) circfirm.cli.download_if_needed(board_id, version, language) circfirm.cli.copy_cache_firmware(board_id, version, language, bootloader) diff --git a/circfirm/cli/update.py b/circfirm/cli/update.py index 87b6f48..6350ea8 100644 --- a/circfirm/cli/update.py +++ b/circfirm/cli/update.py @@ -25,6 +25,12 @@ help="Assume the given board ID (and connect in bootloader mode)", ) @click.option("-l", "--language", default="en_US", help="CircuitPython langauge/locale") +@click.option( + "-t", + "--timeout", + default=-1, + help="Set a timeout in seconds for the switch to bootloader mode", +) @click.option( "-p", "--pre-release", @@ -46,9 +52,10 @@ default=False, help="Upgrade up to patch version updates", ) -def cli( +def cli( # noqa: PLR0913 board_id: Optional[str], language: str, + timeout: int, pre_release: bool, limit_to_minor: bool, limit_to_patch: bool, @@ -65,7 +72,12 @@ def cli( "The latest version will be installed regardless of the currently installed version." ) current_version = "0.0.0" - bootloader, board_id = circfirm.cli.get_board_id(circuitpy, bootloader, board_id) + try: + bootloader, board_id = circfirm.cli.get_board_id( + circuitpy, bootloader, board_id, timeout + ) + except OSError as err: + raise click.ClickException(err.args[0]) new_versions = circfirm.backend.s3.get_board_versions(board_id, language) diff --git a/docs/commands/install.rst b/docs/commands/install.rst index ba4d89a..ec757f4 100644 --- a/docs/commands/install.rst +++ b/docs/commands/install.rst @@ -19,6 +19,10 @@ bootloader mode, you can do so and simply use the ``--board-id`` option to provi You can specify a language using the ``--language`` option - the default is US English. +If you would like to specify a timeout for how long the CLI will wait for a device in bootloader +mode in secounds (e.g., for scripting), you can use the ``--timeout`` option. The default behavior +is that it will wait indefinitely (``-1`` secounds). + .. code-block:: shell # Install CircuitPython 8.0.0 on the connected board @@ -29,3 +33,7 @@ You can specify a language using the ``--language`` option - the default is US E # Install CircuitPython 8.0.0 on the connected Adafruit QT Py ESP32 Pico (in bootloader mode) circfirm install 8.0.0 --board-id adafruit_qtpy_esp32_pico + + # Install CircuitPython 8.0.0 but only wait up to 30 seconds for the device to change from + # bootloader mode + circfirm install 8.0.0 --timeout 30 diff --git a/docs/commands/update.rst b/docs/commands/update.rst index 7f9e15c..ae284a6 100644 --- a/docs/commands/update.rst +++ b/docs/commands/update.rst @@ -19,6 +19,10 @@ bootloader mode, you can do so and simply use the ``--board-id`` option to provi You can specify a language using the ``--language`` option - the default is US English. +If you would like to specify a timeout for how long the CLI will wait for a device in bootloader +mode in secounds (e.g., for scripting), you can use the ``--timeout`` option. The default behavior +is that it will wait indefinitely (``-1`` secounds). + If you would like to include pre-releases as potential update versions, you can use the ``--pre-release`` flag. @@ -44,3 +48,7 @@ both are used, the more limiting flag (``--limit-to-patch``) will take precedenc # Update CircuitPython on the connected board, considering pre-release versions circfirm update --pre-release + + # Update CircuitPython but only wait up to 30 seconds for the device to change from + # bootloader mode + circfirm install 8.0.0 --timeout 30 diff --git a/tests/cli/test_cli_install.py b/tests/cli/test_cli_install.py index a8e3f7e..c3041cd 100644 --- a/tests/cli/test_cli_install.py +++ b/tests/cli/test_cli_install.py @@ -10,6 +10,7 @@ import os import shutil import threading +import time from click.testing import CliRunner @@ -85,3 +86,37 @@ def test_install_bad_version() -> None: # Test using install when in bootloader mode result = RUNNER.invoke(cli, ["install", VERSION]) assert result.exit_code == ERR_IN_BOOTLOADER + + +@tests.helpers.as_circuitpy +def test_install_with_timeout() -> None: + """Tests the install command using the timeout option.""" + try: + threading.Thread(target=tests.helpers.wait_and_set_bootloader).start() + result = RUNNER.invoke(cli, ["install", VERSION, "--timeout", "60"]) + assert result.exit_code == 0 + expected_uf2_filename = circfirm.backend.get_uf2_filename( + "feather_m4_express", VERSION + ) + expected_uf2_filepath = tests.helpers.get_mount_node(expected_uf2_filename) + assert os.path.exists(expected_uf2_filepath) + os.remove(expected_uf2_filepath) + + finally: + board_folder = circfirm.backend.cache.get_board_folder("feather_m4_express") + if board_folder.exists(): + shutil.rmtree(board_folder) + + +@tests.helpers.as_circuitpy +def test_install_with_timeout_failure() -> None: + """Tests the install command using the timeout option that causes a failure.""" + timeout = 3 + start_time = time.time() + result = RUNNER.invoke(cli, ["install", VERSION, "--timeout", f"{timeout}"]) + assert result.exit_code != 0 + assert result.output == ( + "Board ID detected, please switch the device to bootloader mode.\n" + "Error: Bootloader mode device not found within the timeout period\n" + ) + assert time.time() - start_time >= timeout diff --git a/tests/cli/test_cli_update.py b/tests/cli/test_cli_update.py index 6531649..992b87b 100644 --- a/tests/cli/test_cli_update.py +++ b/tests/cli/test_cli_update.py @@ -11,6 +11,7 @@ import pathlib import shutil import threading +import time from click.testing import CliRunner @@ -154,3 +155,17 @@ def test_update_overlimiting() -> None: assert result.exit_code == 1 assert not mount_uf2_files + + +@tests.helpers.as_circuitpy +def test_update_timeout_failure() -> None: + """Tests the update command with a timeout set that times out.""" + timeout = 3 + start_time = time.time() + result = RUNNER.invoke(cli, ["update", "--timeout", f"{timeout}"]) + assert result.exit_code != 0 + assert result.output == ( + "Board ID detected, please switch the device to bootloader mode.\n" + "Error: Bootloader mode device not found within the timeout period\n" + ) + assert time.time() - start_time >= timeout