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-212 — DeviceBackend.is_available Protocol docstring: "reachable right now (no install/start required)".
src/beetroot/api.py:924-929 — Instance.is_available returns self.status == "running" (compose container liveness, not guest reachability).
src/beetroot/backends/vm.py:289-294 — VmDeviceBackend.is_available returns qemu.QemuProcess(self._root).is_running() (QEMU process liveness only).
src/beetroot/backends/adb.py:241-250 — AdbDevice.is_available returns serial_is_available(...) (true adb devices == device reachability).
src/beetroot/cli.py:946 — beetroot 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
- Read the Protocol docstring at
api.py:209-211: is_available is defined as "reachable right now (no install/start required)".
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.
VmDeviceBackend.is_available (vm.py:289-294) returns only qemu.QemuProcess(self._root).is_running() — process liveness.
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.
- 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.
- 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
Summary
The
DeviceBackendProtocol documentsis_availableas "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.AdbDeviceconfirms true guest reachability (adb devicesshows the serial asdevice), whereasVmDeviceBackendandInstance(redroid) confirm only process/container liveness — QEMU running, or the compose containerrunning— neither of which implies the guest Android is actually reachable over adb. Abeetroot lsreader (or any backend-agnostic consumer treatingis_availableas a readiness gate) will seeavailableminutes before the guest can answer.Where
src/beetroot/api.py:207-212—DeviceBackend.is_availableProtocol docstring: "reachable right now (no install/start required)".src/beetroot/api.py:924-929—Instance.is_availablereturnsself.status == "running"(compose container liveness, not guest reachability).src/beetroot/backends/vm.py:289-294—VmDeviceBackend.is_availablereturnsqemu.QemuProcess(self._root).is_running()(QEMU process liveness only).src/beetroot/backends/adb.py:241-250—AdbDevice.is_availablereturnsserial_is_available(...)(trueadb devices==devicereachability).src/beetroot/cli.py:946—beetroot lsrenders the non-directory-backed (adb/vm) row asavailable/unavailablestraight frombackend.is_available.src/beetroot/backends/vm.py:556-614—_wait_for_boot_completed/_boot_completed: the existingadb connect+getprop sys.boot_completedmachinery that does confirm real reachability.Impact
The three implementations split into two incompatible meanings of
is_available:Concretely, on the
binder: vmTCG path a cold boot takes minutes (acknowledged throughoutvm.py).QemuProcess.is_running()isTruefor that entire multi-minute window, long before the guest'sadbdis reachable. Sobeetroot lsrenders the vm row asavailablethe moment QEMU spawns, contradicting the Protocol's "reachable right now (no install/start required)" contract. A user who readsavailableand then runsbeetroot shellagainst the still-booting guest gets a hang/failure. A backend-agnostic consumer that usesis_availableas 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-availablewindow mainly affects concurrent readers, or the crashed-guest-but-live-QEMU state where QEMU survives but the guest is gone.Reasoning / Repro
api.py:209-211:is_availableis defined as "reachable right now (no install/start required)".AdbDevice.is_available(adb.py:241-250) callsserial_is_available, which checksadb deviceslists the serial asdevice— genuine reachability, matching the docstring.VmDeviceBackend.is_available(vm.py:289-294) returns onlyqemu.QemuProcess(self._root).is_running()— process liveness.Instance.is_available(api.py:924-929) returns onlyself.status == "running"— container liveness; the redroid Android userspace continues booting for some time after the container reportsrunning.vm.pyitself distinguishes "QEMU hostfwd bound" from "adbd reachable" (_wait_for_boot_completeddocstring atvm.py:556-565explicitly notes "plain adb connect succeeds the moment QEMU's hostfwd binds, long before the guest's adbd is reachable"),is_running()provably returnsTrueduring the window where the guest is not reachable.is_available == Truedoes 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:
VmDeviceBackend.is_availableconfirm adb reachability by reusing the existing_boot_completed(self.adb_address)helper (vm.py:584-614) — a quickadb connect+getprop sys.boot_completed— so it agrees withAdbDevice. Apply the same treatment toInstance.is_availableso a redroid row reportsavailableonly once the guest is actually reachable (not merely once the container isrunning). Note this adds a subprocess/network probe to a property that is currently cheap; if that is undesirable, prefer Option B.api.py:209-211from "reachable right now" to "started" (process/container liveness), and introduce a separate explicit readiness concept (e.g. anis_ready/wait_until_readymember) for the reachability semantics that adb currently provides. Then alignAdbDevicesois_availablealso 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_availableisFalsewhen QEMU is running but_boot_completedreports not-yet-booted, andTrueonly once the boot-completed probe succeeds (mocksubprocess.run/the adb probe). Mirror the assertion forInstance.is_available(containerrunningbut guest not reachable → not available) and confirmAdbDevicestill behaves as before.Acceptance
is_availableimplementations (Instance,VmDeviceBackend,AdbDevice) share one documented meaning consistent with theDeviceBackendProtocol docstring.VmDeviceBackendandInstanceconfirm 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) rendersavailable/unavailableper the agreed meaning and no longer shows a vm/redroid row asavailablewhile the guest is still booting.