diff --git a/Cargo.lock b/Cargo.lock index d0f05fd19..2fe9ccbf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -968,7 +968,7 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fbuild-bench-fastled-examples" -version = "2.5.42" +version = "2.5.43" dependencies = [ "fbuild-core", "fbuild-library-select", @@ -982,7 +982,7 @@ dependencies = [ [[package]] name = "fbuild-build" -version = "2.5.42" +version = "2.5.43" dependencies = [ "async-trait", "blake3", @@ -1019,7 +1019,7 @@ dependencies = [ [[package]] name = "fbuild-build-arm" -version = "2.5.42" +version = "2.5.43" dependencies = [ "async-trait", "blake3", @@ -1053,7 +1053,7 @@ dependencies = [ [[package]] name = "fbuild-build-engine" -version = "2.5.42" +version = "2.5.43" dependencies = [ "async-trait", "blake3", @@ -1087,7 +1087,7 @@ dependencies = [ [[package]] name = "fbuild-build-esp" -version = "2.5.42" +version = "2.5.43" dependencies = [ "async-trait", "blake3", @@ -1122,7 +1122,7 @@ dependencies = [ [[package]] name = "fbuild-build-mcu" -version = "2.5.42" +version = "2.5.43" dependencies = [ "async-trait", "blake3", @@ -1156,7 +1156,7 @@ dependencies = [ [[package]] name = "fbuild-cli" -version = "2.5.42" +version = "2.5.43" dependencies = [ "blake3", "clap", @@ -1188,7 +1188,7 @@ dependencies = [ [[package]] name = "fbuild-config" -version = "2.5.42" +version = "2.5.43" dependencies = [ "fbuild-core", "fbuild-paths", @@ -1203,7 +1203,7 @@ dependencies = [ [[package]] name = "fbuild-core" -version = "2.5.42" +version = "2.5.43" dependencies = [ "async-trait", "fs2", @@ -1232,7 +1232,7 @@ dependencies = [ [[package]] name = "fbuild-daemon" -version = "2.5.42" +version = "2.5.43" dependencies = [ "async-trait", "axum 0.7.9", @@ -1271,7 +1271,7 @@ dependencies = [ [[package]] name = "fbuild-deploy" -version = "2.5.42" +version = "2.5.43" dependencies = [ "async-trait", "espflash", @@ -1299,7 +1299,7 @@ dependencies = [ [[package]] name = "fbuild-header-scan" -version = "2.5.42" +version = "2.5.43" dependencies = [ "criterion", "fbuild-paths", @@ -1310,7 +1310,7 @@ dependencies = [ [[package]] name = "fbuild-library" -version = "2.5.42" +version = "2.5.43" dependencies = [ "async-trait", "axum 0.7.9", @@ -1342,7 +1342,7 @@ dependencies = [ [[package]] name = "fbuild-library-select" -version = "2.5.42" +version = "2.5.43" dependencies = [ "bincode", "blake3", @@ -1363,7 +1363,7 @@ dependencies = [ [[package]] name = "fbuild-packages" -version = "2.5.42" +version = "2.5.43" dependencies = [ "async-trait", "axum 0.7.9", @@ -1395,7 +1395,7 @@ dependencies = [ [[package]] name = "fbuild-packages-fetch" -version = "2.5.42" +version = "2.5.43" dependencies = [ "async-trait", "axum 0.7.9", @@ -1424,7 +1424,7 @@ dependencies = [ [[package]] name = "fbuild-paths" -version = "2.5.42" +version = "2.5.43" dependencies = [ "blake3", "fbuild-core", @@ -1436,7 +1436,7 @@ dependencies = [ [[package]] name = "fbuild-python" -version = "2.5.42" +version = "2.5.43" dependencies = [ "base64", "fbuild-core", @@ -1460,7 +1460,7 @@ dependencies = [ [[package]] name = "fbuild-serial" -version = "2.5.42" +version = "2.5.43" dependencies = [ "async-trait", "base64", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "fbuild-test-support" -version = "2.5.42" +version = "2.5.43" dependencies = [ "fbuild-config", "fbuild-core", @@ -1503,7 +1503,7 @@ dependencies = [ [[package]] name = "fbuild-toolchain" -version = "2.5.42" +version = "2.5.43" dependencies = [ "async-trait", "axum 0.7.9", diff --git a/Cargo.toml b/Cargo.toml index 98558afa9..687090ca3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,7 +93,7 @@ targets = [ ] [workspace.package] -version = "2.5.42" +version = "2.5.43" edition = "2021" rust-version = "1.95.0" license = "AGPL-3.0-only" diff --git a/ci/platform_boundary_research.tsv b/ci/platform_boundary_research.tsv index bbf7143cf..d27207355 100644 --- a/ci/platform_boundary_research.tsv +++ b/ci/platform_boundary_research.tsv @@ -74,7 +74,7 @@ crates/fbuild-core/src/platform/windows/usb_pnp.rs 40 native_path windows_sys:: crates/fbuild-core/tests/it/platformio_package_resolution.rs 5 attr_cfg #[cfg(windows)] host host_mechanic crates/fbuild-core/tests/it/platformio_package_resolution.rs 107 attr_cfg #[cfg(windows)] host host_mechanic crates/fbuild-daemon/Cargo.toml 74 native_dependency libc process host_mechanic -crates/fbuild-daemon/src/handlers/emulator/tests_npm_cache.rs 146 attr_cfg #[cfg(windows)] host_executable host_artifact_policy +crates/fbuild-daemon/src/handlers/emulator/tests_npm_cache.rs 224 attr_cfg #[cfg(windows)] host_executable host_artifact_policy crates/fbuild-daemon/src/handlers/emulator/tests_process.rs 9 attr_cfg #[cfg(windows)] host_executable host_artifact_policy crates/fbuild-daemon/src/handlers/emulator/tests_process.rs 21 attr_cfg #[cfg(not(windows))] host_executable host_artifact_policy crates/fbuild-daemon/src/handlers/health.rs 51 attr_cfg #[cfg(target_os=)] host host_mechanic diff --git a/crates/fbuild-daemon/src/handlers/emulator/avr8js_npm.rs b/crates/fbuild-daemon/src/handlers/emulator/avr8js_npm.rs index 0012aaf09..b5ea135c0 100644 --- a/crates/fbuild-daemon/src/handlers/emulator/avr8js_npm.rs +++ b/crates/fbuild-daemon/src/handlers/emulator/avr8js_npm.rs @@ -7,31 +7,33 @@ use std::path::{Path, PathBuf}; pub(crate) async fn find_node() -> fbuild_core::Result { let node = fbuild_core::platform::executable::name("node", "node.exe"); - // Route through fbuild-core's `run_command` so the probe spawn is - // captured by the daemon's containment group (issue #32). The probe - // is short-lived (`node --version`) but a missing binary should - // still bubble up the same way. - // - // FastLED/fbuild#844: explicit 5 s timeout. `node --version` is a - // sub-100ms operation on every supported host — anything longer is a - // wedged process node and we want to fall through to the - // "not found" error path quickly so the user sees the install - // hint instead of a stalled emulator launch. - match fbuild_core::subprocess::run_command( - &[node, "--version"], + // nodejs-wheel exposes a Python launcher on PATH. Spawning that launcher + // for emulation means Child::kill stops Python while native Node retains + // the output pipes. Ask Node for its own executable so the runner owns + // the emulator process directly (#1694). Keep discovery bounded and + // contained through the existing subprocess boundary (#32, #844). + let executable = match fbuild_core::subprocess::run_command( + &[node, "--print", "process.execPath"], None, None, Some(std::time::Duration::from_secs(5)), ) .await { - Ok(output) if output.success() => Ok(PathBuf::from(node)), - _ => Err(fbuild_core::FbuildError::DeployFailed( + Ok(output) if output.success() => PathBuf::from(output.stdout.trim()), + _ => return Err(fbuild_core::FbuildError::DeployFailed( "Node.js is required for headless avr8js emulation but 'node' was not found on PATH. \ Install Node.js 18+ from https://nodejs.org/" .to_string(), )), + }; + if !executable.is_absolute() { + return Err(fbuild_core::FbuildError::DeployFailed(format!( + "Node.js returned an invalid executable path: {}", + executable.display() + ))); } + Ok(executable) } /// Env-var that forces `ensure_avr8js_npm` to wipe and reinstall the cache diff --git a/crates/fbuild-daemon/src/handlers/emulator/tests_npm_cache.rs b/crates/fbuild-daemon/src/handlers/emulator/tests_npm_cache.rs index f34061e64..bc71322b2 100644 --- a/crates/fbuild-daemon/src/handlers/emulator/tests_npm_cache.rs +++ b/crates/fbuild-daemon/src/handlers/emulator/tests_npm_cache.rs @@ -23,6 +23,84 @@ async fn env_lock() -> tokio::sync::MutexGuard<'static, ()> { LOCK.get_or_init(|| Mutex::new(())).lock().await } +#[tokio::test] +#[ignore = "requires Node.js on PATH; run under nodejs-wheel's uv environment for launcher regression"] +async fn node_discovery_returns_native_executable() { + let _guard = env_lock().await; + let node = super::avr8js_npm::find_node().await.unwrap(); + assert!( + node.is_absolute(), + "must resolve the native executable, not a PATH launcher" + ); + let output = fbuild_core::subprocess::run_command( + &[node.to_str().unwrap(), "--print", "process.execPath"], + None, + None, + Some(std::time::Duration::from_secs(5)), + ) + .await + .unwrap(); + assert!(output.success()); + assert_eq!(node.as_path(), std::path::Path::new(output.stdout.trim())); +} + +#[tokio::test] +#[ignore = "requires Node.js on PATH; exercises native process shutdown without npm or firmware"] +async fn resolved_node_headless_shutdown() { + use super::avr8js_headless::{RunAvr8jsHeadlessOptions, run_avr8js_headless}; + use crate::handlers::operations::MonitorOutcome; + + let _guard = env_lock().await; + let node = super::avr8js_npm::find_node().await.unwrap(); + assert!(node.is_absolute()); + let temp = tempfile::tempdir_in(fbuild_paths::temp_subdir("daemon-tests")).unwrap(); + let cache = temp.path(); + let module = cache.join("node_modules/avr8js"); + std::fs::create_dir_all(&module).unwrap(); + std::fs::write(module.join("package.json"), "{}").unwrap(); + let script = cache.join("headless.mjs"); + let hex = cache.join("unused.hex"); + std::fs::write( + &script, + "console.log('READY'); setInterval(() => {}, 1000);", + ) + .unwrap(); + for (success, error) in [(Some("READY"), None), (None, Some("READY")), (None, None)] { + let run = run_avr8js_headless( + &node, + &script, + &hex, + 16_000_000, + cache, + RunAvr8jsHeadlessOptions { + timeout_secs: Some(if success.is_some() || error.is_some() { + 5.0 + } else { + 1.0 + }), + halt_on_error: error, + halt_on_success: success, + expect: None, + show_timestamp: false, + verbose: false, + }, + ); + let result = tokio::time::timeout(std::time::Duration::from_secs(10), run) + .await + .expect("native Node shutdown must finish") + .unwrap(); + if success.is_some() { + assert!(matches!(result.outcome, MonitorOutcome::Success(_))); + assert!(result.stdout.contains("READY")); + } else if error.is_some() { + assert!(matches!(result.outcome, MonitorOutcome::Error(_))); + assert!(result.stdout.contains("READY")); + } else { + assert!(matches!(result.outcome, MonitorOutcome::Timeout { .. })); + } + } +} + #[test] fn avr8js_cache_is_intact_detects_missing_dir() { let tmp = tempfile::TempDir::new().unwrap(); diff --git a/pyproject.toml b/pyproject.toml index c7a4b5e8d..81d51e520 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fbuild" -version = "2.5.42" +version = "2.5.43" description = "PlatformIO-compatible embedded build tool (Rust implementation)" readme = "README.md" license = "AGPL-3.0-only"