From 187928a919c7e8504902e8553737ba6b9e337a68 Mon Sep 17 00:00:00 2001 From: steven Date: Mon, 8 Jun 2026 20:08:59 -0600 Subject: [PATCH 1/4] feat: error when foundry binaries are in use --- src/install.rs | 2 ++ src/process.rs | 88 +++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 75 insertions(+), 15 deletions(-) diff --git a/src/install.rs b/src/install.rs index dafa50b..bf6359c 100644 --- a/src/install.rs +++ b/src/install.rs @@ -518,6 +518,8 @@ pub(crate) fn use_version(config: &Config, repo: &str, version: &str) -> Result< bail!("version {version} not installed for {repo}"); } + crate::process::check_bins_in_use(config)?; + for bin in config.network.bins { let bin_name = bin_name(bin); let src = version_dir.join(&bin_name); diff --git a/src/process.rs b/src/process.rs index 3602c5d..d969592 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,23 +1,81 @@ -use crate::{config::Config, warn}; -use eyre::Result; -use sysinfo::System; +use crate::config::Config; +use eyre::{Result, bail}; +use std::ffi::OsStr; +use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System}; pub(crate) fn check_bins_in_use(config: &Config) -> Result<()> { - let bins = config.network.bins; let mut sys = System::new(); - sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true); + // Refresh process names only, excluding per-process threads/tasks. + sys.refresh_processes_specifics( + ProcessesToUpdate::All, + true, + ProcessRefreshKind::nothing().without_tasks(), + ); - for (pid, proc) in sys.processes() { - if let Some(exe) = proc.exe().or_else(|| proc.cmd().first().map(AsRef::as_ref)) - && let Some(exe_fname) = exe.file_name() - && let Some(exe_fname) = exe_fname.to_str() - && let Some(bin) = bins.iter().find(|&&bin| exe_fname.starts_with(bin)) - { - warn!( - "'{bin}' is currently running (PID: {pid}), please stop the process and try again" - ); - } + let names = sys.processes().values().map(sysinfo::Process::name); + + if let Some(bin) = detect_in_use(config.network.bins, names) { + bail!("'{bin}' is currently running. Please stop the process and try again."); } Ok(()) } + +/// Returns the first binary in `bins` whose name exactly matches a running process. +fn detect_in_use<'a, 'n>( + bins: &[&'a str], + names: impl Iterator, +) -> Option<&'a str> { + let names: Vec<&OsStr> = names.collect(); + bins.iter().copied().find(|bin| names.iter().any(|&name| matches_bin(name, bin))) +} + +/// Exact name match, accepting the `.exe` extension on Windows. +fn matches_bin(name: &OsStr, bin: &str) -> bool { + if name == OsStr::new(bin) { + return true; + } + #[cfg(windows)] + if name == OsStr::new(&format!("{bin}.exe")) { + return true; + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + + const BINS: &[&str] = &["forge", "cast", "anvil", "chisel"]; + + fn detect(names: &[&str]) -> Option<&'static str> { + detect_in_use(BINS, names.iter().map(|n| OsStr::new(*n))) + } + + #[test] + fn detects_first_running_bin_in_bins_order() { + assert_eq!(detect(&["bash", "cast", "anvil", "rustc"]), Some("cast")); + } + + #[test] + fn requires_exact_match() { + assert_eq!(detect(&["forge-fmt", "castaway", "myanvil", "chiseling"]), None); + } + + #[test] + fn none_when_no_bins_running() { + assert_eq!(detect(&["bash", "node", "foundryup"]), None); + } + + #[cfg(windows)] + #[test] + fn matches_windows_exe_suffix() { + assert_eq!(detect(&["bash", "anvil.exe"]), Some("anvil")); + } + + #[cfg(not(windows))] + #[test] + fn ignores_exe_suffix_on_unix() { + assert_eq!(detect(&["bash", "anvil.exe"]), None); + } +} From 5281592d6576c0e534d6f37c4a89b0b601e50fbf Mon Sep 17 00:00:00 2001 From: steven Date: Tue, 9 Jun 2026 14:42:32 -0600 Subject: [PATCH 2/4] test: serialize install/use tests that run the global in-use binary check --- .config/nextest.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.config/nextest.toml b/.config/nextest.toml index 80724a0..1b3e457 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -1,3 +1,13 @@ [profile.default] retries = { backoff = "exponential", count = 2, delay = "5s", jitter = true } slow-timeout = { period = "30s", terminate-after = 3 } + +# Run the install/use tests one at a time. They invoke `check_bins_in_use`, +# which scans for running `forge`/`cast`/`anvil`/`chisel` processes globally, and +# they also spawn those binaries themselves, so they must not run concurrently. +[test-groups] +foundry-bins = { max-threads = 1 } + +[[profile.default.overrides]] +filter = 'test(/install_/) | test(reinstall_uses_cache) | test(use_version)' +test-group = 'foundry-bins' From f0437445df0c9ff59e3c1be0e453614bb7b4459f Mon Sep 17 00:00:00 2001 From: steven Date: Wed, 10 Jun 2026 01:06:20 -0600 Subject: [PATCH 3/4] test: group serialized in-use binary tests into `foundry_bins` module --- .config/nextest.toml | 2 +- tests/it/foundry_bins.rs | 113 +++++++++++++++++++++++++++++++++++++++ tests/it/main.rs | 109 +------------------------------------ 3 files changed, 116 insertions(+), 108 deletions(-) create mode 100644 tests/it/foundry_bins.rs diff --git a/.config/nextest.toml b/.config/nextest.toml index 1b3e457..22eee59 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -9,5 +9,5 @@ slow-timeout = { period = "30s", terminate-after = 3 } foundry-bins = { max-threads = 1 } [[profile.default.overrides]] -filter = 'test(/install_/) | test(reinstall_uses_cache) | test(use_version)' +filter = 'test(/^foundry_bins::/)' test-group = 'foundry-bins' diff --git a/tests/it/foundry_bins.rs b/tests/it/foundry_bins.rs new file mode 100644 index 0000000..3d78511 --- /dev/null +++ b/tests/it/foundry_bins.rs @@ -0,0 +1,113 @@ +//! Tests that exercise `check_bins_in_use`, which scans for running +//! `forge`/`cast`/`anvil`/`chisel` processes globally and also spawns those +//! binaries themselves. They must not run concurrently, so they live in this +//! module to be matched by a single nextest filter (see `.config/nextest.toml`). + +use super::*; +use std::path::Path; + +fn run_forge_test(foundry_dir: &Path, temp_dir: &Path) { + let forge = foundry_dir.join(format!("bin/forge{EXE_SUFFIX}")); + + Command::new(&forge).arg("--version").assert().success().stdout_eq(str![[r#" +forge [..] +... +"#]]); + + Command::new(&forge).args(["init", "test-project"]).current_dir(temp_dir).assert().success(); + let project_dir = temp_dir.join("test-project"); + + Command::new(&forge).arg("test").current_dir(&project_dir).assert().success(); +} + +fn test_install(version: &str) { + let temp_dir = tempfile::Builder::new().tempdir().unwrap(); + let foundry_dir = temp_dir.path().join(".foundry"); + + foundryup() + .env("FOUNDRY_DIR", &foundry_dir) + .args(["-i", version]) + .assert() + .success() + .stderr_eq(str![[r#" +... +[..]done! +... +"#]]); + + for &bin in BINS { + let name = format!("{bin}{EXE_SUFFIX}"); + assert!(foundry_dir.join("bin").join(&name).exists(), "{name} does not exist"); + } + + run_forge_test(&foundry_dir, temp_dir.path()); + + foundryup().env("FOUNDRY_DIR", &foundry_dir).arg("--list").assert().success().stderr_eq(str![ + [r#" +foundryup: foundry-rs/foundry [..] +foundryup: - forge [..] +foundryup: - cast [..] +foundryup: - anvil [..] +foundryup: - chisel [..] + +... +"#] + ]); +} + +#[test] +fn install_stable() { + test_install("stable"); +} +#[test] +fn install_nightly() { + test_install("nightly"); +} +#[test] +fn install_v1_5_0() { + test_install("v1.5.0"); +} +#[test] +fn install_1_5_0() { + test_install("1.5.0"); +} + +#[test] +fn use_version() { + let temp_dir = tempfile::Builder::new().tempdir().unwrap(); + let foundry_dir = temp_dir.path().join(".foundry"); + + foundryup().env("FOUNDRY_DIR", &foundry_dir).args(["-i", "stable"]).assert().success(); + + foundryup() + .env("FOUNDRY_DIR", &foundry_dir) + .args(["--use", "stable"]) + .assert() + .success() + .stderr_eq(str![[r#" +... +[..]use - forge [..] +... +"#]]); +} + +#[test] +fn reinstall_uses_cache() { + let temp_dir = tempfile::Builder::new().tempdir().unwrap(); + let foundry_dir = temp_dir.path().join(".foundry"); + + foundryup().env("FOUNDRY_DIR", &foundry_dir).args(["-i", "stable"]).assert().success(); + + foundryup() + .env("FOUNDRY_DIR", &foundry_dir) + .args(["-i", "stable"]) + .assert() + .success() + .stderr_eq(str![[r#" +... +[..]already installed and verified[..] +... +[..]done! +... +"#]]); +} diff --git a/tests/it/main.rs b/tests/it/main.rs index 761399d..c402d4c 100644 --- a/tests/it/main.rs +++ b/tests/it/main.rs @@ -1,8 +1,9 @@ use snapbox::{cmd::Command, str}; -use std::{env::consts::EXE_SUFFIX, path::Path}; +use std::env::consts::EXE_SUFFIX; const BINS: &[&str] = &["forge", "cast", "anvil", "chisel"]; +mod foundry_bins; mod installer_script; mod self_update; @@ -10,20 +11,6 @@ fn foundryup() -> Command { Command::new(snapbox::cmd::cargo_bin!("foundryup")).env("NO_COLOR", "1") } -fn run_forge_test(foundry_dir: &Path, temp_dir: &Path) { - let forge = foundry_dir.join(format!("bin/forge{EXE_SUFFIX}")); - - Command::new(&forge).arg("--version").assert().success().stdout_eq(str![[r#" -forge [..] -... -"#]]); - - Command::new(&forge).args(["init", "test-project"]).current_dir(temp_dir).assert().success(); - let project_dir = temp_dir.join("test-project"); - - Command::new(&forge).arg("test").current_dir(&project_dir).assert().success(); -} - #[test] fn help() { foundryup().arg("--help").assert().success().stdout_eq(str![[r#" @@ -188,95 +175,3 @@ foundryup: migrating legacy version [..] assert!(versions_dir.join("foundry-rs/foundry/nightly").exists()); assert!(versions_dir.join("foundry-rs/foundry/stable").exists()); } - -fn test_install(version: &str) { - let temp_dir = tempfile::Builder::new().tempdir().unwrap(); - let foundry_dir = temp_dir.path().join(".foundry"); - - foundryup() - .env("FOUNDRY_DIR", &foundry_dir) - .args(["-i", version]) - .assert() - .success() - .stderr_eq(str![[r#" -... -[..]done! -... -"#]]); - - for &bin in BINS { - let name = format!("{bin}{EXE_SUFFIX}"); - assert!(foundry_dir.join("bin").join(&name).exists(), "{name} does not exist"); - } - - run_forge_test(&foundry_dir, temp_dir.path()); - - foundryup().env("FOUNDRY_DIR", &foundry_dir).arg("--list").assert().success().stderr_eq(str![ - [r#" -foundryup: foundry-rs/foundry [..] -foundryup: - forge [..] -foundryup: - cast [..] -foundryup: - anvil [..] -foundryup: - chisel [..] - -... -"#] - ]); -} - -#[test] -fn install_stable() { - test_install("stable"); -} -#[test] -fn install_nightly() { - test_install("nightly"); -} -#[test] -fn install_v1_5_0() { - test_install("v1.5.0"); -} -#[test] -fn install_1_5_0() { - test_install("1.5.0"); -} - -#[test] -fn use_version() { - let temp_dir = tempfile::Builder::new().tempdir().unwrap(); - let foundry_dir = temp_dir.path().join(".foundry"); - - foundryup().env("FOUNDRY_DIR", &foundry_dir).args(["-i", "stable"]).assert().success(); - - foundryup() - .env("FOUNDRY_DIR", &foundry_dir) - .args(["--use", "stable"]) - .assert() - .success() - .stderr_eq(str![[r#" -... -[..]use - forge [..] -... -"#]]); -} - -#[test] -fn reinstall_uses_cache() { - let temp_dir = tempfile::Builder::new().tempdir().unwrap(); - let foundry_dir = temp_dir.path().join(".foundry"); - - foundryup().env("FOUNDRY_DIR", &foundry_dir).args(["-i", "stable"]).assert().success(); - - foundryup() - .env("FOUNDRY_DIR", &foundry_dir) - .args(["-i", "stable"]) - .assert() - .success() - .stderr_eq(str![[r#" -... -[..]already installed and verified[..] -... -[..]done! -... -"#]]); -} From 9f809495ae24846d26e795e38e65bc1e4d3b1af2 Mon Sep 17 00:00:00 2001 From: steven Date: Wed, 10 Jun 2026 09:13:30 -0600 Subject: [PATCH 4/4] style: cargo fmt Amp-Thread-ID: https://ampcode.com/threads/T-019eb215-3819-765c-a288-2f57069d8253 Co-authored-by: Amp --- tests/it/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/it/main.rs b/tests/it/main.rs index edd3b01..acf0cd6 100644 --- a/tests/it/main.rs +++ b/tests/it/main.rs @@ -243,4 +243,3 @@ foundryup: migrating legacy version [..] assert!(versions_dir.join("foundry-rs/foundry/nightly").exists()); assert!(versions_dir.join("foundry-rs/foundry/stable").exists()); } -