feat: auto-lock on all hardware-interacting CLI commands - #270
Open
juul-charles-w wants to merge 2 commits into
Open
feat: auto-lock on all hardware-interacting CLI commands#270juul-charles-w wants to merge 2 commits into
juul-charles-w wants to merge 2 commits into
Conversation
Previously, only `lager python` and admin commands (install/update/
uninstall) acquired an ephemeral box lock. All other hardware commands
(gpio, spi, i2c, uart, debug, supply, battery, etc.) only performed a
passive lock-check (read GET /lock) — meaning two users could
simultaneously drive hardware without either being blocked.
This commit introduces `resolve_box_locked()` in cli/core/net_helpers.py
which resolves the box IP AND acquires an ephemeral TTL-based lock
(with heartbeat) for the duration of the CLI process. Every hardware-
interacting command now uses this instead of plain `resolve_box()`:
- measurement: gpi, gpo, adc, dac, thermocouple, watt, energy,
scope, logic
- communication: spi, i2c, uart, wifi, ble, blufi, usb, router
- power: supply, battery, eload, solar
- development: debug (flash/connect/erase/etc.), arm, webcam
Read-only listing commands (e.g. `lager supply --box X` with no
subcommand) still use the passive check so they never block.
Behavior:
- Lock is ephemeral (holder_type=ephemeral or ci) with a 30min TTL
refreshed by a heartbeat thread every 60s.
- If a pre-existing user lock is held by the same user, it's
preserved (no release on exit).
- If locked by another user: fail-fast in dev, wait+retry in CI
(LAGER_LOCK_WAIT controls).
- LAGER_AUTO_LOCK_DISABLE=1 skips all auto-locking (escape hatch).
- Release on normal exit, exception, signal, or atexit.
The release callable is stashed on ctx.obj._lock_releases for
downstream code or test fixtures that need access.
Co-authored-by: Cursor <cursoragent@cursor.com>
Expand the "Which commands auto-lock" table to cover all measurement, communication, power, and development commands (not just the original 5 admin/test commands). Clarify read-only exclusions and update the backward-compat rationale explaining how atexit + heartbeat + TTL avoids all three v0.13.4 corner cases for the wider command surface. Co-authored-by: Cursor <cursoragent@cursor.com>
juul-charles-w
requested review from
adhanali,
cmfisher606 and
danielrmerskine
as code owners
August 14, 2026 22:16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
resolve_box_locked()tocli/core/net_helpers.py— resolves box IP and acquires an ephemeral TTL+heartbeat lock in one call.resolve_box()withresolve_box_locked()in all hardware-interacting commands: measurement (gpi, gpo, adc, dac, thermocouple, watt, energy, scope, logic), communication (spi, i2c, uart, wifi, ble, blufi, usb, router), power (supply, battery, eload, solar), and development (debug, arm, webcam).lager supply --box Xwith no subcommand,lager boxes list).auto_lock_acquire_for_commandinfrastructure aslager python(atexit release, heartbeat thread, CI-aware collision policy,LAGER_AUTO_LOCK_DISABLEescape hatch).docs/source/reference/cli/locking.mdxwith the expanded command table and revised backward-compat rationale.Motivation
Previously only
lager pythonand admin commands (install,uninstall,update,install-wheel) auto-locked. Any other hardware interaction (toggling GPIO, flashing firmware, connecting UART, etc.) could collide with a running test or another user with no protection. This was the most common source of "mysterious test failures" on shared benches and in CI.How it avoids the v0.13.4 revert issues
atexithook + heartbeat — lock is released on any exit path or reaped by TTL on SIGKILLresolve_box()ttl_seconds=1800+ heartbeat every 60s; server reaps if heartbeat stopsTest plan
test/unit/cli/test_resolve_box_locked.py(acquire, skip-on-disable, stash-release, already-ours)lager gpi --box <name> read DIO0acquires and releases lock (visible inlager boxes lock --status)LAGER_AUTO_LOCK_DISABLE=1 lager gpi ...skips locklager supply voltage 3.3 --box <name>from different users → second gets "box is locked" errorMade with Cursor