Skip to content

feat: auto-lock on all hardware-interacting CLI commands - #270

Open
juul-charles-w wants to merge 2 commits into
lagerdata:mainfrom
juul-charles-w:feature/auto-lock-all-commands
Open

feat: auto-lock on all hardware-interacting CLI commands#270
juul-charles-w wants to merge 2 commits into
lagerdata:mainfrom
juul-charles-w:feature/auto-lock-all-commands

Conversation

@juul-charles-w

Copy link
Copy Markdown
Contributor

Summary

  • Adds resolve_box_locked() to cli/core/net_helpers.py — resolves box IP and acquires an ephemeral TTL+heartbeat lock in one call.
  • Replaces resolve_box() with resolve_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).
  • Read-only/listing paths remain unlocked (e.g. lager supply --box X with no subcommand, lager boxes list).
  • Leverages the same auto_lock_acquire_for_command infrastructure as lager python (atexit release, heartbeat thread, CI-aware collision policy, LAGER_AUTO_LOCK_DISABLE escape hatch).
  • Updates docs/source/reference/cli/locking.mdx with the expanded command table and revised backward-compat rationale.

Motivation

Previously only lager python and 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

Original corner case How this avoids it
Supply commands never released the lock atexit hook + heartbeat — lock is released on any exit path or reaped by TTL on SIGKILL
Long-running commands blocked all other commands Only hardware-interacting subcommands acquire; listing/status paths use passive resolve_box()
Detached processes left stale locks All ephemeral locks have ttl_seconds=1800 + heartbeat every 60s; server reaps if heartbeat stops

Test plan

  • New unit tests in test/unit/cli/test_resolve_box_locked.py (acquire, skip-on-disable, stash-release, already-ours)
  • Manual: lager gpi --box <name> read DIO0 acquires and releases lock (visible in lager boxes lock --status)
  • Manual: LAGER_AUTO_LOCK_DISABLE=1 lager gpi ... skips lock
  • Manual: Two concurrent lager supply voltage 3.3 --box <name> from different users → second gets "box is locked" error
  • CI: existing test suite passes on rebase

Made with Cursor

juul-charles-w and others added 2 commits August 14, 2026 15:15
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant