Skip to content

Commit 4545045

Browse files
authored
refactor: simplify fspy::Command API and add pre_exec hook (#23)
### TL;DR Refactored the `fspy` crate to simplify the API by removing the `Spy` struct and making `Command` the primary entry point. ### What changed? - Removed the `Spy` struct and replaced it with a global `SPY_IMPL` static using `LazyLock` - Made `Command::new()` the primary entry point for creating commands instead of `Spy::new_command()` - Renamed `SpyInner` to `SpyImpl` and moved spawn implementation into this struct - Added `pre_exec` support to `Command` on Unix platforms - Made most `Command` fields private with proper accessors - Added `derive_more` dependency for better Debug implementations - Updated all tests and examples to use the new API ### How to test? - Run the existing test suite to verify the refactored API works correctly - Try creating and running commands with the new API: ```rust let mut command = fspy::Command::new("program"); command.args(["arg1", "arg2"]); let child = command.spawn().await?; ``` ### Why make this change? This refactoring simplifies the API by removing an unnecessary abstraction layer. The `Spy` struct was mostly a wrapper around creating commands, and having a global instance was redundant. By making `Command` the primary entry point and using a global implementation internally, the API becomes more intuitive and follows patterns similar to standard libraries. The change also improves encapsulation by making fields private and adding proper accessors.
1 parent d66556a commit 4545045

File tree

16 files changed

+270
-254
lines changed

16 files changed

+270
-254
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ crossterm = { version = "0.29.0", features = ["event-stream"] }
5454
csv-async = { version = "1.3.1", features = ["tokio"] }
5555
ctor = "0.6"
5656
dashmap = "6.1.0"
57+
derive_more = "2.0.1"
5758
diff-struct = "0.5.3"
5859
directories = "6.0.0"
5960
elf = { version = "0.8.0", default-features = false }
@@ -124,8 +125,14 @@ winsafe = { version = "0.0.24", features = ["kernel"] }
124125
xxhash-rust = { version = "0.8.15", features = ["const_xxh3"] }
125126

126127
[workspace.metadata.cargo-shear]
127-
# These are artifact dependencies. They are not directly `use`d in Rust code.
128-
ignored = ["fspy_preload_unix", "fspy_preload_windows", "fspy_test_bin"]
128+
ignored = [
129+
# These are artifact dependencies. They are not directly `use`d in Rust code.
130+
"fspy_preload_unix",
131+
"fspy_preload_windows",
132+
"fspy_test_bin",
133+
# used in a macro in crates/fspy_test_utils/src/lib.rs
134+
"ctor",
135+
]
129136

130137
[profile.dev]
131138
# Disabling debug info speeds up local and CI builds,

crates/fspy/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ bincode = { workspace = true }
1010
bstr = { workspace = true, default-features = false }
1111
bumpalo = { workspace = true }
1212
const_format = { workspace = true, features = ["fmt"] }
13+
derive_more = { workspace = true, features = ["debug"] }
1314
fspy_shared = { workspace = true }
15+
fspy_test_utils = { workspace = true }
1416
futures-util = { workspace = true }
1517
libc = { workspace = true }
1618
ouroboros = { workspace = true }

crates/fspy/examples/cli.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ async fn main() -> anyhow::Result<()> {
1616

1717
let program = PathBuf::from(args.next().unwrap());
1818

19-
let spy = fspy::Spy::global()?;
20-
21-
let mut command = spy.new_command(program);
19+
let mut command = fspy::Command::new(program);
2220
command.envs(std::env::vars_os()).args(args);
2321

2422
let child = command.spawn().await?;

crates/fspy/src/command.rs

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,49 @@ use std::{
99
use fspy_shared_unix::exec::Exec;
1010
use tokio::process::Command as TokioCommand;
1111

12-
use crate::{
13-
TrackedChild,
14-
error::SpawnError,
15-
os_impl::{self, spawn_impl},
16-
};
12+
use crate::{SPY_IMPL, TrackedChild, error::SpawnError};
1713

18-
#[derive(Debug)]
14+
#[derive(derive_more::Debug)]
1915
pub struct Command {
20-
pub(crate) program: OsString,
21-
pub(crate) args: Vec<OsString>,
22-
pub(crate) envs: HashMap<OsString, OsString>,
23-
pub(crate) cwd: Option<PathBuf>,
16+
program: OsString,
17+
args: Vec<OsString>,
18+
envs: HashMap<OsString, OsString>,
19+
cwd: Option<PathBuf>,
2420
#[cfg(unix)]
25-
pub(crate) arg0: Option<OsString>,
21+
arg0: Option<OsString>,
2622

27-
pub(crate) stderr: Option<Stdio>,
28-
pub(crate) stdout: Option<Stdio>,
29-
pub(crate) stdin: Option<Stdio>,
23+
stderr: Option<Stdio>,
24+
stdout: Option<Stdio>,
25+
stdin: Option<Stdio>,
3026

31-
pub(crate) spy_inner: os_impl::SpyInner,
27+
#[cfg(unix)]
28+
#[debug("({} pre_exec closures)", pre_exec_closures.len())]
29+
pre_exec_closures: Vec<Box<dyn FnMut() -> std::io::Result<()> + Send + Sync>>,
3230
}
3331

3432
impl Command {
33+
/// Create a new command to spy on the given program.
34+
/// Initially, environment variables are not inherited from the parent.
35+
/// To inherit, explicitly use `.envs(std::env::vars_os())`.
36+
pub fn new<P: AsRef<OsStr>>(program: P) -> Self {
37+
Self {
38+
program: program.as_ref().to_os_string(),
39+
args: Vec::new(),
40+
envs: HashMap::new(),
41+
cwd: None,
42+
#[cfg(unix)]
43+
arg0: None,
44+
stderr: None,
45+
stdout: None,
46+
stdin: None,
47+
#[cfg(unix)]
48+
pre_exec_closures: Vec::new(),
49+
}
50+
}
51+
3552
#[cfg(unix)]
3653
#[must_use]
37-
pub fn get_exec(&self) -> Exec {
54+
pub(crate) fn get_exec(&self) -> Exec {
3855
use std::{
3956
iter::once,
4057
os::unix::ffi::{OsStrExt, OsStringExt},
@@ -57,7 +74,7 @@ impl Command {
5774
}
5875

5976
#[cfg(unix)]
60-
pub fn set_exec(&mut self, mut exec: Exec) {
77+
pub(crate) fn set_exec(&mut self, mut exec: Exec) {
6178
use std::os::unix::ffi::OsStringExt;
6279

6380
self.program = OsString::from_vec(exec.program.into());
@@ -147,7 +164,7 @@ impl Command {
147164

148165
pub async fn spawn(mut self) -> Result<TrackedChild, SpawnError> {
149166
self.resolve_program()?;
150-
spawn_impl(self).await
167+
SPY_IMPL.spawn(self).await
151168
}
152169

153170
/// Resolve program name to full path using `PATH` and cwd.
@@ -179,7 +196,22 @@ impl Command {
179196
Ok(())
180197
}
181198

182-
pub(crate) fn into_tokio_command(self) -> TokioCommand {
199+
/// Schedules a closure to be run just before the exec function is invoked.
200+
///
201+
/// # Safety
202+
///
203+
/// <https://doc.rust-lang.org/1.91.1/std/os/unix/process/trait.CommandExt.html#tymethod.pre_exec>
204+
#[cfg(unix)]
205+
pub unsafe fn pre_exec<F>(&mut self, f: F) -> &mut Self
206+
where
207+
F: FnMut() -> std::io::Result<()> + Send + Sync + 'static,
208+
{
209+
self.pre_exec_closures.push(Box::new(f));
210+
self
211+
}
212+
213+
/// Convert to a `tokio::process::Command` without tracking.
214+
pub fn into_tokio_command(self) -> TokioCommand {
183215
let mut tokio_cmd = TokioCommand::new(self.program);
184216
if let Some(cwd) = &self.cwd {
185217
tokio_cmd.current_dir(cwd);
@@ -205,6 +237,12 @@ impl Command {
205237
tokio_cmd.stderr(stderr);
206238
}
207239

240+
#[cfg(unix)]
241+
for pre_exec in self.pre_exec_closures {
242+
// Safety: The caller of `pre_exec` is responsible for ensuring safety.
243+
unsafe { tokio_cmd.pre_exec(pre_exec) };
244+
}
245+
208246
tokio_cmd
209247
}
210248
}

crates/fspy/src/lib.rs

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ mod os_impl;
2020
mod arena;
2121
mod command;
2222

23-
use std::{env::temp_dir, ffi::OsStr, fs::create_dir, io, process::ExitStatus, sync::OnceLock};
23+
use std::{env::temp_dir, fs::create_dir, io, process::ExitStatus, sync::LazyLock};
2424

2525
pub use command::Command;
2626
pub use fspy_shared::ipc::{AccessMode, PathAccess};
2727
use futures_util::future::BoxFuture;
2828
pub use os_impl::PathAccessIterable;
29-
use os_impl::SpyInner;
29+
use os_impl::SpyImpl;
3030
use tokio::process::{ChildStderr, ChildStdin, ChildStdout};
3131

3232
/// The result of a tracked child process upon its termination.
@@ -54,33 +54,8 @@ pub struct TrackedChild {
5454
pub wait_handle: BoxFuture<'static, io::Result<ChildTermination>>,
5555
}
5656

57-
pub struct Spy(SpyInner);
58-
impl Spy {
59-
pub fn new() -> io::Result<Self> {
60-
let tmp_dir = temp_dir().join("fspy");
61-
let _ = create_dir(&tmp_dir);
62-
Ok(Self(SpyInner::init_in(&tmp_dir)?))
63-
}
64-
65-
pub fn global() -> io::Result<&'static Self> {
66-
static GLOBAL_SPY: OnceLock<Spy> = OnceLock::new();
67-
GLOBAL_SPY.get_or_try_init(Self::new)
68-
}
69-
70-
pub fn new_command<S: AsRef<OsStr>>(&self, program: S) -> Command {
71-
Command {
72-
program: program.as_ref().to_os_string(),
73-
envs: Default::default(),
74-
args: vec![],
75-
cwd: None,
76-
#[cfg(unix)]
77-
arg0: None,
78-
spy_inner: self.0.clone(),
79-
stderr: None,
80-
stdout: None,
81-
stdin: None,
82-
}
83-
}
84-
}
85-
86-
// pub use fspy_shared::ipc::*;
57+
pub(crate) static SPY_IMPL: LazyLock<SpyImpl> = LazyLock::new(|| {
58+
let tmp_dir = temp_dir().join("fspy");
59+
let _ = create_dir(&tmp_dir);
60+
SpyImpl::init_in(&tmp_dir).expect("Failed to initialize global spy")
61+
});

0 commit comments

Comments
 (0)