Skip to content

feat(serial): Linux sysfs USB health/topology enrichment (#1091) - #1207

Merged
zackees merged 1 commit into
mainfrom
feat/1091-linux-sysfs-usb
Jul 28, 2026
Merged

feat(serial): Linux sysfs USB health/topology enrichment (#1091)#1207
zackees merged 1 commit into
mainfrom
feat/1091-linux-sysfs-usb

Conversation

@zackees

@zackees zackees commented Jul 28, 2026

Copy link
Copy Markdown
Member

Implements the Linux slice of #1091: sysfs-based USB health/topology enrichment, designed so every line of parsing logic is fixture-testable on any OS.

  • Platform-neutral core: sysfs_usb parses a rooted directory tree; only live_root() (/sys/bus/usb/devices) is Linux-gated. Interfaces are detected structurally (subdirectory containing bInterfaceClass) instead of by Linux's colon-containing interface names — colons are illegal in NTFS filenames, so this keeps fixtures Windows-legal while matching real sysfs nesting.
  • Conservative health mapping (per meta: Windows USB phantom-device detection and recoverable native-USB deployment #1149's locked invariants — facts preserved, Unknown never fabricated): PresentProblem only on concrete signals (authorized=0, unconfigured, CDC interface with no bound driver) using documented Linux-only problem codes explicitly unrelated to Windows CM_PROB_*; healthy requires a bound tty interface; everything ambiguous stays Unknown. The Windows UsbProblemDevice shape is not abused — Linux gets a distinct diagnostics-only LinuxUsbProblemDevice + present_usb_problem_devices_linux(); existing APIs behave identically on all platforms.
  • behind_external_hub mirrors the Windows ancestry classification via port-path depth (1-1 root-attached vs 1-1.4 behind a hub).
  • macOS: explicitly unchanged/Unknown — IOKit work requires a Mac host (documented in the module).

Validation (local)

  • soldr cargo test -p fbuild-serial → 176 + 7 passed (15 new fixture tests: healthy CDC, hub vs root topology, unauthorized, unconfigured, driverless CDC interface, malformed/partial entries never panic, RP2040-typical synthetic VID/PID fixture, unowned tty stays Unknown); clippy -D warnings clean; dependents (fbuild-daemon, fbuild-cli) check clean; full bash test before merge.

Part of #1091 — remaining: macOS (IOKit) and the real-device validation runs on both platforms.

🤖 Generated with Claude Code

- New platform-neutral sysfs_usb module: pure functions over a rooted
  directory (live root /sys/bus/usb/devices is the only
  cfg(target_os=linux) piece), so all parsing is fixture-tested on any
  OS. Interfaces are identified structurally (subdir with
  bInterfaceClass) rather than by the colon-named Linux dir, keeping
  fixtures Windows-legal while staying faithful to real sysfs layout.
- Health mapping is conservative per #1149's invariants: concrete
  signals only (unauthorized / unconfigured / CDC interface without a
  bound driver -> PresentProblem with documented Linux-only codes;
  healthy requires a bound tty interface); everything ambiguous stays
  Unknown, never fabricated. Windows shapes are not reused: a distinct
  LinuxUsbProblemDevice diagnostics type + sibling
  present_usb_problem_devices_linux(); existing APIs unchanged on all
  platforms. behind_external_hub mirrors the Windows ancestry
  semantics via port-path depth.
- macOS explicitly unchanged (IOKit needs a mac host; documented).

Part of #1091 (Linux implementation + fixtures; macOS and
real-device validation runs remain).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@zackees, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 32 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 690b9990-4c46-4aee-b342-cff72cf75787

📥 Commits

Reviewing files that changed from the base of the PR and between c7351bf and be818e0.

📒 Files selected for processing (4)
  • crates/fbuild-serial/README.md
  • crates/fbuild-serial/src/lib.rs
  • crates/fbuild-serial/src/ports.rs
  • crates/fbuild-serial/src/sysfs_usb.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1091-linux-sysfs-usb

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@zackees
zackees merged commit d709b18 into main Jul 28, 2026
8 of 14 checks passed
@zackees
zackees deleted the feat/1091-linux-sysfs-usb branch July 28, 2026 06:04
@fastled-project-sync fastled-project-sync Bot moved this to Triage in FastLED Tracker Jul 29, 2026
zackees added a commit that referenced this pull request Jul 31, 2026
… unused_mut, rustdoc links

main is red on six checks. They are unrelated to each other and accumulated
because the workflows that run them are `pull_request`-triggered, so nothing
re-ran them on main after the merges that broke them (#1197, #1206, #1207).
Found while verifying an unrelated dependency bump (#1215); every failure
there reproduced identically on main.

Dylint: drop `crates/fbuild-cli/src/cli/clangd_config.rs` from
ban_unrooted_tempdir's allowlist -- the file was deleted in #1197.

Lint subprocess spawns: annotate three deliberate direct spawns with
`allow-direct-spawn:` markers (clangd test driver, interactive gdb that must
inherit terminal stdio, detached editor launch that must outlive the CLI).
The scanner only inspects the hit line and the one line directly above it, so
the marker must be a single line -- a wrapped comment silently fails to
register. This also fixes Check (ubuntu-latest), which runs the same script.

Check (macos-latest): `ports.rs` bound `let mut ports` unconditionally while
only `cfg(target_os = "linux")` mutates it, so every other unix target tripped
`-D unused-mut`. The `mut` now exists only inside the Linux cfg via a
shadowing block, rather than an `#[allow]`.

Documentation: six rustdoc errors across three crates. URL templates such as
`?query=<optional filter>` parsed as unclosed HTML tags; several intra-doc
links pointed at `cfg(target_os = "linux")` items that do not exist to resolve
against when docs build on a non-Linux host; three more pointed at private
items. Unresolvable links become code spans, each with a short note on why it
is deliberately not a link.

CH32V006 (`build / build`) is also red on main but is a vendor-C compile error
in the openwch Arduino core, not CI hygiene, and is left for a targeted fix.

Verified locally on Windows, all exit 0: check_dylint_allowlists.py,
find_direct_subprocess.py --fail, cargo doc with RUSTDOCFLAGS=-D warnings,
clippy -D warnings, cargo fmt --check, and `bash test` (77 suites, 0 failed).

The macOS fix could not be verified locally: cross-compiling fbuild-serial to
x86_64-apple-darwin fails on ring's build script needing a macOS `cc`. The
cfg-shadowing pattern was verified in an isolated cargo project (clean under
-D warnings with the cfg both on and off); Check (macos-latest) on this PR is
the real proof.

Co-Authored-By: Claude <noreply@anthropic.com>
zackees added a commit that referenced this pull request Jul 31, 2026
Fallout from repairing Dylint. With `dylint_linting` pinned back to 6.0.1 the
driver builds again, so the lints actually *ran* for the first time in a
while -- and `ban_std_pathbuf` immediately flagged three real violations in
`crates/fbuild-serial/src/sysfs_usb.rs`, a file added by #1207. They went
unnoticed because Dylint was failing at two earlier layers (a stale allowlist
path, then the 6.0.2 driver), so the lint pass was never reached.

Migrated rather than allowlisted. The allowlist's own header is explicit:
"New files MUST NOT be added here. The target state is zero entries."
`sysfs_usb.rs` is a new file, and exempting it would have quietly defeated the
lint that was just repaired.

  - `scan_usb_devices`'s `device_dirs` is now `Vec<(String, NormalizedPath)>`.
  - `live_root()` returns `NormalizedPath`. Callers are unchanged: they pass
    `&root` into functions taking `&Path`, which `Deref` coercion handles.

Dropping `PathBuf` from the library left the top-level import unused, which is
an error under CI's `RUSTFLAGS="-D warnings"`, not a warning. The test module
still builds fixture paths with it, so the import moved there.

Verified: `cargo clippy --workspace --all-targets -- -D warnings`,
`cargo fmt --all --check`, and `bash test` (77 suites, 0 failed) all green;
workspace clippy confirms the `live_root` signature change breaks no callers.

Co-Authored-By: Claude <noreply@anthropic.com>
zackees added a commit that referenced this pull request Jul 31, 2026
Pinning dylint back to =6.0.1 made the driver build again, so the lints
actually ran for the first time in a while and reported 34 violations across
three crates. None are new: they accumulated in #1197, #1206 and #1207 while
Dylint was failing at earlier steps (a stale allowlist path, then the 6.0.2
driver) and never reached the lint pass.

Handled by category rather than uniformly.

ban_std_pathbuf (30) -- migrated to `fbuild_core::path::NormalizedPath` in
`fbuild-cli` (`ide.rs`, `debug.rs`, `ide_debug.rs`, `clangd_config/{mod,
vscode,zed}.rs`), `fbuild-daemon` (`handlers/libraries.rs`) and the
`sysfs_usb.rs` test fixtures. Not allowlisted: that allowlist's header states
"New files MUST NOT be added here. The target state is zero entries", and
exempting brand-new files would hollow out the gate that was just repaired.
`NormalizedPath` derefs to `Path`, so call sites taking `&Path` are unchanged;
`to_path_buf()` appears only where an external API (`BuildLayout::new`)
demands an owned `PathBuf`.

ban_std_fs_in_async (2) -- `handlers/libraries.rs::installed_dir_names` now
uses async `fbuild_core::fs::read_dir` (the sanctioned re-export of
`tokio::fs`), which makes it and `build_library_entries` async along with
their three unit tests. It also switches the directory test from
`Path::is_dir()` to `entry.file_type().await`: `is_dir()` is a blocking
`stat` on the async worker, so keeping it would have satisfied the lint's
letter while preserving exactly the stall the lint exists to prevent
(#844).

ban_raw_subprocess (2) -- allowlisted with justifications, which is what the
lint's own diagnostic offers for genuinely-justified sites. Both are correct
as raw spawns: `fbuild debug` hands the terminal to gdb, so the child must
inherit this process's stdio rather than be captured; and `fbuild ide`
launches the editor detached so it OUTLIVES the CLI, which is the opposite of
what a containment group guarantees.

Verified: `cargo clippy --workspace --all-targets -- -D warnings`,
`cargo fmt --all --check`, and `bash test` (77 suites, 0 failed) all green.

Dylint itself could not be run locally to pre-verify -- the pinned nightly and
`cargo-dylint` are present, but `dylint-link` fails to link the lint cdylibs
on this Windows host. CI is the proof for that gate.

Co-Authored-By: Claude <noreply@anthropic.com>
zackees added a commit that referenced this pull request Jul 31, 2026
Pinning dylint back to =6.0.1 made the driver build again, so the lints
actually ran for the first time in a while and reported 34 violations across
three crates. None are new: they accumulated in #1197, #1206 and #1207 while
Dylint was failing at earlier steps (a stale allowlist path, then the 6.0.2
driver) and never reached the lint pass.

Handled by category rather than uniformly.

ban_std_pathbuf (30) -- migrated to `fbuild_core::path::NormalizedPath` in
`fbuild-cli` (`ide.rs`, `debug.rs`, `ide_debug.rs`, `clangd_config/{mod,
vscode,zed}.rs`), `fbuild-daemon` (`handlers/libraries.rs`) and the
`sysfs_usb.rs` test fixtures. Not allowlisted: that allowlist's header states
"New files MUST NOT be added here. The target state is zero entries", and
exempting brand-new files would hollow out the gate that was just repaired.
`NormalizedPath` derefs to `Path`, so call sites taking `&Path` are unchanged;
`to_path_buf()` appears only where an external API (`BuildLayout::new`)
demands an owned `PathBuf`.

ban_std_fs_in_async (2) -- `handlers/libraries.rs::installed_dir_names` now
uses async `fbuild_core::fs::read_dir` (the sanctioned re-export of
`tokio::fs`), which makes it and `build_library_entries` async along with
their three unit tests. It also switches the directory test from
`Path::is_dir()` to `entry.file_type().await`: `is_dir()` is a blocking
`stat` on the async worker, so keeping it would have satisfied the lint's
letter while preserving exactly the stall the lint exists to prevent
(#844).

ban_raw_subprocess (2) -- allowlisted with justifications, which is what the
lint's own diagnostic offers for genuinely-justified sites. Both are correct
as raw spawns: `fbuild debug` hands the terminal to gdb, so the child must
inherit this process's stdio rather than be captured; and `fbuild ide`
launches the editor detached so it OUTLIVES the CLI, which is the opposite of
what a containment group guarantees.

Verified: `cargo clippy --workspace --all-targets -- -D warnings`,
`cargo fmt --all --check`, and `bash test` (77 suites, 0 failed) all green.

Dylint itself could not be run locally to pre-verify -- the pinned nightly and
`cargo-dylint` are present, but `dylint-link` fails to link the lint cdylibs
on this Windows host. CI is the proof for that gate.

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

Also widens the cargo-cache .gitignore entries to `**/`-prefixed forms. The
existing patterns contain a slash, so git anchors them to the repo root and
they never covered nested crates; running any cargo command inside
`dylints/<lint>/` (each is its own workspace) materializes a full registry
cache there that a `git add -A` will stage.
zackees added a commit that referenced this pull request Jul 31, 2026
Fallout from repairing Dylint. With `dylint_linting` pinned back to 6.0.1 the
driver builds again, so the lints actually *ran* for the first time in a
while -- and `ban_std_pathbuf` immediately flagged three real violations in
`crates/fbuild-serial/src/sysfs_usb.rs`, a file added by #1207. They went
unnoticed because Dylint was failing at two earlier layers (a stale allowlist
path, then the 6.0.2 driver), so the lint pass was never reached.

Migrated rather than allowlisted. The allowlist's own header is explicit:
"New files MUST NOT be added here. The target state is zero entries."
`sysfs_usb.rs` is a new file, and exempting it would have quietly defeated the
lint that was just repaired.

  - `scan_usb_devices`'s `device_dirs` is now `Vec<(String, NormalizedPath)>`.
  - `live_root()` returns `NormalizedPath`. Callers are unchanged: they pass
    `&root` into functions taking `&Path`, which `Deref` coercion handles.

Dropping `PathBuf` from the library left the top-level import unused, which is
an error under CI's `RUSTFLAGS="-D warnings"`, not a warning. The test module
still builds fixture paths with it, so the import moved there.

Verified: `cargo clippy --workspace --all-targets -- -D warnings`,
`cargo fmt --all --check`, and `bash test` (77 suites, 0 failed) all green;
workspace clippy confirms the `live_root` signature change breaks no callers.

Co-Authored-By: Claude <noreply@anthropic.com>
zackees added a commit that referenced this pull request Jul 31, 2026
Pinning dylint back to =6.0.1 made the driver build again, so the lints
actually ran for the first time in a while and reported 34 violations across
three crates. None are new: they accumulated in #1197, #1206 and #1207 while
Dylint was failing at earlier steps (a stale allowlist path, then the 6.0.2
driver) and never reached the lint pass.

Handled by category rather than uniformly.

ban_std_pathbuf (30) -- migrated to `fbuild_core::path::NormalizedPath` in
`fbuild-cli` (`ide.rs`, `debug.rs`, `ide_debug.rs`, `clangd_config/{mod,
vscode,zed}.rs`), `fbuild-daemon` (`handlers/libraries.rs`) and the
`sysfs_usb.rs` test fixtures. Not allowlisted: that allowlist's header states
"New files MUST NOT be added here. The target state is zero entries", and
exempting brand-new files would hollow out the gate that was just repaired.
`NormalizedPath` derefs to `Path`, so call sites taking `&Path` are unchanged;
`to_path_buf()` appears only where an external API (`BuildLayout::new`)
demands an owned `PathBuf`.

ban_std_fs_in_async (2) -- `handlers/libraries.rs::installed_dir_names` now
uses async `fbuild_core::fs::read_dir` (the sanctioned re-export of
`tokio::fs`), which makes it and `build_library_entries` async along with
their three unit tests. It also switches the directory test from
`Path::is_dir()` to `entry.file_type().await`: `is_dir()` is a blocking
`stat` on the async worker, so keeping it would have satisfied the lint's
letter while preserving exactly the stall the lint exists to prevent
(#844).

ban_raw_subprocess (2) -- allowlisted with justifications, which is what the
lint's own diagnostic offers for genuinely-justified sites. Both are correct
as raw spawns: `fbuild debug` hands the terminal to gdb, so the child must
inherit this process's stdio rather than be captured; and `fbuild ide`
launches the editor detached so it OUTLIVES the CLI, which is the opposite of
what a containment group guarantees.

Verified: `cargo clippy --workspace --all-targets -- -D warnings`,
`cargo fmt --all --check`, and `bash test` (77 suites, 0 failed) all green.

Dylint itself could not be run locally to pre-verify -- the pinned nightly and
`cargo-dylint` are present, but `dylint-link` fails to link the lint cdylibs
on this Windows host. CI is the proof for that gate.

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

Also widens the cargo-cache .gitignore entries to `**/`-prefixed forms. The
existing patterns contain a slash, so git anchors them to the repo root and
they never covered nested crates; running any cargo command inside
`dylints/<lint>/` (each is its own workspace) materializes a full registry
cache there that a `git add -A` will stage.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

1 participant