Repository navigation
Description
Context
The daemon prevents two simultaneous flash operations, but an explicit fbuild device lease ... --track-serial does not reserve the board against another project's deploy. This breaks sharing one board between independent build/flash/RPC test workflows: a second caller can replace the firmware between the first caller's deploy and its validation.
Observed on Linux during FastLED/FastLED#5001 hardware validation, with two independent checkouts (fastled and fastled3) targeting the same ESP32-S3 at /dev/ttyACM1 through the production daemon.
Version: fbuild 2.5.49+git.98affea702ff (98affea702ff58e551124cda68f863659acebb7c). Source was also checked at current local main bebd679cb2decd3ac6cd860f413d1b9bbca5b904; the relevant device-manager/deploy/preemption files are unchanged between those revisions.
Two distinct observations:
-
Concurrent deploy protection works, but does not queue. A deploy from
fastledended after 311 seconds of build work with:serial error: port /dev/ttyACM1 is in use: deploy (project /home/niteris/dev/fastled3/.build/fbuild/esp32s3) by request 521b9eed-ff9c-4695-b428-91c8bb288829 for 20s Deploy failed (fbuild) [311.0s]This is the intended fail-fast operation guard from Daemon lets a second deploy preempt an in-flight deploy on the same port (no per-port lease); contention misreported as 'serial driver may be wedged' #1461 / fix(serial): reject concurrent device operations #1502, not proof that two flashers overlapped.
-
The explicit device lease is bypassed. The daemon log shows:
2026-11-02T23:09:14.480681Z exclusive lease acquired on '/dev/ttyACM1' by client '7a3bf83c-4e00-4d81-b95e-62f02b8840de' 2026-11-02T23:14:32.662980Z port closed port="/dev/ttyACM1" client_id="deploy_preemption" 2026-11-02T23:14:36.650408Z flashing /home/niteris/dev/fastled3/.build/fbuild/esp32s3/.fbuild/build/release/firmware.bin to /dev/ttyACM1No intervening lease-release or daemon-restart event appears in that interval. The next daemon restart is at 23:15:41. This distinguishes the bypass from a lease lost on restart. The lease was acquired for the other checkout's ongoing validation workflow.
Investigation was read-only: no new deploy/reset/serial attach was attempted to reproduce contention on the board currently reserved for validation.
Confirmed source path
handlers/devices.rs:107delegates the public lease endpoint toDeviceManager::acquire_exclusive.device_manager.rs:473checks/storesDeviceState.exclusive_lease.handlers/operations/deploy.rs:490admits deploy throughserial_manager.preempt_for_deploy, without consulting that lease.fbuild-serial/src/manager.rs:961uses a separatePreemptionTracker. Its per-operation guard is released after deploy/recovery.DeployRequesthas request attribution, but no workflow lease ID/token to carry into deploy and later serial/RPC attachment.- Current tests cover exclusive-lease-vs-lease conflicts and deploy-vs-deploy conflicts separately; they do not establish that an explicit exclusive device lease blocks a competing deploy.
Proposal
Unify admission around an identity-bound workflow lease, while retaining the existing per-operation protection:
- Require every mutating device path (deploy, reset, serial write/RPC writer) to honor a foreign exclusive device lease before touching the board. Read-only monitor subscribers may continue to share the daemon's stream where supported.
- Let the lease holder pass its lease identity through deploy, re-enumeration/recovery, and subsequent serial/RPC calls. Holding a lease must not prevent its owner from using the device, and releasing an individual operation must not release the enclosing workflow lease.
- Add an opt-in bounded wait/queue for competing workflows, with holder/progress diagnostics, cancellation, and deterministic ordering. Retain fail-fast behavior for callers that do not opt in. Waiting must occur before preemption/reset/open, not after affecting the current holder.
- Keep the lease keyed to physical device identity when
track_serialis requested, and make disconnect/restart/lost-lease events observable so callers cannot unknowingly validate replacement firmware.
Acceptance criteria
- RED -> GREEN: first add a hardware-free regression using the production admission path: acquire an exclusive device lease for client A, attempt client B's deploy, and assert B cannot enter preemption/flashing. The current code should fail that assertion; the implementation must make it pass.
- Add the equivalent reset/write admission coverage; demonstrate that A can deploy and then perform RPC under the same lease without a release/reacquire gap.
- A bounded queued B waits while A validates, then starts only after A releases. Timeout/cancellation removes B without resetting/closing/flashing A's board; competing devices remain independent.
- Existing fix(serial): reject concurrent device operations #1502 concurrent-deploy protection stays green. A workflow token must not permit two overlapping operations within the same workflow.
- Cover tracked USB port renumbering and clear lost-lease reporting on daemon restart/disconnect.
- After unit coverage passes, validate two real client processes/checkouts taking turns on one board: A builds its firmware, acquires the workflow lease, deploys and completes RPC validation; B queues and runs only after release. Record firmware identity and holder attribution so sequential sharing is proven.
Decisions
- Scope is sequential board sharing through the daemon, not running two firmware images simultaneously.
- Keep compilation parallel and outside the exclusive board phase by default; reserve the board across deploy -> recovery -> RPC validation, which is the critical ownership gap.
- This is a follow-up to the already-fixed operation race, not a reopening of Daemon lets a second deploy preempt an in-flight deploy on the same port (no per-port lease); contention misreported as 'serial driver may be wedged' #1461. Preserve its default fail-fast API and add explicit wait semantics.
- Do not fix this downstream with raw serial opens, shell lock files, or retries that can overwrite another test's firmware. fbuild owns device arbitration.
Related issues
- Daemon lets a second deploy preempt an in-flight deploy on the same port (no per-port lease); contention misreported as 'serial driver may be wedged' #1461 / fix(serial): reject concurrent device operations #1502: simultaneous deploy/preemption protection is present and is working in the observed failure.
- design: restore broker-mediated serial proxy + finish running-process broker (port custody, disconnect events, multi-client singleton, cross-version device lease) #592: the closed broker design explicitly called for lease enforcement on write paths; the current implementation still separates advisory device leases from operation admission.
- device leases: add track_serial and preserve leases across USB port renumbering #638: tracked USB serial identity is the existing foundation for retaining workflow ownership across port renumbering.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTriage
Activity
zackees commentedon Nov 3, 2026
Steward note: the admission half of this issue is now in PR #1869.
DeviceManager::admit_operation(port, lease_id)refuses a mutatingoperation on a port leased by another client with a new
DeviceLeaseError::ForeignLeaseHeld(names the lease id, the holder and bothescape hatches).
/api/deployconsults it on the resolved port beforeserial_manager.preempt_for_deploy, so a competing checkout can no longerclose the holder's session or flash over its firmware — closing observation
2 from the report.
DeployRequest/fbuild deploygain an optional--lease-id./api/resetgets the same gate (a reset would drop the holdermid-validation).
fbuild device take --reasonremains the explicit human override.Still open here (not in #1869): bounded wait/queue for competing workflows
with ordering + cancellation, lease identity on the serial-write/RPC writer
path, tracked-renumbering coverage beyond the existing move test, and
lost-lease reporting on daemon restart/disconnect. The two-process
on-hardware validation still needs a board slot that is not in use for
FastLED/FastLED#5001; it was not attempted this hour.
zackees commentedon Nov 3, 2026
Admission slice of this issue is merged: #1869 (
e55b63e3, squash-merged as13e48205).What landed:
DeviceManager::admit_operation(port, lease_id)— an exclusive lease admits only the lease id it was acquired under; anything else returnsDeviceLeaseError::ForeignLeaseHeldnaming the lease, holder,--lease-id <id>andfbuild device release <port> --lease-id <id>./api/deploychecks admission on the resolved port beforepreempt_for_deploy, so a competing client can no longer close the holder's session or flash over its image./api/resetis gated the same way.DeployRequest/ResetRequestcarry an optionallease_id; CLI gainsfbuild deploy --lease-id <id>(also threaded throughdebug,mcptools and the daemon client types).fbuild device take --reasonis still the explicit human override.DaemonContext; verified RED first by stubbing the gate.Still open on this issue (not in #1869):
--track-serialheuristics (renumbering test beyond the existing move test).zackees commentedon Nov 3, 2026
Second slice of #1864 landed as PR #1871 (unmerged, awaiting review/CI): the
serial write path now honors a foreign exclusive lease.
SerialClientMessage::Attachcarries an optionallease_id(omitted byolder clients → "no lease", the safe reading).
handle_serial_wsadmits throughDeviceManager::admit_operationbeforeopen_port(), so a refused client never opens/resets/reads the board, andre-admits on every write frame so a lease acquired mid-session stops the
next byte with
write_ack{success:false}.fbuild monitor --lease-id <id>lets the holder's own session through.Coverage:
ws_serial_attach_honors_foreign_exclusive_leasedrives the realhandler over a real WebSocket + pty and the real
DeviceManager(refusedattach with no port opened; holder attaches and writes; mid-session lease
steal refuses the next write). RED first with the gate stubbed, then green;
daemon 307p/0f, serial 203p/0f, cli bin 472p/0f, clippy -D warnings clean.
Remaining acceptance criteria on this issue:
holder/progress diagnostics (criterion 3);
lease_idplumbing (currently refused-by-default, safe);board).
zackees commentedon Nov 3, 2026
Second slice is now merged: PR #1871 — the serial write path honors an
exclusive workflow lease (merge commit
9f182e4f).Shipped:
SerialClientMessage::Attachcarries an optionallease_id; omitted means"present no lease" (older clients), which is refused on a reserved board.
handle_serial_wsadmits throughDeviceManager::admit_operationbeforeopen_port(), so a refused client never opens/resets/reads the board, andre-admits on every write frame — a lease taken by another workflow
mid-session stops the next byte with
write_ack{success:false}.fbuild monitor --lease-id <id>for the holder;docs/reference/cli.mdworkflow-lease section updated.
Coverage on main:
ws_serial_attach_is_refused_under_a_foreign_exclusive_lease(portable: refuses no-lease attach without opening the port; holder clears
admission) and the mid-session steal assertion in
serial_ws_replies_follow_request_order(real pty). Zero new host-platformledger rows.
Still open on this issue:
progress diagnostics, waiting before preemption/reset/open;
reattach after
PortRenumberedis not yet re-admitted on the new port);lease_idplumbing (fbuild-pythonhas its own message type;today such sessions are correctly refused, which is the safe default);
board.
zackees commentedon Nov 3, 2026
Third slice of #1864 is merged: PR #1873 — the opt-in bounded wait/queue
(acceptance criterion 3). Merge commit
acc3ac55.Shipped:
DeviceManageradmission queue (device_manager/wait_queue.rs):enqueue_waiter/leave_queue/queued_client_ids/poll_admissionandthe bounded
await_admission.port's FIFO with no foreign lease, the lease holder is never queued, and a
fail-fast caller (no ticket) is refused while anyone is queued — so two
clients that observe a release in the same instant cannot reorder. Queues are
per device, so competing boards stay independent.
QueueGuardowns the ticket, so a dropped/cancelled requestleaves the queue and promotes the next workflow (no phantom head-of-line
blocker). A timeout also leaves the queue.
fbuild deploy --lease-wait <seconds>/DeployRequest.lease_wait_ms(serde default
0= today's fail-fast behaviour). The wait runs at the sameadmission site as fix(daemon): enforce explicit device leases on deploy and reset admission #1869, on the resolved port, before
preempt_for_deploy— so a queued deploy never resets, closes or flashes theholder's board, and never opens the port.
DeviceLeaseError::LeaseWaitTimedOutnames the queue position,the holder (lease id, client, description) and the elapsed wait, and states
the board was not reset/closed/flashed; the refusal without
--lease-waitnow recommends the flag.
queued_client_ids/queued_workflowsexpose queuedepth for status output.
Coverage (RED first, behaviourally: stubbing the admission rule gave
2 passed; 6 failed; restored → green): FIFO head-only ordering, foreign-leaseblocking + holder naming, unqueued caller cannot jump the queue, unknown ports
admitted, bounded wait admits after a real release (≥100 ms), timeout leaves the
queue empty,
wait_ms == 0stays fail-fast, holder never queued, pluslease_wait_msserde defaults.fbuild-daemon --lib317 passed / 0 failed,fbuild-cli --bin fbuild472 passed / 0 failed, clippy-D warningsrc 0,fmt rc 0, boundary
rows=44; dylint_rows=43(ledger untouched).Remaining on this issue:
renumbering half);
lease_id(and nowlease_wait_ms) plumbing;slot not in use for ESP32-S3 LCD_CLOCKLESS: let the application choose the worker task's core (currently created unpinned at priority 23) FastLED#5001.