From f2c89067727d8a956f3a7bc9f3a6da2568c6d085 Mon Sep 17 00:00:00 2001 From: zackees Date: Thu, 20 Aug 2026 04:38:02 -0700 Subject: [PATCH] fix(paths): avoid Windows-excluded daemon ports --- CLAUDE.md | 2 +- crates/CLAUDE.md | 2 +- crates/fbuild-paths/README.md | 2 +- crates/fbuild-paths/src/lib.rs | 40 +++++++++++++++++++++++++++------- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e5171efe3..213966691 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -139,7 +139,7 @@ The current inventory is auto-published to a stable tracking issue every Monday ## Key Constraints - **No file-based locks, with one sanctioned exception** — almost all synchronization is through the daemon's in-memory managers. The narrow exception is fbuild-daemon startup/lifetime root-ownership and spawn-herd election (soldr-style, FastLED/fbuild#1159): a version-blind `root-owner.lock` held for the daemon's whole lifetime, plus a `spawn.lock` single-flight election for concurrent CLI spawns. zccache compile/object access keeps its own zccache-internal in-memory synchronization — these locks never gate cache reads/writes. Locks are OS-released on process death and must never be manually broken or deleted. -- **Dev mode isolation** — `FBUILD_DEV_MODE=1` → `~/.fbuild/dev/`. The daemon endpoint is no longer a fixed port: it's derived per (backend version + cache identity) in the IANA dynamic range 49152–65535 (`fbuild_paths::default_daemon_port` / `daemon_endpoint_key`), so different-version checkouts get isolated daemons and can't serve each other wrong-version builds (FastLED/fbuild#1009). Override with `FBUILD_DAEMON_PORT`. +- **Dev mode isolation** — `FBUILD_DEV_MODE=1` → `~/.fbuild/dev/`. The daemon endpoint is no longer a fixed port: it's derived per (backend version + cache identity) in 10000–49151 on Windows (below commonly excluded dynamic ports; FastLED/fbuild#1322) and the IANA dynamic range 49152–65535 elsewhere (`fbuild_paths::default_daemon_port` / `daemon_endpoint_key`), so different-version checkouts get isolated daemons and can't serve each other wrong-version builds (FastLED/fbuild#1009). Override with `FBUILD_DAEMON_PORT`. - **HTTP API compatibility** — same endpoints and JSON schemas as the Python daemon - **Windows USB-CDC** — 30 retries, aggressive buffer drain, DTR/RTS toggling after flash - **Emulator CLI convention** — prefer `fbuild test-emu` for CI; `fbuild deploy --to emu [--emulator ]` for interactive use; keep `--target` and `--qemu` only as compatibility aliases diff --git a/crates/CLAUDE.md b/crates/CLAUDE.md index 154186d07..98493cd6e 100644 --- a/crates/CLAUDE.md +++ b/crates/CLAUDE.md @@ -59,7 +59,7 @@ fbuild-test-support (test utilities) ────────────── - **fbuild-core** — `FbuildError`/`Result`, `BuildProfile`, `Platform`, `SizeInfo`, `DaemonState`. USB identity catalogues are fetched from FastLED/boards and must never be generated or embedded here outside test fixtures. - **fbuild-config** — `PlatformIOConfig` (INI parser with `extends` inheritance), `BoardConfig`, `McuSpec` -- **fbuild-paths** — Dev/prod path isolation (`~/.fbuild/{dev|prod}/`), version+identity-keyed daemon endpoint (`daemon_endpoint_key`/`default_daemon_port`, dynamic range 49152–65535; FastLED/fbuild#1009), cache dirs +- **fbuild-paths** — Dev/prod path isolation (`~/.fbuild/{dev|prod}/`), version+identity-keyed daemon endpoint (`daemon_endpoint_key`/`default_daemon_port`, Windows 10000–49151 to avoid excluded dynamic ports, 49152–65535 elsewhere; FastLED/fbuild#1009), cache dirs - **fbuild-packages** — URL-based package downloads, toolchain resolution, library manager, parallel pipeline - **fbuild-serial** — `SharedSerialManager` (centralized serial I/O), deploy preemption protocol, WebSocket messages, USB-CDC retry logic - **fbuild-build** — `BuildOrchestrator` trait, per-platform orchestrators (AVR, ESP32, ESP8266, RP2040, STM32, Teensy, WASM) diff --git a/crates/fbuild-paths/README.md b/crates/fbuild-paths/README.md index 2edd54f46..5b943ff5d 100644 --- a/crates/fbuild-paths/README.md +++ b/crates/fbuild-paths/README.md @@ -9,7 +9,7 @@ Single source of truth for all `.fbuild` directory paths, with dev/prod isolatio - `get_other_fbuild_root()` -- Returns the opposite mode's root (for cross-mode daemon discovery) - `get_daemon_dir()` / `get_daemon_pid_file()` / `get_daemon_port_file()` / `get_daemon_log_file()` / `get_daemon_status_file()` -- Daemon file paths - `daemon_ownership` module -- `RootOwnershipGuard` (version-blind, per-cache-root exclusive lock at `root-owner.lock`, held by the daemon for its whole lifetime; `fbuild clean cache` takes it exclusively before deleting the zccache store), `SpawnLockGuard` (`spawn.lock` single-flight election so concurrent CLI spawns don't race), and `OwnerClaim`/`write_owner_claim()`/`read_owner_claim()`/`remove_owner_claim()` (a `root-owner.json` claim recording pid/exe/version/mode/cache_root_key/port, written after the daemon acquires ownership and knows its port; never authoritative on its own — always verified against pid liveness + exe identity before acting on it) -- `get_daemon_port()` -- Port resolution with four-level priority: env var, current mode port file, cross-mode port file, default (8865 dev / 8765 prod) +- `get_daemon_port()` -- Port resolution priority: explicit environment override, live endpoint-keyed port file, then a deterministic version/cache-identity default (10000–49151 on Windows to avoid dynamic exclusion blocks; 49152–65535 elsewhere) - `get_daemon_url()` -- Daemon HTTP URL (`http://127.0.0.1:{port}`) - `get_cache_root()` -- Global cache dir (`FBUILD_CACHE_DIR` override or `~/.fbuild/{mode}/cache`) - `get_project_build_root()` -- Per-project build-dir root (`FBUILD_BUILD_DIR` override or `/.fbuild/build`). Returns the *root* — does not append `/`. Most callers should use `BuildLayout` instead. diff --git a/crates/fbuild-paths/src/lib.rs b/crates/fbuild-paths/src/lib.rs index 6ad1fc322..ed3a3a510 100644 --- a/crates/fbuild-paths/src/lib.rs +++ b/crates/fbuild-paths/src/lib.rs @@ -77,19 +77,29 @@ fn endpoint_key_from_material(material: &str) -> String { /// Deterministic default daemon port derived from [`daemon_endpoint_key`]. /// -/// Lands in the IANA dynamic range (49152–65535) so it never collides with a -/// well-known service, and is stable for a given version+identity. Distinct -/// versions (and dev vs prod, since mode is part of the identity) get distinct -/// ports. `FBUILD_DAEMON_PORT` still overrides this (see [`get_daemon_port`]). +/// On Windows this lands in 10000–49151, below the dynamic range that +/// Hyper-V/HNS commonly reserves in excluded blocks. Other platforms use the +/// IANA dynamic range (49152–65535). The result is stable for a given +/// version+identity; distinct versions (and dev vs prod, since mode is part of +/// the identity) get distinct ports. `FBUILD_DAEMON_PORT` still overrides this +/// (see [`get_daemon_port`]). pub fn default_daemon_port() -> u16 { port_from_endpoint_key(&daemon_endpoint_key()) } -/// Map a hex endpoint key into the IANA dynamic port window (49152–65535). -/// Pure + deterministic (unit-tested). +/// Map a hex endpoint key into the platform's default daemon-port window. +/// Pure and deterministic (unit-tested). fn port_from_endpoint_key(key: &str) -> u16 { + #[cfg(windows)] + const LOW: u32 = 10000; + #[cfg(windows)] + const SPAN: u32 = 49152 - LOW; + + #[cfg(not(windows))] const LOW: u32 = 49152; - const SPAN: u32 = 65536 - LOW; // 16384 + #[cfg(not(windows))] + const SPAN: u32 = 65536 - LOW; + let n = u64::from_str_radix(key, 16).unwrap_or(0); (LOW + (n % u64::from(SPAN)) as u32) as u16 } @@ -516,6 +526,7 @@ mod tests { assert!(live.chars().all(|c| c.is_ascii_hexdigit())); } + #[cfg(not(windows))] #[test] fn default_daemon_port_is_in_dynamic_range() { let p = default_daemon_port(); @@ -525,16 +536,29 @@ mod tests { ); } + #[cfg(windows)] + #[test] + fn default_daemon_port_avoids_windows_dynamic_exclusion_range() { + let p = default_daemon_port(); + assert!( + (10000..49152).contains(&p), + "Windows daemon port {p} overlaps the dynamic exclusion range" + ); + } + #[test] fn port_from_key_is_deterministic_and_ranged() { // Same key → same port; keys differing (e.g. by version or checkout) - // map into the dynamic range and generally differ. + // map into the platform range and generally differ. assert_eq!( port_from_endpoint_key("0123456789abcdef"), port_from_endpoint_key("0123456789abcdef") ); for key in ["0000000000000000", "ffffffffffffffff", "deadbeefcafef00d"] { let p = port_from_endpoint_key(key); + #[cfg(windows)] + assert!((10000..49152).contains(&p), "key {key} → {p} out of range"); + #[cfg(not(windows))] assert!((49152..=65535).contains(&p), "key {key} → {p} out of range"); } // Two distinct version/identity keys should not collapse to one port