Skip to content

fix(#1154): soldr-style daemon ownership for cold-cache reset (#1159)#1168

Merged
zackees merged 2 commits into
mainfrom
fix/issue-1154-cold-cache
Jul 23, 2026
Merged

fix(#1154): soldr-style daemon ownership for cold-cache reset (#1159)#1168
zackees merged 2 commits into
mainfrom
fix/issue-1154-cold-cache

Conversation

@zackees

@zackees zackees commented Jul 23, 2026

Copy link
Copy Markdown
Member

Implements the #1159 handoff: replaces the rejected two-lock design with soldr's daemon ownership strategy so fbuild clean cache can safely clear the shared zccache compiler-object root.

What changed

  • fbuild_paths::daemon_ownership (new): version-blind, per-cache-root root-owner.lock held by the daemon for its entire lifetime; spawn.lock spawn-herd single-flight; advisory root-owner.json claim (pid/exe/version/mode/cache_root_key/port) — never authoritative without live-PID + exe-identity verification.
  • fbuild_core::process_identity (new): pid liveness, executable-identity gates (fail closed on unverifiable images), bounded TERM→KILL termination — semantics copied from soldr's lifecycle.rs.
  • fbuild-daemon: acquires root ownership before starting the embedded zccache service (10 s bounded poll, fails closed), writes the owner claim once its port is known, removes it on graceful exit.
  • CLI spawn path: spawn-herd election in ensure_daemon_running — one winner spawns and holds the lock through readiness; losers adopt the winner's daemon.
  • fbuild clean cache: holds spawn election across the whole reset → validates cache identity (refuses cross-mode/FBUILD_DAEMON_PORT mismatches) → graceful stop (busy 409 is a hard refusal; lost response with the daemon actually gone is accepted) → legacy/rollout sweep (daemon-*.port discovery + identity-matched HTTP shutdown; claim/pid-file/status-json PID candidates displaced only after pid_is_alive && exe-stem verification) → exclusive root ownership as positive proof before deleting <root>/zccache → daemon restored even when cleanup fails. Idempotent; packages/toolchains/archives retained.
  • fbuild_core::file_lock: Windows ERROR_LOCK_VIOLATION now classified as lock contention instead of a hard error (fixed real test failures on Windows).
  • Docs: CLAUDE.md / runtime.md narrow the "no file-based locks" rule to this sanctioned daemon-lifecycle exception; crate READMEs + CLI reference updated.
  • Benchmark cold-trial cleanup work from the fix(bench): clear compiler caches before every cold build trial #1154 WIP retained (untimed cleanup before every cold trial, none before warm).

Validation (local, per instruction to skip GHA)

  • soldr cargo clippy --workspace --all-targets -- -D warnings → exit 0
  • bash test (full unit suite) → exit 0 (one pre-existing flaky in fbuild-packages-fetch streaming_download_retries_chunk_stalls_five_times_without_output passed 3/3 on rerun; crate untouched by this PR)
  • Real-process regressions: legacy_daemon_transition.rs — unverified/recycled PID never signaled; real fbuild-daemon blocks root-owner.lock while alive and releases it on hard kill (--ignored run passed)
  • Cross-process version-blind ownership test (subprocess re-exec pattern), spawn-lock thread serialization, OwnerClaim round-trip
  • Verified no test writes to the real user cache (temp-home/try_acquire_at seams; real ~/.fbuild/*/daemon unchanged after runs)

Closes #1159. Closes #1154.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added cache as a supported scope for fbuild clean.
    • Clears the active mode’s compiler cache while retaining installed packages, toolchains, frameworks, and archives.
    • Safely coordinates with active build services and restores service availability after cleanup attempts.
    • Improved concurrent service startup and lifecycle reliability.
  • Documentation

    • Expanded CLI and architecture documentation for cache cleanup and daemon coordination.
    • Clarified cold and warm benchmark procedures and outputs.
  • Tests

    • Added coverage for cache cleanup safety, service recovery, benchmark behavior, and output generation.

zackees and others added 2 commits July 23, 2026 16:27
Replace the rejected two-lock (reset-gate + shared lifecycle) design with
soldr's daemon ownership strategy:

- fbuild_paths::daemon_ownership: version-blind per-cache-root
  root-owner.lock held for the daemon's entire lifetime, spawn.lock
  single-flight election, and an advisory root-owner.json claim
  (pid/exe/version/mode/cache_root_key/port).
- fbuild_core::process_identity: pid liveness, executable-identity
  (fails closed), and bounded terminate helpers copied from soldr.
- fbuild-daemon acquires root ownership before starting the embedded
  zccache service and fails closed if another daemon owns the root.
- fbuild clean cache: holds spawn election across the whole reset,
  validates cache identity, distinguishes busy refusal from lost
  shutdown, sweeps legacy daemons via daemon-*.port discovery with
  verified-PID displacement only, takes exclusive root ownership as
  positive proof before deleting <root>/zccache, and restores the
  daemon even when cleanup fails.
- fbuild_core::file_lock: classify Windows ERROR_LOCK_VIOLATION as
  contention (was misread as a hard error).
- Real-process regression tests: legacy/unverified PID is never
  signaled; abrupt daemon death releases root ownership.
- Docs: narrow the "no file-based locks" rule to the sanctioned
  daemon-lifecycle exception.

Closes #1159. Closes #1154.

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

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request adds daemon cache-root ownership and spawn coordination, introduces fbuild clean cache, strengthens process-safety checks, refactors benchmark cold-cache preparation, and adds coverage and documentation for cleanup, ownership, and generated benchmark artifacts.

Changes

Cache reset and benchmark coordination

Layer / File(s) Summary
Locking and process identity foundation
Cargo.toml, crates/fbuild-core/*, crates/fbuild-serial/src/ports.rs
Adds cross-process file-locking APIs, PID liveness and executable-identity helpers, platform-specific tests, and supporting dependency/module documentation.
Daemon ownership lifecycle
crates/fbuild-paths/*, crates/fbuild-daemon/src/main.rs, crates/fbuild-daemon/tests/*
Adds root ownership and spawn locks, serialized owner claims, daemon startup/shutdown integration, and real-process ownership-release regression tests.
CLI cache reset and daemon election
crates/fbuild-cli/src/cli/*, crates/fbuild-cli/src/daemon_client.rs, crates/fbuild-build-engine/src/framework_core_cache.rs, docs/*, agents/docs/*, CLAUDE.md
Adds the cache cleanup scope, coordinated cache deletion and daemon restart behavior, legacy-daemon safety checks, spawn election, hermetic cache tests, and updated CLI/runtime documentation.
Benchmark cold-cache sequencing
bench/fastled-examples/src/*, bench/blink/README.md, .github/workflows/*
Separates untimed cold preparation from timed builds, applies tool-specific cleanup before every cold trial, preserves warm rebuilds, updates published metadata, and adds benchmark output and rendering tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: soldr-style daemon ownership for cold-cache reset.
Linked Issues check ✅ Passed The PR adds cache cleanup, ownership/spawn coordination, tests, and docs that align with #1159 and #1154.
Out of Scope Changes check ✅ Passed The changes stay focused on cold-cache reset, daemon ownership, tests, and documentation; no unrelated scope is evident.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-1154-cold-cache

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 d4b20d2 into main Jul 23, 2026
90 of 97 checks passed
@zackees
zackees deleted the fix/issue-1154-cold-cache branch July 23, 2026 23:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agents/docs/commands-reference.md`:
- Line 15: Escape the pipe separators in the `fbuild clean sketch|all|cache`
command text within the commands-reference table as `\|`, preserving the
three-column table structure and all other row content.

In `@crates/fbuild-core/Cargo.toml`:
- Around line 41-43: Replace the fs2-based locking dependency with Rust
standard-library FileExt locking primitives while preserving the existing
shared/exclusive try-lock API and File::lock_contended_error behavior. Update
the fbuild-core locking implementation to use std::fs::FileExt, then remove fs2
from crates/fbuild-core/Cargo.toml and the root workspace dependencies once no
references remain.

In `@crates/fbuild-core/src/file_lock.rs`:
- Around line 6-59: Replace the fs2-based locking in try_acquire with Rust 1.89+
std::fs::File methods, using try_lock_shared or try_lock according to
FileLockMode and mapping TryLockError::WouldBlock to Ok(None) while propagating
other errors. Remove lock_is_held and the fs2 import, then delete the related
fs2 dependency references from the workspace and Cargo manifests.
- Line 1: Update the file-locking implementation in file_lock.rs to use
std::fs::File native try_lock/try_lock_shared APIs, handling
TryLockError::WouldBlock for contention and removing the hand-rolled
lock_is_held classifier. Remove the fs2 dependency from both workspace and
fbuild-core Cargo.toml manifests while preserving existing lock behavior.

In `@crates/fbuild-core/src/process_identity.rs`:
- Around line 75-87: Update pid_executable_path to satisfy the subprocess-spawn
lint by routing the /bin/ps invocation through
fbuild_core::subprocess::{run_command,run_command_passthrough}, or add an
allow-direct-spawn marker with a concise reason if direct Command usage must
remain. Ensure the ps execution is bounded with the existing supported timeout
mechanism before processing its output.

In `@crates/fbuild-paths/src/daemon_ownership.rs`:
- Line 293: Update the #[ignore] attribute for
root_ownership_is_version_blind_across_processes to include a valid tracking
issue reference or concrete hardware/toolchain requirement, following the
sibling ignore annotations’ convention such as (`#1159`).
- Around line 309-320: Add the required allow-direct-spawn marker comment
immediately above the std::process::Command::new call inside the run_probe
closure, including a concise reason for this intentional subprocess test probe.
Leave the existing command arguments and environment setup unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5800ff9b-ccd3-4b59-ad8b-941dbf797aec

📥 Commits

Reviewing files that changed from the base of the PR and between fa72629 and be506e8.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • .github/workflows/benchmark-build-comparison.yml
  • CLAUDE.md
  • Cargo.toml
  • agents/docs/commands-reference.md
  • bench/blink/README.md
  • bench/fastled-examples/src/README.md
  • bench/fastled-examples/src/build_comparison.rs
  • bench/fastled-examples/src/build_comparison_tests.rs
  • crates/fbuild-build-engine/src/framework_core_cache.rs
  • crates/fbuild-cli/src/cli/args.rs
  • crates/fbuild-cli/src/cli/clean.rs
  • crates/fbuild-cli/src/cli/tests.rs
  • crates/fbuild-cli/src/daemon_client.rs
  • crates/fbuild-core/Cargo.toml
  • crates/fbuild-core/README.md
  • crates/fbuild-core/src/file_lock.rs
  • crates/fbuild-core/src/lib.rs
  • crates/fbuild-core/src/process_identity.rs
  • crates/fbuild-daemon/src/main.rs
  • crates/fbuild-daemon/tests/legacy_daemon_transition.rs
  • crates/fbuild-paths/Cargo.toml
  • crates/fbuild-paths/README.md
  • crates/fbuild-paths/src/daemon_ownership.rs
  • crates/fbuild-paths/src/lib.rs
  • crates/fbuild-serial/src/ports.rs
  • docs/architecture/runtime.md
  • docs/reference/cli.md

|---|---|---|
| `fbuild build` | You want to compile firmware for the env specified by `-e <env>` and `<project_dir>`. The default path; cache via daemon. | `fbuild help build` |
| `fbuild clean sketch|all` | You want to remove one environment/profile's project outputs, optionally including its exact reusable framework-cache entries, without compiling or deploying. | `fbuild help clean`, FastLED/fbuild#1089 |
| `fbuild clean sketch|all|cache` | You want to remove one target's project outputs, optionally its reusable framework objects; `cache` additionally resets the active mode's global compiler-object cache while retaining installed packages/toolchains. | `fbuild help clean`, FastLED/fbuild#1089, FastLED/fbuild#1154 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Escape the pipes — the table row is broken (MD056).

The | characters in sketch|all|cache are parsed as column delimiters, so this row expands to 5 cells against a 3-column table and renders incorrectly. Escape them as \|.

Proposed fix
-| `fbuild clean sketch|all|cache` | You want to remove one target's project outputs, optionally its reusable framework objects; `cache` additionally resets the active mode's global compiler-object cache while retaining installed packages/toolchains. | `fbuild help clean`, FastLED/fbuild#1089, FastLED/fbuild#1154 |
+| `fbuild clean sketch\|all\|cache` | You want to remove one target's project outputs, optionally its reusable framework objects; `cache` additionally resets the active mode's global compiler-object cache while retaining installed packages/toolchains. | `fbuild help clean`, FastLED/fbuild#1089, FastLED/fbuild#1154 |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| `fbuild clean sketch|all|cache` | You want to remove one target's project outputs, optionally its reusable framework objects; `cache` additionally resets the active mode's global compiler-object cache while retaining installed packages/toolchains. | `fbuild help clean`, FastLED/fbuild#1089, FastLED/fbuild#1154 |
| `fbuild clean sketch\|all\|cache` | You want to remove one target's project outputs, optionally its reusable framework objects; `cache` additionally resets the active mode's global compiler-object cache while retaining installed packages/toolchains. | `fbuild help clean`, FastLED/fbuild#1089, FastLED/fbuild#1154 |
🧰 Tools
🪛 LanguageTool

[style] ~15-~15: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ...| fbuild clean sketch|all|cache | You want to remove one target's project outputs, op...

(REP_WANT_TO_VB)

🪛 markdownlint-cli2 (0.23.0)

[warning] 15-15: Table column count
Expected: 3; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agents/docs/commands-reference.md` at line 15, Escape the pipe separators in
the `fbuild clean sketch|all|cache` command text within the commands-reference
table as `\|`, preserving the three-column table structure and all other row
content.

Source: Linters/SAST tools

Comment on lines +41 to +43
# Cross-process shared/exclusive fbuild-daemon lifecycle gates. These are not
# used for zccache object access. Locks are released if a process exits.
fs2 = { workspace = true }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant files =="
git ls-files | rg '(^|/)Cargo\.toml$|file_lock\.rs$|crates/fbuild-core/Cargo\.toml$' | sed -n '1,120p'

echo "== root Cargo manifests around relevant dependencies =="
for f in Cargo.toml crates/fbuild-core/Cargo.toml; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    cat -n "$f" | sed -n '1,180p'
  fi
done

echo "== fs2 references =="
rg -n "fs2|flock|lock" crates/fbuild-core crates/fbuild-* Cargo.toml ci/check_workspace_crates.py ci/hooks/crate_guard.py -S || true

Repository: FastLED/fbuild

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file_lock.rs =="
fd -a 'file_lock\.rs$' . | sed 's#^\./##'

echo "== inspect file_lock.rs files =="
while read -r f; do
  echo "--- $f ---"
  wc -l "$f"
  cat -n "$f" | sed -n '1,220p'
done < <(fd 'file_lock\.rs$' .)

Repository: FastLED/fbuild

Length of output: 319


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file_lock.rs =="
wc -l crates/fbuild-core/src/file_lock.rs
cat -n crates/fbuild-core/src/file_lock.rs | sed -n '1,220p'

echo "== fs2 references (targeted) =="
rg -n "fs2|LockFileExt|file_lock" crates/fbuild-core Cargo.toml crates/fbuild-core/Cargo.toml -S || true

echo "== manifest dependency sections (targeted) =="
cat -n Cargo.toml | sed -n '1,180p' | rg -n '^\s*(version|edition|fs2|fbuild-core|workspace|dependencies|members)=|^\s*fs2|^\s*\[work|^\s*\[package|^\s*dependencies|^#|^$' -C 2 || true
cat -n crates/fbuild-core/Cargo.toml | sed -n '1,180p' | rg -n '^\s*(version|edition|fs2|devdependencies?|dependencies|features)=|^\s*fs2|^\s*\[package\]|^\s*\[dependencies\]|^\s*\[dev-dependencies\]|^#|^$' -C 2 || true

Repository: FastLED/fbuild

Length of output: 7102


🌐 Web query:

Rust std::fs::File lock_shared lock_exclusive FileExt lock_shared lock_exclusive stable version

💡 Result:

As of July 23, 2026, the Rust standard library (std::fs::File) provides built-in, stable support for file locking, including lock_shared and lock_exclusive [1][2][3]. These methods were introduced directly into the std::fs::File API and are no longer dependent on third-party crates like fs2 or fs4 for basic file locking functionality [1][4][5]. Key details regarding the standard library's file locking API: Methods: - lock_shared: Acquires a shared (non-exclusive) lock, blocking until acquired [1][3]. - lock: Acquires an exclusive lock, blocking until acquired [3]. - try_lock_shared: Attempts to acquire a shared lock without blocking [1][3]. - try_lock: Attempts to acquire an exclusive lock without blocking [1][3]. - unlock: Releases locks held on the file [3]. Important Considerations: - Advisory vs. Mandatory: These locks may be advisory or mandatory depending on the platform [1][3]. - Platform Specifics: Behavior regarding interactions with read/write operations and other file handles is platform-dependent [1][3]. - Cloned Handles: If a file handle (or its clone) already holds a lock, behavior is unspecified and may cause deadlocks [1]. - Release: Locks are automatically released when the file handle (and any duplicated/inherited descriptors) is closed, or explicitly via the unlock method [1][3]. Historically, Rust developers relied on extension traits like FileExt from crates such as fs2 or fs4 to provide this functionality [4][5]. With the inclusion of these methods in the standard library, this is now a native capability of std::fs::File [1][3].

Citations:


🌐 Web query:

Rust 1.76 std::fs::File lock_shared lock_exclusive try_lock_shared try_lock_exclusive

💡 Result:

As of Rust 1.76, the std::fs::File struct does not include the methods lock_shared, lock_exclusive, try_lock_shared, or try_lock_exclusive [1]. These file-locking features were stabilized in Rust 1.89 [2]. Before this release, developers needing file locking in Rust typically relied on third-party crates such as the 'fs2' or 'fd-lock' crates. In the current stable version of Rust, the std::fs::File struct provides methods like lock, lock_shared, try_lock, and try_lock_shared to handle advisory or mandatory file locking [3][4][2]. These methods are designed to interact with each other and release automatically when the File handle is dropped, though they can also be released explicitly using the unlock method [3][5][2].

Citations:


Replace fs2 with std file-locking primitives.

fbuild-core targets Rust 1.94.1, so use std::fs::FileExt; covers shared/exclusive try locks, but lock_contended_error() has been File::lock_contended_error() in the std API long enough here; keep the API surface and remove fs2 from crates/fbuild-core/Cargo.toml and the root workspace dependency when it is no longer needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fbuild-core/Cargo.toml` around lines 41 - 43, Replace the fs2-based
locking dependency with Rust standard-library FileExt locking primitives while
preserving the existing shared/exclusive try-lock API and
File::lock_contended_error behavior. Update the fbuild-core locking
implementation to use std::fs::FileExt, then remove fs2 from
crates/fbuild-core/Cargo.toml and the root workspace dependencies once no
references remain.

@@ -0,0 +1,157 @@
//! Cross-process locks for fbuild-daemon startup/lifecycle coordination.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo files =="
git ls-files | rg '(^|/)Cargo.toml$|crates/fbuild-core/src/file_lock.rs' || true

echo
echo "== Cargo manifest snippets =="
for f in Cargo.toml crates/fbuild-core/Cargo.toml; do
  if [ -f "$f" ]; then
    echo "--- $f"
    nl -ba "$f" | sed -n '1,150p'
  fi
done

echo
echo "== file_lock.rs =="
if [ -f crates/fbuild-core/src/file_lock.rs ]; then
  nl -ba crates/fbuild-core/src/file_lock.rs | sed -n '1,180p'
fi

echo
echo "== search fs2/lock_is_held/try_lock_shared/lock_shared =="
rg -n 'fs2|lock_is_held|try_lock_shared|try_lock_exclusive|lock_shared|lock_exclusive|std::fs::File|TryLockError' . --glob '*.rs' --glob 'Cargo.toml' || true

Repository: FastLED/fbuild

Length of output: 2142


🌐 Web query:

Rust std::fs::File try_lock shared lock TryLockError stabilized 1.89 documentation

💡 Result:

The file locking API for std::fs::File, including the try_lock and try_lock_shared methods, was stabilized in Rust 1.89.0 [1][2][3]. This API introduces advisory file locking, allowing file handles to acquire either exclusive or shared locks [4]. The methods try_lock and try_lock_shared return a Result<, TryLockError> [1][5][6]. The TryLockError enum is also stabilized in 1.89.0 and includes the following variants [5][3]: - TryLockError::Error(io::Error): Indicates that the lock could not be acquired due to an I/O error [5][3]. - TryLockError::WouldBlock: Indicates that the lock could not be acquired because it is already held by another handle or process [5][3]. These methods and the associated error type are documented as stable features starting from Rust 1.89.0 [1][5][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Cargo manifest snippets =="
for f in Cargo.toml crates/fbuild-core/Cargo.toml; do
  if [ -f "$f" ]; then
    echo "--- $f"
    awk '{printf "%6d\t%s\n", NR, $0}' "$f" | sed -n '1,150p'
  fi
done

echo
echo "== file_lock.rs =="
if [ -f crates/fbuild-core/src/file_lock.rs ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' crates/fbuild-core/src/file_lock.rs | sed -n '1,180p'
fi

echo
echo "== search fs2/lock_is_held/try_lock_shared/lock_shared =="
rg -n 'fs2|lock_is_held|try_lock_shared|try_lock_exclusive|lock_share(d|exclusive)|TryLockError|FileExt' . --glob '*.rs' --glob 'Cargo.toml' || true

echo
echo "== local lock helper outline =="
if command -v rg >/dev/null 2>&1; then
  rg -n 'fn .*lock|struct .*Lock|lock\(|try_lock' crates/fbuild-core/src crates/fbuild-daemon/src | head -200 || true
fi

Repository: FastLED/fbuild

Length of output: 31166


Replace fs2 file locking with std::fs::File’s native lock API. Rust 1.89 adds stable try_lock/try_lock_shared with TryLockError::WouldBlock handling contention cross-platform, so crates/fbuild-core/src/file_lock.rs can use the std API and remove the hand-rolled lock_is_held classifier. Then remove fs2 from both Cargo.toml and crates/fbuild-core/Cargo.toml.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fbuild-core/src/file_lock.rs` at line 1, Update the file-locking
implementation in file_lock.rs to use std::fs::File native
try_lock/try_lock_shared APIs, handling TryLockError::WouldBlock for contention
and removing the hand-rolled lock_is_held classifier. Remove the fs2 dependency
from both workspace and fbuild-core Cargo.toml manifests while preserving
existing lock behavior.

Comment on lines +6 to +59
use fs2::FileExt;
use std::fs::{File, OpenOptions};
use std::io;
use std::path::Path;
use std::time::{Duration, Instant};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FileLockMode {
Shared,
Exclusive,
}

#[derive(Debug)]
pub struct FileLockGuard {
_file: File,
}

/// Try to acquire an OS-released lock on `path`.
///
/// Returns `Ok(None)` when another process holds a conflicting lock. The lock
/// is released automatically when the guard is dropped or the process exits.
pub fn try_acquire(path: &Path, mode: FileLockMode) -> io::Result<Option<FileLockGuard>> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(path)?;
let result = match mode {
FileLockMode::Shared => FileExt::try_lock_shared(&file),
FileLockMode::Exclusive => FileExt::try_lock_exclusive(&file),
};
match result {
Ok(()) => Ok(Some(FileLockGuard { _file: file })),
Err(error) if lock_is_held(&error) => Ok(None),
Err(error) => Err(error),
}
}

/// Does this error mean "another process holds a conflicting lock"?
///
/// Unix reports contention as `EWOULDBLOCK` (kind `WouldBlock`), but Windows
/// `LockFileEx(LOCKFILE_FAIL_IMMEDIATELY)` reports `ERROR_LOCK_VIOLATION`
/// (os error 33), which std maps to an uncategorized kind — so a kind check
/// alone misclassifies contention as a hard error on Windows. Also compare
/// against `fs2::lock_contended_error()` (the canonical per-platform
/// contention error), mirroring soldr's `lock_is_held`.
fn lock_is_held(error: &io::Error) -> bool {
error.kind() == io::ErrorKind::WouldBlock
|| error.raw_os_error() == fs2::lock_contended_error().raw_os_error()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant Rust std file-lock availability =="
python3 - <<'PY'
# Read-only comparison of the repository MSRV/toolchain baseline and stabilized std::fs::File locks.
import re, pathlib, textwrap
for f in [pathlib.Path("Cargo.toml"), *pathlib.Path(".").glob("crates/*/Cargo.toml")]:
    if f.exists():
        txt=f.read_text(errors="replace")
        for m in re.finditer(r'rust-version\s*=\s*"([^"]+)"|toolchain\s*=.*"([^"]+)"|min-supported-rust-version\s*=.*"([^"]+)"', txt):
            print(f"{f}: {m.group(0)}")
PY

echo
echo "== locate file_lock.rs and lock_is_held usage =="
git ls-files | rg '(^|/)file_lock\.rs$|Cargo\.toml$|Cargo\.lock$' || true
echo
rg -n "fs2::FileExt|fs2|lock_contested_error|lock_contended_error|TryLockError|fbuild-core" Cargo.toml crates -S --glob 'Cargo.toml' --glob '*.rs' || true
echo
echo "== file_lock.rs =="
cat -n crates/fbuild-core/src/file_lock.rs
echo
echo "== Rust std docs via cargo if available =="
if command -v rustc >/dev/null 2>&1; then rustc --version; rustc --print sysroot; fi

Repository: FastLED/fbuild

Length of output: 11635


🌐 Web query:

Rust std::fs::File try_lock try_lock_shared TryLockError WouldBlock stabilized 1.89.0

💡 Result:

The file locking API for std::fs::File, including the methods try_lock and try_lock_shared and the error enum TryLockError, was officially stabilized in Rust 1.89.0 [1][2][3]. These methods and the associated enum are part of the file_lock feature and provide cross-platform support for advisory file locking [1][4]. The try_lock and try_lock_shared methods return a Result<, TryLockError> [5][1]. The TryLockError enum is defined as follows [6][1]: pub enum TryLockError { Error(io::Error), WouldBlock, } The WouldBlock variant is returned when a lock cannot be acquired because it is currently held by another process or file handle [6][1]. The Error variant captures other I/O-related issues that may occur during the locking attempt [6][1]. This design replaces earlier proposals that used boolean return values, providing a more robust and less error-prone way to handle lock contention [7][4].

Citations:


Use std::fs::File’s native lock API instead of fs2.

Rust 1.89.0+ already provides try_lock(), try_lock_shared(), lock(), lock_shared(), and unlock() on std::fs::File, with TryLockError::WouldBlock unifying Unix/Windows lock contention. This removes the custom lock_is_held fallback and fs2 dependency from the file-lock path.

Also remove the fs2 workspace/Cargo dependency references after switching try_acquire to the std methods.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fbuild-core/src/file_lock.rs` around lines 6 - 59, Replace the
fs2-based locking in try_acquire with Rust 1.89+ std::fs::File methods, using
try_lock_shared or try_lock according to FileLockMode and mapping
TryLockError::WouldBlock to Ok(None) while propagating other errors. Remove
lock_is_held and the fs2 import, then delete the related fs2 dependency
references from the workspace and Cargo manifests.

Comment on lines +75 to +87
#[cfg(all(unix, not(target_os = "linux")))]
pub fn pid_executable_path(pid: u32) -> Option<PathBuf> {
let output = std::process::Command::new("/bin/ps")
.args(["-p", &pid.to_string(), "-o", "comm="])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let image = String::from_utf8(output.stdout).ok()?;
let image = image.trim();
(!image.is_empty()).then(|| PathBuf::from(image))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Direct subprocess spawn flagged by CI lint.

The pipeline's subprocess-spawn lint fails on this line: std::process::Command::new("/bin/ps") is a direct spawn without the required allow-direct-spawn: <reason> marker or routing through fbuild_core::subprocess::{run_command,run_command_passthrough}. It's also unbounded — no timeout guards the ps invocation.

🔧 Minimal fix: annotate with an allow-marker (or route through the sanctioned helper)
 pub fn pid_executable_path(pid: u32) -> Option<PathBuf> {
+    // allow-direct-spawn: reads a single process's `comm` name to verify PID
+    // identity before termination; no untrusted input, bounded one-shot call.
     let output = std::process::Command::new("/bin/ps")
         .args(["-p", &pid.to_string(), "-o", "comm="])
         .output()
         .ok()?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[cfg(all(unix, not(target_os = "linux")))]
pub fn pid_executable_path(pid: u32) -> Option<PathBuf> {
let output = std::process::Command::new("/bin/ps")
.args(["-p", &pid.to_string(), "-o", "comm="])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let image = String::from_utf8(output.stdout).ok()?;
let image = image.trim();
(!image.is_empty()).then(|| PathBuf::from(image))
}
#[cfg(all(unix, not(target_os = "linux")))]
pub fn pid_executable_path(pid: u32) -> Option<PathBuf> {
// allow-direct-spawn: reads a single process's `comm` name to verify PID
// identity before termination; no untrusted input, bounded one-shot call.
let output = std::process::Command::new("/bin/ps")
.args(["-p", &pid.to_string(), "-o", "comm="])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let image = String::from_utf8(output.stdout).ok()?;
let image = image.trim();
(!image.is_empty()).then(|| PathBuf::from(image))
}
🧰 Tools
🪛 GitHub Actions: Lint subprocess spawns / 0_Lint subprocess spawns.txt

[error] 77-77: Direct subprocess spawn without allow-direct-spawn: <reason> marker: std::process::Command::new("/bin/ps").

🪛 GitHub Actions: Lint subprocess spawns / Lint subprocess spawns

[error] 77-77: Direct subprocess spawn detected without required allow marker: std::process::Command::new("/bin/ps"). Annotate with a one-line reason (route via fbuild_core::subprocess::{run_command,run_command_passthrough} or add 'allow-direct-spawn: ').

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fbuild-core/src/process_identity.rs` around lines 75 - 87, Update
pid_executable_path to satisfy the subprocess-spawn lint by routing the /bin/ps
invocation through
fbuild_core::subprocess::{run_command,run_command_passthrough}, or add an
allow-direct-spawn marker with a concise reason if direct Command usage must
remain. Ensure the ps execution is bounded with the existing supported timeout
mechanism before processing its output.

Source: Pipeline failures

/// blocked, drops the lock, then confirms a second subprocess probe
/// succeeds.
#[test]
#[ignore = "subprocess helper for root_ownership_is_version_blind_across_processes"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

#[ignore] reason must cite a tracking issue or hardware/toolchain requirement.

The reason describes the helper's purpose but cites neither a tracking issue nor a concrete hardware/toolchain need, unlike the sibling ignores in legacy_daemon_transition.rs (which append (#1159)).

Proposed fix
-    #[ignore = "subprocess helper for root_ownership_is_version_blind_across_processes"]
+    #[ignore = "subprocess helper for root_ownership_is_version_blind_across_processes (`#1159`)"]

As per coding guidelines: "Every Rust #[ignore] attribute must include a reason string in the form #[ignore = "..."]; the reason must cite a tracking issue or a concrete hardware/toolchain requirement."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[ignore = "subprocess helper for root_ownership_is_version_blind_across_processes"]
#[ignore = "subprocess helper for root_ownership_is_version_blind_across_processes (`#1159`)"]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fbuild-paths/src/daemon_ownership.rs` at line 293, Update the
#[ignore] attribute for root_ownership_is_version_blind_across_processes to
include a valid tracking issue reference or concrete hardware/toolchain
requirement, following the sibling ignore annotations’ convention such as
(`#1159`).

Source: Coding guidelines

Comment on lines +309 to +320
let run_probe = |expected: &str| {
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--ignored",
"--exact",
"daemon_ownership::tests::subprocess_probe_root_owner",
"--nocapture",
])
.env("FBUILD_TEST_ROOT_OWNER_PATH", &path)
.env("FBUILD_TEST_ROOT_OWNER_EXPECT", expected)
.output()
.expect("run subprocess probe");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

CI lint failure: direct subprocess spawn needs an allow-direct-spawn marker.

The subprocess-spawn linter fails the build here because std::process::Command::new(std::env::current_exe().unwrap()) is spawned without the required marker (the sibling spawns in legacy_daemon_transition.rs already carry one). Add a one-line reason comment immediately above the spawn.

Proposed fix
         let run_probe = |expected: &str| {
+            // allow-direct-spawn: test driver re-invokes its own test binary as a root-owner probe.
             let output = std::process::Command::new(std::env::current_exe().unwrap())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let run_probe = |expected: &str| {
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--ignored",
"--exact",
"daemon_ownership::tests::subprocess_probe_root_owner",
"--nocapture",
])
.env("FBUILD_TEST_ROOT_OWNER_PATH", &path)
.env("FBUILD_TEST_ROOT_OWNER_EXPECT", expected)
.output()
.expect("run subprocess probe");
let run_probe = |expected: &str| {
// allow-direct-spawn: test driver re-invokes its own test binary as a root-owner probe.
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--ignored",
"--exact",
"daemon_ownership::tests::subprocess_probe_root_owner",
"--nocapture",
])
.env("FBUILD_TEST_ROOT_OWNER_PATH", &path)
.env("FBUILD_TEST_ROOT_OWNER_EXPECT", expected)
.output()
.expect("run subprocess probe");
🧰 Tools
🪛 GitHub Actions: Lint subprocess spawns / 0_Lint subprocess spawns.txt

[error] 310-310: Direct subprocess spawn without allow-direct-spawn: <reason> marker: std::process::Command::new(std::env::current_exe().unwrap()).

🪛 GitHub Actions: Lint subprocess spawns / Lint subprocess spawns

[error] 310-310: Direct subprocess spawn detected without required allow marker: std::process::Command::new(std::env::current_exe().unwrap()). Annotate with a one-line reason (route via fbuild_core::subprocess::{run_command,run_command_passthrough} or add 'allow-direct-spawn: ').

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fbuild-paths/src/daemon_ownership.rs` around lines 309 - 320, Add the
required allow-direct-spawn marker comment immediately above the
std::process::Command::new call inside the run_probe closure, including a
concise reason for this intentional subprocess test probe. Leave the existing
command arguments and environment setup unchanged.

Source: Pipeline failures

@fastled-project-sync fastled-project-sync Bot moved this to Triage in FastLED Tracker Jul 24, 2026
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.

handoff(#1154): finish cold-cache reset with soldr-style daemon ownership fix(bench): clear compiler caches before every cold build trial

1 participant