Skip to content

DeviceBackend.is_available ("reachable right now") is violated by vm and redroid backends, which confirm only process/container liveness #222

Description

@Xiddoc

Summary

The DeviceBackend Protocol documents is_available as "True iff the backend is reachable right now (no install/start required)" (api.py:209-211), but the three implementations disagree on what "reachable" means. AdbDevice confirms true guest reachability (adb devices shows the serial as device), whereas VmDeviceBackend and Instance (redroid) confirm only process/container liveness — QEMU running, or the compose container running — neither of which implies the guest Android is actually reachable over adb. A beetroot ls reader (or any backend-agnostic consumer treating is_available as a readiness gate) will see available minutes before the guest can answer.

Where

  • src/beetroot/api.py:207-212DeviceBackend.is_available Protocol docstring: "reachable right now (no install/start required)".
  • src/beetroot/api.py:924-929Instance.is_available returns self.status == "running" (compose container liveness, not guest reachability).
  • src/beetroot/backends/vm.py:289-294VmDeviceBackend.is_available returns qemu.QemuProcess(self._root).is_running() (QEMU process liveness only).
  • src/beetroot/backends/adb.py:241-250AdbDevice.is_available returns serial_is_available(...) (true adb devices == device reachability).
  • src/beetroot/cli.py:946beetroot ls renders the non-directory-backed (adb/vm) row as available/unavailable straight from backend.is_available.
  • src/beetroot/backends/vm.py:556-614_wait_for_boot_completed / _boot_completed: the existing adb connect + getprop sys.boot_completed machinery that does confirm real reachability.

Impact

The three implementations split into two incompatible meanings of is_available:

  • adb backend — true guest reachability.
  • vm and redroid backends — process/container liveness only.

Concretely, on the binder: vm TCG path a cold boot takes minutes (acknowledged throughout vm.py). QemuProcess.is_running() is True for that entire multi-minute window, long before the guest's adbd is reachable. So beetroot ls renders the vm row as available the moment QEMU spawns, contradicting the Protocol's "reachable right now (no install/start required)" contract. A user who reads available and then runs beetroot shell against the still-booting guest gets a hang/failure. A backend-agnostic consumer that uses is_available as a readiness gate — the natural reading of the docstring — acts prematurely on vm (and, for the boot-not-yet-complete window, redroid) instances.

Severity is low: this is a design/contract inconsistency, not a crash. up() already blocks on _wait_for_boot_completed (vm.py:539-543) before returning, so the stale-available window mainly affects concurrent readers, or the crashed-guest-but-live-QEMU state where QEMU survives but the guest is gone.

Reasoning / Repro

  1. Read the Protocol docstring at api.py:209-211: is_available is defined as "reachable right now (no install/start required)".
  2. AdbDevice.is_available (adb.py:241-250) calls serial_is_available, which checks adb devices lists the serial as device — genuine reachability, matching the docstring.
  3. VmDeviceBackend.is_available (vm.py:289-294) returns only qemu.QemuProcess(self._root).is_running() — process liveness.
  4. Instance.is_available (api.py:924-929) returns only self.status == "running" — container liveness; the redroid Android userspace continues booting for some time after the container reports running.
  5. Because vm.py itself distinguishes "QEMU hostfwd bound" from "adbd reachable" (_wait_for_boot_completed docstring at vm.py:556-565 explicitly notes "plain adb connect succeeds the moment QEMU's hostfwd binds, long before the guest's adbd is reachable"), is_running() provably returns True during the window where the guest is not reachable.
  6. Therefore a vm/redroid is_available == True does not satisfy the documented "reachable right now" contract, while the adb implementation does — the three disagree.

Proposed fix

Pick one consistent meaning across all three implementations:

  • Option A (tighten to match the docstring): Make VmDeviceBackend.is_available confirm adb reachability by reusing the existing _boot_completed(self.adb_address) helper (vm.py:584-614) — a quick adb connect + getprop sys.boot_completed — so it agrees with AdbDevice. Apply the same treatment to Instance.is_available so a redroid row reports available only once the guest is actually reachable (not merely once the container is running). Note this adds a subprocess/network probe to a property that is currently cheap; if that is undesirable, prefer Option B.
  • Option B (relax the contract): Change the Protocol docstring at api.py:209-211 from "reachable right now" to "started" (process/container liveness), and introduce a separate explicit readiness concept (e.g. an is_ready/wait_until_ready member) for the reachability semantics that adb currently provides. Then align AdbDevice so is_available also means "started" and reachability moves to the new member.

Either way the three implementations must agree on a single meaning, and beetroot ls (cli.py:946) should render the column according to that agreed meaning.

Given the 100% line+branch coverage gate, add a unit test that asserts the agreed semantics for each backend: e.g. for Option A, a test that VmDeviceBackend.is_available is False when QEMU is running but _boot_completed reports not-yet-booted, and True only once the boot-completed probe succeeds (mock subprocess.run/the adb probe). Mirror the assertion for Instance.is_available (container running but guest not reachable → not available) and confirm AdbDevice still behaves as before.

Acceptance

  • All three is_available implementations (Instance, VmDeviceBackend, AdbDevice) share one documented meaning consistent with the DeviceBackend Protocol docstring.
  • If "reachable" is kept, VmDeviceBackend and Instance confirm guest adb reachability (e.g. sys.boot_completed), not just process/container liveness; if relaxed to "started", the docstring is updated and a separate readiness concept is added.
  • beetroot ls (cli.py:946) renders available/unavailable per the agreed meaning and no longer shows a vm/redroid row as available while the guest is still booting.
  • New unit tests assert the chosen semantics for each backend (mocked); 100% line+branch coverage maintained.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions