Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add timeout option to install and update commands #78

Merged
merged 1 commit into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions circfirm/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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


Expand Down
15 changes: 13 additions & 2 deletions circfirm/cli/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
16 changes: 14 additions & 2 deletions circfirm/cli/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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,
Expand All @@ -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)

Expand Down
8 changes: 8 additions & 0 deletions docs/commands/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
8 changes: 8 additions & 0 deletions docs/commands/update.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
35 changes: 35 additions & 0 deletions tests/cli/test_cli_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import os
import shutil
import threading
import time

from click.testing import CliRunner

Expand Down Expand Up @@ -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
15 changes: 15 additions & 0 deletions tests/cli/test_cli_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import pathlib
import shutil
import threading
import time

from click.testing import CliRunner

Expand Down Expand Up @@ -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