Skip to content

Commit ba48c0e

Browse files
authored
refactor: make TrackedChild easier to use (#20)
# refactor: make `TrackedChild` easier to use This PR refactors the `TrackedChild` struct to make it easier to use by: - Adding a `ChildTermination` struct that contains both the exit status and path accesses - Providing a `wait_handle` future that resolves to `ChildTermination` when the process exits - Exposing stdin, stdout, and stderr handles directly on `TrackedChild` - Automatically locking the IPC channel when the child process exits - Updating all usages of `TrackedChild` throughout the codebase
2 parents 79359e1 + 163ab7d commit ba48c0e

File tree

9 files changed

+160
-125
lines changed

9 files changed

+160
-125
lines changed

crates/fspy/examples/cli.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{env::args_os, ffi::OsStr, path::PathBuf, pin::Pin};
22

3-
use fspy::{AccessMode, TrackedChild};
3+
use fspy::AccessMode;
44
use tokio::{
55
fs::File,
66
io::{AsyncWrite, stdout},
@@ -21,19 +21,16 @@ async fn main() -> anyhow::Result<()> {
2121
let mut command = spy.new_command(program);
2222
command.envs(std::env::vars_os()).args(args);
2323

24-
let TrackedChild { mut tokio_child, accesses_future } = command.spawn().await?;
25-
26-
let output = tokio_child.wait().await?;
27-
28-
let accesses = accesses_future.await?;
24+
let child = command.spawn().await?;
25+
let termination = child.wait_handle.await?;
2926

3027
let mut path_count = 0usize;
3128
let out_file: Pin<Box<dyn AsyncWrite>> =
3229
if out_path == "-" { Box::pin(stdout()) } else { Box::pin(File::create(out_path).await?) };
3330

3431
let mut csv_writer = csv_async::AsyncWriter::from_writer(out_file);
3532

36-
for acc in accesses.iter() {
33+
for acc in termination.path_accesses.iter() {
3734
path_count += 1;
3835
csv_writer
3936
.write_record(&[
@@ -49,6 +46,6 @@ async fn main() -> anyhow::Result<()> {
4946
}
5047
csv_writer.flush().await?;
5148

52-
eprintln!("\nfspy: {path_count} paths accessed. {output}");
49+
eprintln!("\nfspy: {path_count} paths accessed. status: {}", termination.status);
5350
Ok(())
5451
}

crates/fspy/src/lib.rs

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

23-
use std::{env::temp_dir, ffi::OsStr, fs::create_dir, io, sync::OnceLock};
23+
use std::{env::temp_dir, ffi::OsStr, fs::create_dir, io, process::ExitStatus, sync::OnceLock};
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;
2929
use os_impl::SpyInner;
30-
use tokio::process::Child;
30+
use tokio::process::{ChildStderr, ChildStdin, ChildStdout};
31+
32+
/// The result of a tracked child process upon its termination.
33+
pub struct ChildTermination {
34+
/// The exit status of the child process.
35+
pub status: ExitStatus,
36+
/// The path accesses captured from the child process.
37+
pub path_accesses: PathAccessIterable,
38+
}
3139

3240
pub struct TrackedChild {
33-
pub tokio_child: Child,
34-
/// This future lazily locks the IPC channel when it's polled.
35-
/// Do not `await` it until the child process has exited.
36-
pub accesses_future: BoxFuture<'static, io::Result<PathAccessIterable>>,
41+
/// The handle for writing to the child's standard input (stdin), if it has
42+
/// been captured.
43+
pub stdin: Option<ChildStdin>,
44+
45+
/// The handle for reading from the child's standard output (stdout), if it
46+
/// has been captured.
47+
pub stdout: Option<ChildStdout>,
48+
49+
/// The handle for reading from the child's standard error (stderr), if it
50+
/// has been captured.
51+
pub stderr: Option<ChildStderr>,
52+
53+
/// The future that resolves to exit status and path accesses when the process exits.
54+
pub wait_handle: BoxFuture<'static, io::Result<ChildTermination>>,
3755
}
3856

3957
pub struct Spy(SpyInner);

crates/fspy/src/unix/mod.rs

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use syscall_handler::SyscallHandler;
2222
use tokio::task::spawn_blocking;
2323

2424
use crate::{
25-
Command, TrackedChild,
25+
ChildTermination, Command, TrackedChild,
2626
arena::PathAccessArena,
2727
error::SpawnError,
2828
ipc::{OwnedReceiverLockGuard, SHM_CAPACITY},
@@ -131,26 +131,35 @@ pub(crate) async fn spawn_impl(mut command: Command) -> Result<TrackedChild, Spa
131131
// tokio_command.spawn blocks while executing the `pre_exec` closure.
132132
// Run it inside spawn_blocking to avoid blocking the tokio runtime, especially the supervisor loop,
133133
// which needs to accept incoming connections while `pre_exec` is connecting to it.
134-
let child = spawn_blocking(move || tokio_command.spawn())
134+
let mut child = spawn_blocking(move || tokio_command.spawn())
135135
.await
136136
.map_err(|err| SpawnError::OsSpawnError(err.into()))?
137137
.map_err(SpawnError::OsSpawnError)?;
138138

139-
let arenas_future = async move {
140-
let arenas = std::iter::once(exec_resolve_accesses);
141-
#[cfg(target_os = "linux")]
142-
let arenas =
143-
arenas.chain(supervisor.stop().await?.into_iter().map(|handler| handler.into_arena()));
144-
io::Result::Ok(arenas.collect::<Vec<_>>())
145-
};
146-
147-
let accesses_future = async move {
148-
let arenas = arenas_future.await?;
149-
// `receiver.lock()` blocks. Run it inside `spawn_blocking` to avoid blocking the tokio runtime.
150-
let ipc_receiver_lock_guard = OwnedReceiverLockGuard::lock_async(ipc_receiver).await?;
151-
Ok(PathAccessIterable { arenas, ipc_receiver_lock_guard })
152-
}
153-
.boxed();
154-
155-
Ok(TrackedChild { tokio_child: child, accesses_future })
139+
Ok(TrackedChild {
140+
stdin: child.stdin.take(),
141+
stdout: child.stdout.take(),
142+
stderr: child.stderr.take(),
143+
// Keep polling for the child to exit in the background even if `wait_handle` is not awaited,
144+
// because we need to stop the supervisor and lock the channel as soon as the child exits.
145+
wait_handle: tokio::spawn(async move {
146+
let status = child.wait().await?;
147+
148+
let arenas = std::iter::once(exec_resolve_accesses);
149+
// Stop the supervisor and collect path accesses from it.
150+
#[cfg(target_os = "linux")]
151+
let arenas = arenas
152+
.chain(supervisor.stop().await?.into_iter().map(|handler| handler.into_arena()));
153+
let arenas = arenas.collect::<Vec<_>>();
154+
155+
// Lock the ipc channel after the child has exited.
156+
// We are not interested in path accesses from descendants after the main child has exited.
157+
let ipc_receiver_lock_guard = OwnedReceiverLockGuard::lock_async(ipc_receiver).await?;
158+
let path_accesses = PathAccessIterable { arenas, ipc_receiver_lock_guard };
159+
160+
io::Result::Ok(ChildTermination { status, path_accesses })
161+
})
162+
.map(|f| io::Result::Ok(f??)) // flatten JoinError and io::Result
163+
.boxed(),
164+
})
156165
}

crates/fspy/src/windows/mod.rs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use winsafe::co::{CP, WC};
2121
use xxhash_rust::const_xxh3::xxh3_128;
2222

2323
use crate::{
24-
TrackedChild,
24+
ChildTermination, TrackedChild,
2525
command::Command,
2626
error::SpawnError,
2727
fixture::Fixture,
@@ -81,17 +81,9 @@ pub(crate) async fn spawn_impl(command: Command) -> Result<TrackedChild, SpawnEr
8181
let (channel_conf, receiver) =
8282
channel(SHM_CAPACITY).map_err(SpawnError::ChannelCreationError)?;
8383

84-
let accesses_future = async move {
85-
let ipc_receiver_lock_guard = OwnedReceiverLockGuard::lock_async(receiver).await?;
86-
io::Result::Ok(PathAccessIterable { ipc_receiver_lock_guard })
87-
}
88-
.boxed();
89-
90-
// let path_access_stream = PathAccessIterable { pipe_receiver };
91-
9284
let mut spawn_success = false;
9385
let spawn_success = &mut spawn_success;
94-
let child = command
86+
let mut child = command
9587
.spawn_with(|std_command| {
9688
let std_child = std_command.spawn()?;
9789
*spawn_success = true;
@@ -138,5 +130,22 @@ pub(crate) async fn spawn_impl(command: Command) -> Result<TrackedChild, SpawnEr
138130
}
139131
})?;
140132

141-
Ok(TrackedChild { tokio_child: child, accesses_future })
133+
Ok(TrackedChild {
134+
stdin: child.stdin.take(),
135+
stdout: child.stdout.take(),
136+
stderr: child.stderr.take(),
137+
// Keep polling for the child to exit in the background even if `wait_handle` is not awaited,
138+
// because we need to stop the supervisor and lock the channel as soon as the child exits.
139+
wait_handle: tokio::spawn(async move {
140+
let status = child.wait().await?;
141+
// Lock the ipc channel after the child has exited.
142+
// We are not interested in path accesses from descendants after the main child has exited.
143+
let ipc_receiver_lock_guard = OwnedReceiverLockGuard::lock_async(receiver).await?;
144+
let path_accesses = PathAccessIterable { ipc_receiver_lock_guard };
145+
146+
io::Result::Ok(ChildTermination { status, path_accesses })
147+
})
148+
.map(|f| io::Result::Ok(f??)) // flatten JoinError and io::Result
149+
.boxed(),
150+
})
142151
}

crates/fspy/tests/node_fs.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ mod test_utils;
22

33
use std::env::{current_dir, vars_os};
44

5-
use fspy::{AccessMode, PathAccessIterable, TrackedChild};
5+
use fspy::{AccessMode, PathAccessIterable};
66
use test_utils::assert_contains;
77

88
async fn track_node_script(script: &str) -> anyhow::Result<PathAccessIterable> {
@@ -11,11 +11,10 @@ async fn track_node_script(script: &str) -> anyhow::Result<PathAccessIterable> {
1111
.arg("-e")
1212
.envs(vars_os()) // https://github.com/jdx/mise/discussions/5968
1313
.arg(script);
14-
let TrackedChild { mut tokio_child, accesses_future } = command.spawn().await?;
15-
let status = tokio_child.wait().await?;
16-
let accesses = accesses_future.await?;
17-
assert!(status.success());
18-
Ok(accesses)
14+
let child = command.spawn().await?;
15+
let termination = child.wait_handle.await?;
16+
assert!(termination.status.success());
17+
Ok(termination.path_accesses)
1918
}
2019

2120
#[tokio::test]

crates/fspy/tests/static_executable.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ async fn track_test_bin(args: &[&str], cwd: Option<&str>) -> PathAccessIterable
4141
cmd.current_dir(cwd);
4242
};
4343
cmd.args(args);
44-
let mut tracked_child = cmd.spawn().await.unwrap();
44+
let tracked_child = cmd.spawn().await.unwrap();
4545

46-
let output = tracked_child.tokio_child.wait().await.unwrap();
47-
assert!(output.success());
46+
let termination = tracked_child.wait_handle.await.unwrap();
47+
assert!(termination.status.success());
4848

49-
tracked_child.accesses_future.await.unwrap()
49+
termination.path_accesses
5050
}
5151

5252
#[tokio::test]

crates/fspy/tests/test_utils.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::path::{Path, PathBuf, StripPrefixError};
22

3-
use fspy::{AccessMode, PathAccessIterable, TrackedChild};
3+
use fspy::{AccessMode, PathAccessIterable};
44

55
#[track_caller]
66
pub fn assert_contains(
@@ -55,10 +55,7 @@ macro_rules! track_child {
5555
pub async fn _spawn_with_id(id: &str) -> anyhow::Result<PathAccessIterable> {
5656
let mut command = fspy::Spy::global()?.new_command(::std::env::current_exe()?);
5757
command.arg(id);
58-
let TrackedChild { mut tokio_child, accesses_future } = command.spawn().await?;
59-
60-
let status = tokio_child.wait().await?;
61-
let accesses = accesses_future.await?;
62-
assert!(status.success());
63-
Ok(accesses)
58+
let termination = command.spawn().await?.wait_handle.await?;
59+
assert!(termination.status.success());
60+
Ok(termination.path_accesses)
6461
}

crates/fspy_e2e/src/main.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use std::{
99

1010
use fspy::{AccessMode, PathAccess};
1111
use serde::{Deserialize, Serialize};
12+
use tokio::io::AsyncReadExt;
1213

1314
#[derive(Serialize, Deserialize)]
1415
struct Config {
@@ -86,25 +87,30 @@ async fn main() {
8687
.stderr(Stdio::piped())
8788
.current_dir(&dir);
8889

89-
let tracked_child = cmd.spawn().await.unwrap();
90+
let mut tracked_child = cmd.spawn().await.unwrap();
9091

91-
let output = tracked_child.tokio_child.wait_with_output().await.unwrap();
92-
let accesses = tracked_child.accesses_future.await.unwrap();
92+
let mut stdout_bytes = Vec::<u8>::new();
93+
tracked_child.stdout.take().unwrap().read_to_end(&mut stdout_bytes).await.unwrap();
9394

94-
if !output.status.success() {
95+
let mut stderr_bytes = Vec::<u8>::new();
96+
tracked_child.stderr.take().unwrap().read_to_end(&mut stderr_bytes).await.unwrap();
97+
98+
let termination = tracked_child.wait_handle.await.unwrap();
99+
100+
if !termination.status.success() {
95101
eprintln!("----- stdout begin -----");
96-
stderr().write_all(&output.stdout).unwrap();
102+
stderr().write_all(&stdout_bytes).unwrap();
97103
eprintln!("----- stdout end -----");
98104
eprintln!("----- stderr begin-----");
99-
stderr().write_all(&output.stderr).unwrap();
105+
stderr().write_all(&stderr_bytes).unwrap();
100106
eprintln!("----- stderr end -----");
101107

102-
eprintln!("Case `{}` failed with status: {}", name, output.status);
108+
eprintln!("Case `{}` failed with status: {}", name, termination.status);
103109
process::exit(1);
104110
}
105111

106112
let mut collector = AccessCollector::new(dir);
107-
for access in accesses.iter() {
113+
for access in termination.path_accesses.iter() {
108114
collector.add(access);
109115
}
110116
let snap_file = File::create(manifest_dir.join(format!("snaps/{name}.txt"))).unwrap();

0 commit comments

Comments
 (0)