Skip to content

fix(input): release host capture on pump abort and cap blocksd replies - #374

Merged
hyperb1iss merged 3 commits into
mainfrom
nova/device-input-followups
Oct 9, 2026
Merged

hyperb1iss merged 3 commits into
mainfrom
nova/device-input-followups

Conversation

@hyperb1iss

Copy link
Copy Markdown
Owner

Stacked on #323 (nova/device-input). Both fixes reuse code that PR adds. Once #323 merges, this retargets to main.

What this changes

Two small robustness gaps that turned up while hardening #323, neither of them new:

  • Host capture is released on every pump exit. The input pump released screen, audio, and interaction capture only when its worker exited cleanly. A worker aborted at the 1 s shutdown deadline, or one that panicked, left capture demand active. feat(input): route ROLI touch and button input from blocksd #323 fixed the same gap for device input demand with a drop guard. A HostCaptureDemand guard now does the same for host capture, declared so it drops after DeviceInputDemand, matching the clean path's order.
  • blocksd request replies are length-capped. The request connection read replies with an uncapped read_line, bounded only by its timeout. feat(input): route ROLI touch and button input from blocksd #323's 64 KiB event-stream framing moves into a shared blocks/framing.rs helper (read_line_capped), and the request connection now uses it with a 1 MiB cap. An oversized reply fails its request with a clear error and is discarded through its newline, so the next request reads its own reply. The backend still reconnects after any failed request, as before.

Why 1 MiB

The largest blocksd reply is discover_response, at about 230 bytes per block. blocksd counts a topology's blocks in 7 bits (at most 127), so a full topology is about 28 KiB. With every name and version string at its 127-character limit and fully escaped, it's about 215 KiB. A lost discover reply hides every block, so the cap leaves a wide margin. Spec 30 §4.3 and its failure table record both caps.

Verification

  • Added or updated tests
  • Added or updated docs (README, AGENTS.md, relevant spec, or guide)
  • just verify passes locally (Rust fmt + lint + test)
  • just deny passes (required for dependency or license changes)
  • just ui-test and just ui-build pass (required for crates/hypercolor-ui/)
  • just sdk-lint, just sdk-check, and just sdk-build pass (required for sdk/)
  • just python-verify passes (required for python/)
  • just compat-check passes (required for data/drivers/vendors/*.toml)
  • just docs-build passes (required for docs or README changes)
  • cd docs && zola check passes (required for docs link/content changes)
  • Packaging scripts were syntax-checked (required for scripts/ or packaging/)
  • just e2e-build passes with the normal Servo stack (required for daemon/UI/effect integration changes)
  • just e2e-build-cpu passes when validating the CPU smoke fallback
  • just e2e passes against the Servo stack (required for end-to-end behavior changes; starts daemon/browser)
  • Tested on real hardware, simulator, or e2e harness (describe below)
Check Result
Daemon input_publication tests, serialized 40 passed
Core device::blocks unit tests, blocks_backend_tests, blocks_input_tests 6, 7, 6 passed
clippy -D warnings on core and daemon, all targets; cargo fmt --check; cargo check --workspace --all-targets clean

The new tests are aborted_pump_worker_releases_host_capture_demand, an_oversized_reply_fails_its_request_and_the_next_request_resyncs, blocks_backend_fails_an_oversized_reply_and_reconnects, and three framing unit tests. An independent verifier passed both commits. It mutation-checked the guard test twice: without the guard, screen capture stays Active, and with the two guards swapped, the probe sees device input still demanded at release. It also confirmed the extracted framing reproduces #323's event-stream behavior line for line.

Notes for reviewers

The guard runs the capture setters from Drop. If the worker panicked inside one of those setters and the release panicked again during unwinding, the process would abort. That can't be made safe from a destructor, because any panic during unwinding aborts. The setters return Result, so this needs a setter that panics on deactivate.

🤖 Generated with Claude Code

hyperb1iss and others added 2 commits October 8, 2026 21:16
The pump released host screen, audio, and interaction capture only
after its worker loop exited normally. When shutdown passed its one
second deadline and aborted the worker, or the worker panicked, that
release never ran and capture sessions stayed open with no pump left
to close them.

A HostCaptureDemand guard now owns the capture demand state for the
worker's lifetime and reconciles every domain to inactive on drop. It
is declared before the DeviceInputDemand guard, so an unwinding or
aborted worker withdraws device input before releasing host capture,
the same order as the clean exit. Abort is asynchronous, so the release
runs when the runtime drops the cancelled task, which can be after
shutdown has already returned its deadline error.

A new test aborts the worker while screen and interaction capture are
active and checks both are released, with a probe source confirming
device input was withdrawn first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The request connection read blocksd replies with an uncapped read_line,
bounded only by the two second request timeout, so a peer that sent
bytes without a newline could grow the reply buffer without limit
inside that window.

The hand framing behind the event stream's 64 KiB cap now lives in a
shared read_line_capped helper that both connections use. It buffers
at most a given number of bytes of a line and discards a longer one
through its newline, leaving the reader framed at the next line.

Replies cap at 1 MiB. The largest is discover_response at about 230
bytes per block; a full topology (blocksd counts blocks in seven bits)
runs about 28 KiB, and about 215 KiB with every name and version string
at its 127-character limit and fully escaped. Losing a discover reply
hides every block, so the cap covers several worst-case topologies.

An oversized reply fails its request with an error naming the cap. The
connection stays framed, so the next request reads its own reply, and
the backend still reconnects after any failed request.

Framing unit tests cover reassembly across refills, the exact cap
boundary, an oversized line, and an unterminated tail. A connection
test pads a pong past the cap, checks the request fails, and checks the
next ping on the same connection succeeds. A backend test pads a key
frame acknowledgement past the cap and checks the write fails and the
backend reconnects. Spec 30 records both caps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

This review includes 10 billable files and costs up to $2.50.

Or wait 59 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a824efcf-4039-4b05-92a6-0a7c99a67ede

📥 Commits

Reviewing files that changed from the base of the PR and between ced501c and 34d02ae.


📒 Files selected for processing (10)
  • crates/hypercolor-core/src/device/blocks/connection.rs
  • crates/hypercolor-core/src/device/blocks/connection/tests.rs
  • crates/hypercolor-core/src/device/blocks/framing.rs
  • crates/hypercolor-core/src/device/blocks/framing/tests.rs
  • crates/hypercolor-core/src/device/blocks/input.rs
  • crates/hypercolor-core/src/device/blocks/mod.rs
  • crates/hypercolor-core/tests/blocks_backend_tests.rs
  • crates/hypercolor-daemon/src/render_thread/input_publication.rs
  • crates/hypercolor-daemon/src/render_thread/input_publication/tests.rs
  • docs/specs/30-roli-blocks-backend.md


  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@hyperb1iss
hyperb1iss changed the base branch from nova/device-input to main October 9, 2026 12:05
Bring in main now that the device input base this branch stacked on
has landed, so the branch can merge on its own. The merge was clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hyperb1iss
hyperb1iss merged commit f8190c9 into main Oct 9, 2026
1 check passed
@hyperb1iss
hyperb1iss deleted the nova/device-input-followups branch October 12, 2026 03:22
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