From 92959663c57f7ccb7909b163b6f9ecd145e01d27 Mon Sep 17 00:00:00 2001 From: branchseer Date: Tue, 4 Nov 2025 02:18:16 +0800 Subject: [PATCH 1/3] chore: add fspy::error::SpawnError for detailed error reporting --- Cargo.lock | 29 +++------- Cargo.toml | 4 +- crates/fspy/Cargo.toml | 2 + crates/fspy/examples/cli.rs | 4 +- crates/fspy/src/command.rs | 25 ++++---- crates/fspy/src/error.rs | 20 +++++++ crates/fspy/src/lib.rs | 2 + crates/fspy/src/unix/mod.rs | 14 +++-- crates/fspy/src/windows/mod.rs | 91 +++++++++++++++++------------- crates/fspy/tests/node_fs.rs | 13 ++--- crates/fspy/tests/rust_std.rs | 9 ++- crates/fspy/tests/rust_tokio.rs | 10 ++-- crates/fspy/tests/test_utils.rs | 7 +-- crates/vite_task/src/error.rs | 4 ++ crates/vite_task/src/test_utils.rs | 7 ++- 15 files changed, 138 insertions(+), 103 deletions(-) create mode 100644 crates/fspy/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 64db324d..ad98152a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -997,6 +997,7 @@ dependencies = [ "rand 0.9.2", "tar", "tempfile", + "thiserror 2.0.17", "tokio", "which", "winapi", @@ -1461,17 +1462,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - [[package]] name = "is_ci" version = "1.2.0" @@ -2966,29 +2956,26 @@ dependencies = [ [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -3008,9 +2995,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index 4da25d44..de93a270 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,8 +101,8 @@ tar = "0.4.43" tempfile = "3.14.0" test-log = { version = "0.2.18", features = ["trace"] } thiserror = "2" -tokio = "1.46.1" -tokio-util = "0.7.15" +tokio = "1.48.0" +tokio-util = "0.7.17" toml = "0.9.5" tracing = "0.1.41" tracing-error = "0.2.1" diff --git a/crates/fspy/Cargo.toml b/crates/fspy/Cargo.toml index d7016830..bd059218 100644 --- a/crates/fspy/Cargo.toml +++ b/crates/fspy/Cargo.toml @@ -16,6 +16,7 @@ libc = { workspace = true } ouroboros = { workspace = true } rand = { workspace = true } tempfile = { workspace = true } +thiserror = { workspace = true } tokio = { workspace = true, features = ["net", "process", "io-util", "sync"] } which = { workspace = true } xxhash-rust = { workspace = true } @@ -40,6 +41,7 @@ winsafe = { workspace = true } tempfile = { workspace = true } [dev-dependencies] +anyhow = { workspace = true } csv-async = { workspace = true } ctor = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros", "fs", "io-std"] } diff --git a/crates/fspy/examples/cli.rs b/crates/fspy/examples/cli.rs index 85c0d114..ebd5a085 100644 --- a/crates/fspy/examples/cli.rs +++ b/crates/fspy/examples/cli.rs @@ -1,4 +1,4 @@ -use std::{env::args_os, ffi::OsStr, io, path::PathBuf, pin::Pin}; +use std::{env::args_os, ffi::OsStr, path::PathBuf, pin::Pin}; use fspy::{AccessMode, TrackedChild}; use tokio::{ @@ -7,7 +7,7 @@ use tokio::{ }; #[tokio::main] -async fn main() -> io::Result<()> { +async fn main() -> anyhow::Result<()> { let mut args = args_os(); let _ = args.next(); assert_eq!(args.next().as_deref(), Some(OsStr::new("-o"))); diff --git a/crates/fspy/src/command.rs b/crates/fspy/src/command.rs index 18130f8a..56808f7d 100644 --- a/crates/fspy/src/command.rs +++ b/crates/fspy/src/command.rs @@ -1,7 +1,6 @@ use std::{ collections::HashMap, ffi::{OsStr, OsString}, - io, path::{Path, PathBuf}, process::Stdio, }; @@ -12,6 +11,7 @@ use tokio::process::Command as TokioCommand; use crate::{ TrackedChild, + error::SpawnError, os_impl::{self, spawn_impl}, }; @@ -145,13 +145,13 @@ impl Command { self } - pub async fn spawn(mut self) -> io::Result { + pub async fn spawn(mut self) -> Result { self.resolve_program()?; spawn_impl(self).await } /// Resolve program name to full path using `PATH` and cwd. - pub fn resolve_program(&mut self) -> io::Result<()> { + pub fn resolve_program(&mut self) -> Result<(), SpawnError> { let mut path_env: Option<&OsStr> = None; for (env_name, env_value) in &self.envs { let Some(env_name) = env_name.to_str() else { @@ -163,13 +163,18 @@ impl Command { } } - self.program = which::which_in( - self.program.as_os_str(), - path_env, - if let Some(cwd) = &self.cwd { cwd.clone() } else { std::env::current_dir()? }, - ) - .map_err(|err| io::Error::new(io::ErrorKind::NotFound, err))? - .into_os_string(); + let cwd = if let Some(cwd) = &self.cwd { + cwd.clone() + } else { + std::env::current_dir().expect("failed to get current dir") + }; + self.program = which::which_in(self.program.as_os_str(), path_env, &cwd) + .map_err(|err| SpawnError::WhichError { + program: self.program.clone(), + path: path_env.map(OsStr::to_owned), + cause: err, + })? + .into_os_string(); Ok(()) } diff --git a/crates/fspy/src/error.rs b/crates/fspy/src/error.rs new file mode 100644 index 00000000..0986f426 --- /dev/null +++ b/crates/fspy/src/error.rs @@ -0,0 +1,20 @@ +use std::ffi::OsString; + +#[derive(thiserror::Error, Debug)] +pub enum SpawnError { + #[error( + "could not resolve the full path of program '{program:?}' with PATH={path:?} under cwd() because: {cause}" + )] + WhichError { program: OsString, path: Option, cause: which::Error }, + + #[error("failed to create IPC channel: {0}")] + ChannelCreationError(std::io::Error), + + /// On unix systems, the injection happens before the spawn actually occurs on. + /// On Windows, the injection happens after the spawn but before resuming the process. + #[error("failed to prepare the command for injection: {0}")] + InjectionError(std::io::Error), + + #[error("underlying os error: {0}")] + OsSpawnError(std::io::Error), +} diff --git a/crates/fspy/src/lib.rs b/crates/fspy/src/lib.rs index 9cb22dc4..96aee7dd 100644 --- a/crates/fspy/src/lib.rs +++ b/crates/fspy/src/lib.rs @@ -4,6 +4,8 @@ // Persist the injected DLL/shared library somewhere in the filesystem. mod fixture; +pub mod error; + mod ipc; #[cfg(unix)] diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index bd2cc027..c6b93a57 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -24,6 +24,7 @@ use tokio::task::spawn_blocking; use crate::{ Command, TrackedChild, arena::PathAccessArena, + error::SpawnError, ipc::{OwnedReceiverLockGuard, SHM_CAPACITY}, }; @@ -82,11 +83,12 @@ impl PathAccessIterable { } } -pub(crate) async fn spawn_impl(mut command: Command) -> io::Result { +pub(crate) async fn spawn_impl(mut command: Command) -> Result { #[cfg(target_os = "linux")] let supervisor = supervise::()?; - let (ipc_channel_conf, ipc_receiver) = channel(SHM_CAPACITY)?; + let (ipc_channel_conf, ipc_receiver) = + channel(SHM_CAPACITY).map_err(SpawnError::ChannelCreationError)?; let payload = Payload { ipc_channel_conf, @@ -111,7 +113,8 @@ pub(crate) async fn spawn_impl(mut command: Command) -> io::Result |path_access| { exec_resolve_accesses.add(path_access); }, - )?; + ) + .map_err(|err| SpawnError::InjectionError(err.into()))?; command.set_exec(exec); let mut tokio_command = command.into_tokio_command(); @@ -128,7 +131,10 @@ pub(crate) async fn spawn_impl(mut command: Command) -> io::Result // tokio_command.spawn blocks while executing the `pre_exec` closure. // Run it inside spawn_blocking to avoid blocking the tokio runtime, especially the supervisor loop, // which needs to accept incoming connections while `pre_exec` is connecting to it. - let child = spawn_blocking(move || tokio_command.spawn()).await??; + let child = spawn_blocking(move || tokio_command.spawn()) + .await + .map_err(|err| SpawnError::OsSpawnError(err.into()))? + .map_err(SpawnError::OsSpawnError)?; let arenas_future = async move { let arenas = std::iter::once(exec_resolve_accesses); diff --git a/crates/fspy/src/windows/mod.rs b/crates/fspy/src/windows/mod.rs index e2ac3004..a39fdaa3 100644 --- a/crates/fspy/src/windows/mod.rs +++ b/crates/fspy/src/windows/mod.rs @@ -23,6 +23,7 @@ use xxhash_rust::const_xxh3::xxh3_128; use crate::{ TrackedChild, command::Command, + error::SpawnError, fixture::Fixture, ipc::{OwnedReceiverLockGuard, SHM_CAPACITY}, }; @@ -71,13 +72,14 @@ impl SpyInner { } } -pub(crate) async fn spawn_impl(command: Command) -> io::Result { +pub(crate) async fn spawn_impl(command: Command) -> Result { let asni_dll_path_with_nul = Arc::clone(&command.spy_inner.asni_dll_path_with_nul); let mut command = command.into_tokio_command(); command.creation_flags(CREATE_SUSPENDED); - let (channel_conf, receiver) = channel(SHM_CAPACITY)?; + let (channel_conf, receiver) = + channel(SHM_CAPACITY).map_err(SpawnError::ChannelCreationError)?; let accesses_future = async move { let ipc_receiver_lock_guard = OwnedReceiverLockGuard::lock_async(receiver).await?; @@ -87,43 +89,54 @@ pub(crate) async fn spawn_impl(command: Command) -> io::Result { // let path_access_stream = PathAccessIterable { pipe_receiver }; - let child = command.spawn_with(|std_command| { - let std_child = std_command.spawn()?; - - let mut dll_paths = asni_dll_path_with_nul.as_ptr().cast::(); - let process_handle = std_child.as_raw_handle().cast::(); - let success = unsafe { DetourUpdateProcessWithDll(process_handle, &mut dll_paths, 1) }; - if success != TRUE { - return Err(io::Error::last_os_error()); - } - - let payload = Payload { - channel_conf: channel_conf.clone(), - asni_dll_path_with_nul: asni_dll_path_with_nul.to_bytes(), - }; - let payload_bytes = bincode::encode_to_vec(payload, BINCODE_CONFIG).unwrap(); - let success = unsafe { - DetourCopyPayloadToProcess( - process_handle, - &PAYLOAD_ID, - payload_bytes.as_ptr().cast(), - payload_bytes.len().try_into().unwrap(), - ) - }; - if success != TRUE { - return Err(io::Error::last_os_error()); - } - - let main_thread_handle = std_child.main_thread_handle(); - let resume_thread_ret = - unsafe { ResumeThread(main_thread_handle.as_raw_handle().cast()) } as i32; - - if resume_thread_ret == -1 { - return Err(io::Error::last_os_error()); - } - - Ok(std_child) - })?; + let mut spawn_success = false; + let spawn_success = &mut spawn_success; + let child = command + .spawn_with(|std_command| { + let std_child = std_command.spawn()?; + *spawn_success = true; + + let mut dll_paths = asni_dll_path_with_nul.as_ptr().cast::(); + let process_handle = std_child.as_raw_handle().cast::(); + let success = unsafe { DetourUpdateProcessWithDll(process_handle, &mut dll_paths, 1) }; + if success != TRUE { + return Err(io::Error::last_os_error()); + } + + let payload = Payload { + channel_conf: channel_conf.clone(), + asni_dll_path_with_nul: asni_dll_path_with_nul.to_bytes(), + }; + let payload_bytes = bincode::encode_to_vec(payload, BINCODE_CONFIG).unwrap(); + let success = unsafe { + DetourCopyPayloadToProcess( + process_handle, + &PAYLOAD_ID, + payload_bytes.as_ptr().cast(), + payload_bytes.len().try_into().unwrap(), + ) + }; + if success != TRUE { + return Err(io::Error::last_os_error()); + } + + let main_thread_handle = std_child.main_thread_handle(); + let resume_thread_ret = + unsafe { ResumeThread(main_thread_handle.as_raw_handle().cast()) } as i32; + + if resume_thread_ret == -1 { + return Err(io::Error::last_os_error()); + } + + Ok(std_child) + }) + .map_err(|err| { + if !*spawn_success { + SpawnError::InjectionError(err.into()) + } else { + SpawnError::OsSpawnError(err.into()) + } + })?; Ok(TrackedChild { tokio_child: child, accesses_future }) } diff --git a/crates/fspy/tests/node_fs.rs b/crates/fspy/tests/node_fs.rs index ceaf0e8e..0a115afa 100644 --- a/crates/fspy/tests/node_fs.rs +++ b/crates/fspy/tests/node_fs.rs @@ -1,14 +1,11 @@ mod test_utils; -use std::{ - env::{current_dir, vars_os}, - io, -}; +use std::env::{current_dir, vars_os}; use fspy::{AccessMode, PathAccessIterable, TrackedChild}; use test_utils::assert_contains; -async fn track_node_script(script: &str) -> io::Result { +async fn track_node_script(script: &str) -> anyhow::Result { let mut command = fspy::Spy::global()?.new_command("node"); command .arg("-e") @@ -22,21 +19,21 @@ async fn track_node_script(script: &str) -> io::Result { } #[tokio::test] -async fn read_sync() -> io::Result<()> { +async fn read_sync() -> anyhow::Result<()> { let accesses = track_node_script("try { fs.readFileSync('hello') } catch {}").await?; assert_contains(&accesses, current_dir().unwrap().join("hello").as_path(), AccessMode::Read); Ok(()) } #[tokio::test] -async fn read_dir_sync() -> io::Result<()> { +async fn read_dir_sync() -> anyhow::Result<()> { let accesses = track_node_script("try { fs.readdirSync('.') } catch {}").await?; assert_contains(&accesses, ¤t_dir().unwrap(), AccessMode::ReadDir); Ok(()) } #[tokio::test] -async fn subprocess() -> io::Result<()> { +async fn subprocess() -> anyhow::Result<()> { let cmd = if cfg!(windows) { r"'cmd', ['/c', 'type hello']" } else { diff --git a/crates/fspy/tests/rust_std.rs b/crates/fspy/tests/rust_std.rs index 52e95feb..b6c3ae7c 100644 --- a/crates/fspy/tests/rust_std.rs +++ b/crates/fspy/tests/rust_std.rs @@ -3,7 +3,6 @@ mod test_utils; use std::{ env::current_dir, fs::{File, OpenOptions}, - io, path::Path, process::Stdio, }; @@ -12,7 +11,7 @@ use fspy::AccessMode; use test_utils::assert_contains; #[tokio::test] -async fn open_read() -> io::Result<()> { +async fn open_read() -> anyhow::Result<()> { let accesses = track_child!({ File::open("hello"); }) @@ -23,7 +22,7 @@ async fn open_read() -> io::Result<()> { } #[tokio::test] -async fn open_write() -> io::Result<()> { +async fn open_write() -> anyhow::Result<()> { let accesses = track_child!({ let path = format!("{}/hello", env!("CARGO_TARGET_TMPDIR")); OpenOptions::new().write(true).open(path); @@ -39,7 +38,7 @@ async fn open_write() -> io::Result<()> { } #[tokio::test] -async fn readdir() -> io::Result<()> { +async fn readdir() -> anyhow::Result<()> { let accesses = track_child!({ let path = format!("{}/hello", env!("CARGO_TARGET_TMPDIR")); std::fs::read_dir(path); @@ -55,7 +54,7 @@ async fn readdir() -> io::Result<()> { } #[tokio::test] -async fn subprocess() -> io::Result<()> { +async fn subprocess() -> anyhow::Result<()> { let accesses = track_child!({ let mut command = if cfg!(windows) { let mut command = std::process::Command::new("cmd"); diff --git a/crates/fspy/tests/rust_tokio.rs b/crates/fspy/tests/rust_tokio.rs index a267680e..a07a0bfb 100644 --- a/crates/fspy/tests/rust_tokio.rs +++ b/crates/fspy/tests/rust_tokio.rs @@ -1,13 +1,13 @@ mod test_utils; -use std::{env::current_dir, io, path::Path, process::Stdio}; +use std::{env::current_dir, path::Path, process::Stdio}; use fspy::AccessMode; use test_utils::assert_contains; use tokio::fs::OpenOptions; #[tokio::test] -async fn open_read() -> io::Result<()> { +async fn open_read() -> anyhow::Result<()> { let accesses = track_child!({ tokio::runtime::Builder::new_current_thread().enable_io().build().unwrap().block_on( async { @@ -22,7 +22,7 @@ async fn open_read() -> io::Result<()> { } #[tokio::test] -async fn open_write() -> io::Result<()> { +async fn open_write() -> anyhow::Result<()> { let accesses = track_child!({ let path = format!("{}/hello", env!("CARGO_TARGET_TMPDIR")); @@ -43,7 +43,7 @@ async fn open_write() -> io::Result<()> { } #[tokio::test] -async fn readdir() -> io::Result<()> { +async fn readdir() -> anyhow::Result<()> { let accesses = track_child!({ let path = format!("{}/hello", env!("CARGO_TARGET_TMPDIR")); @@ -64,7 +64,7 @@ async fn readdir() -> io::Result<()> { } #[tokio::test] -async fn subprocess() -> io::Result<()> { +async fn subprocess() -> anyhow::Result<()> { let accesses = track_child!({ tokio::runtime::Builder::new_current_thread().enable_io().build().unwrap().block_on( async { diff --git a/crates/fspy/tests/test_utils.rs b/crates/fspy/tests/test_utils.rs index 646bb6e2..f4448b71 100644 --- a/crates/fspy/tests/test_utils.rs +++ b/crates/fspy/tests/test_utils.rs @@ -1,7 +1,4 @@ -use std::{ - io, - path::{Path, PathBuf, StripPrefixError}, -}; +use std::path::{Path, PathBuf, StripPrefixError}; use fspy::{AccessMode, PathAccessIterable, TrackedChild}; @@ -55,7 +52,7 @@ macro_rules! track_child { }}; } -pub async fn _spawn_with_id(id: &str) -> io::Result { +pub async fn _spawn_with_id(id: &str) -> anyhow::Result { let mut command = fspy::Spy::global()?.new_command(::std::env::current_exe()?); command.arg(id); let TrackedChild { mut tokio_child, accesses_future } = command.spawn().await?; diff --git a/crates/vite_task/src/error.rs b/crates/vite_task/src/error.rs index 03c9fc20..aa239599 100644 --- a/crates/vite_task/src/error.rs +++ b/crates/vite_task/src/error.rs @@ -1,5 +1,6 @@ use std::{ffi::OsString, io, path::Path, sync::Arc}; +use fspy::error::SpawnError; use petgraph::algo::Cycle; use vite_path::{ AbsolutePath, RelativePathBuf, @@ -104,6 +105,9 @@ pub enum Error { #[cfg(unix)] #[error(transparent)] Nix(#[from] nix::Error), + + #[error("Failed to spawn task because: {0}")] + SpawnError(#[from] SpawnError), } impl From> for Error { diff --git a/crates/vite_task/src/test_utils.rs b/crates/vite_task/src/test_utils.rs index bcaf00e1..a30f561a 100644 --- a/crates/vite_task/src/test_utils.rs +++ b/crates/vite_task/src/test_utils.rs @@ -1,4 +1,4 @@ -use vite_path::{AbsolutePath, AbsolutePathBuf}; +use vite_path::{AbsolutePath, AbsolutePathBuf, current_dir}; use vite_str::format; pub fn with_unique_cache_path(test_name: &str, f: F) -> R @@ -21,5 +21,8 @@ where } pub fn get_fixture_path(rel_path: &str) -> AbsolutePathBuf { - AbsolutePath::new(env!("CARGO_MANIFEST_DIR")).unwrap().join(rel_path) + // The current dir is the manifest dir of the crate being tested. + // We don't use `env!("CARGO_MANIFEST_DIR")` because we want the test binary to be relocatable, + // so it can be cross-compiled and then run in a different os. + current_dir().unwrap().join(rel_path) } From d94714c1337121588e750a84f8ecd4737b8a373a Mon Sep 17 00:00:00 2001 From: branchseer Date: Tue, 4 Nov 2025 02:29:01 +0800 Subject: [PATCH 2/3] add SupervisorError --- crates/fspy/src/error.rs | 3 +++ crates/fspy/src/unix/mod.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/fspy/src/error.rs b/crates/fspy/src/error.rs index 0986f426..b74c7a0f 100644 --- a/crates/fspy/src/error.rs +++ b/crates/fspy/src/error.rs @@ -7,6 +7,9 @@ pub enum SpawnError { )] WhichError { program: OsString, path: Option, cause: which::Error }, + #[error("failed to initialize seccomp_unotify supervisor: {0}")] + SupervisorError(std::io::Error), + #[error("failed to create IPC channel: {0}")] ChannelCreationError(std::io::Error), diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index c6b93a57..d4c9a223 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -85,7 +85,7 @@ impl PathAccessIterable { pub(crate) async fn spawn_impl(mut command: Command) -> Result { #[cfg(target_os = "linux")] - let supervisor = supervise::()?; + let supervisor = supervise::().map_err(SpawnError::SupervisorError)?; let (ipc_channel_conf, ipc_receiver) = channel(SHM_CAPACITY).map_err(SpawnError::ChannelCreationError)?; From 8c590ece5c521ad46a25c5edf5182306c8223368 Mon Sep 17 00:00:00 2001 From: branchseer Date: Tue, 4 Nov 2025 13:52:21 +0800 Subject: [PATCH 3/3] update error --- crates/fspy/src/command.rs | 1 + crates/fspy/src/error.rs | 12 +++++++++--- crates/vite_task/src/error.rs | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/fspy/src/command.rs b/crates/fspy/src/command.rs index 56808f7d..c50b7ab5 100644 --- a/crates/fspy/src/command.rs +++ b/crates/fspy/src/command.rs @@ -172,6 +172,7 @@ impl Command { .map_err(|err| SpawnError::WhichError { program: self.program.clone(), path: path_env.map(OsStr::to_owned), + cwd, cause: err, })? .into_os_string(); diff --git a/crates/fspy/src/error.rs b/crates/fspy/src/error.rs index b74c7a0f..8c8f83b0 100644 --- a/crates/fspy/src/error.rs +++ b/crates/fspy/src/error.rs @@ -1,11 +1,17 @@ -use std::ffi::OsString; +use std::{ffi::OsString, path::PathBuf}; #[derive(thiserror::Error, Debug)] pub enum SpawnError { #[error( - "could not resolve the full path of program '{program:?}' with PATH={path:?} under cwd() because: {cause}" + "could not resolve the full path of program '{program:?}' with PATH={path:?} under cwd({cwd:?})" )] - WhichError { program: OsString, path: Option, cause: which::Error }, + WhichError { + program: OsString, + path: Option, + cwd: PathBuf, + #[source] + cause: which::Error, + }, #[error("failed to initialize seccomp_unotify supervisor: {0}")] SupervisorError(std::io::Error), diff --git a/crates/vite_task/src/error.rs b/crates/vite_task/src/error.rs index aa239599..1aba67eb 100644 --- a/crates/vite_task/src/error.rs +++ b/crates/vite_task/src/error.rs @@ -106,7 +106,7 @@ pub enum Error { #[error(transparent)] Nix(#[from] nix::Error), - #[error("Failed to spawn task because: {0}")] + #[error("Failed to spawn task")] SpawnError(#[from] SpawnError), }