diff --git a/README.md b/README.md index 04a0599..dfbb2be 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,16 @@ process-wrap = { version = "10.0.0", features = ["tokio1"] } By default, the crate does nothing, you need to enable either the std or Tokio "frontend". A default set of wrappers are enabled; you may choose to only compile those you need, see [the features list]. +Both frontends use the same process-wrap `Command` configuration API. The frontend selected by the +module controls spawning and the child contract: std child operations block, while Tokio child +operations are asynchronous. `CommandWrap` remains an alias for compatibility. Enabling both +frontends exposes both `process_wrap::std::Command` and `process_wrap::tokio::Command` without one +taking precedence. + ```rust use process_wrap::tokio::*; -let mut child = CommandWrap::with_new("watch", |command| { command.arg("ls"); }) +let mut child = Command::with_new("watch", |command| { command.arg("ls"); }) .wrap(ProcessGroup::leader()) .spawn()?; let status = child.wait().await?; @@ -48,7 +54,7 @@ dbg!(status); ```rust use process_wrap::tokio::*; -let mut child = CommandWrap::with_new("watch", |command| { command.arg("ls"); }) +let mut child = Command::with_new("watch", |command| { command.arg("ls"); }) .wrap(JobObject) .spawn()?; let status = child.wait().await?; @@ -60,7 +66,7 @@ dbg!(status); ```rust use process_wrap::tokio::*; -let mut child = CommandWrap::with_new("watch", |command| { command.arg("ls"); }) +let mut child = Command::with_new("watch", |command| { command.arg("ls"); }) .wrap(ProcessSession) .spawn()?; let status = child.wait().await?; @@ -72,7 +78,7 @@ dbg!(status); ```rust use process_wrap::tokio::*; -let mut child = CommandWrap::with_new("watch", |command| { command.arg("ls"); }) +let mut child = Command::with_new("watch", |command| { command.arg("ls"); }) .wrap(ProcessSession) .wrap(KillOnDrop) .spawn()?; @@ -90,13 +96,38 @@ process-wrap = { version = "10.0.0", features = ["std"] } ```rust use process_wrap::std::*; -let mut child = CommandWrap::with_new("watch", |command| { command.arg("ls"); }) +let mut child = Command::with_new("watch", |command| { command.arg("ls"); }) .wrap(ProcessGroup::leader()) .spawn()?; let status = child.wait()?; dbg!(status); ``` +### Native command compatibility + +Commands built through `Command::new`, `Command::with_new`, and the process-wrap configuration +methods retain exact portable command intent and create a fresh native command for every spawn +attempt. The `with_new` closure now receives process-wrap's `Command`; inferred calls such as +`command.arg(...)` continue unchanged. + +`Command::from(native_command)` preserves an existing std or Tokio command as native-only state. +`native_mut()` and non-reconstructable configuration such as arbitrary `Stdio` do the same. +Native-only commands retain exact ordinary spawning and `spawn_with*` behavior, but alternate +portable transports cannot recover raw argument tags, environment-clear history, `pre_exec` +callbacks, or arbitrary native handles and will reject that state. Use `into_native()` when the rest +of the lifecycle belongs to the native API. + +The facade keeps native stable configuration methods where their behavior can be preserved, including +Unix identity setup, the standard frontend's process-group setter, Windows creation flags, and Tokio +kill-on-drop. These platform-specific operations make the command native-only. The standard library's +supplementary-groups setter remains unstable and is not mirrored by the facade. Tokio's process-group +setter is also omitted at the declared Tokio floor because it cannot exactly replace process-group +state already stored in a native-only Tokio command. Use the `ProcessGroup` wrapper for tracked Tokio +commands, or configure a `std::process::Command` before converting it into Tokio and then process-wrap. +An immutable Tokio `as_std()` requires the explicit `command.native_mut().as_std()` transition. Tokio +1.38.2 does not expose mutable access to its inner standard command, so use the same conversion path +when that escape is needed. + ## Wrappers ### Job object @@ -106,7 +137,7 @@ dbg!(status); - Feature: `job-object` (default) ```rust -CommandWrap::with_new("watch", |command| { command.arg("ls"); }) +Command::with_new("watch", |command| { command.arg("ls"); }) .wrap(JobObject) .spawn()?; ``` @@ -122,7 +153,7 @@ after assignment unless the caller explicitly requested `CREATE_SUSPENDED`. - Feature: `process-group` (default) ```rust -CommandWrap::with_new("watch", |command| { command.arg("ls"); }) +Command::with_new("watch", |command| { command.arg("ls"); }) .wrap(ProcessGroup::leader()) .spawn()?; ``` @@ -130,7 +161,7 @@ CommandWrap::with_new("watch", |command| { command.arg("ls"); }) Or join a different group instead: ```rust -CommandWrap::with_new("watch", |command| { command.arg("ls"); }) +Command::with_new("watch", |command| { command.arg("ls"); }) .wrap(ProcessGroup::attach_to(pgid)) .spawn()?; ``` @@ -147,7 +178,7 @@ This combines creating a new session and a new group, and setting this process a To join the session from another process, use `ProcessGroup::attach_to()` instead. ```rust -CommandWrap::with_new("watch", |command| { command.arg("ls"); }) +Command::with_new("watch", |command| { command.arg("ls"); }) .wrap(ProcessSession) .spawn()?; ``` @@ -162,7 +193,7 @@ This resets the [signal mask] of the process instead of inheriting it from the p [signal mask]: https://www.man7.org/linux/man-pages/man2/sigprocmask.2.html ```rust -CommandWrap::with_new("watch", |command| { command.arg("ls"); }) +Command::with_new("watch", |command| { command.arg("ls"); }) .wrap(ResetSigmask) .spawn()?; ``` @@ -177,7 +208,7 @@ This is a shim to allow setting Windows process creation flags with this API, as ```rust use windows::Win32::System::Threading::*; -CommandWrap::with_new("watch", |command| { command.arg("ls"); }) +Command::with_new("watch", |command| { command.arg("ls"); }) .wrap(CreationFlags(CREATE_NO_WINDOW | CREATE_DETACHED)) .wrap(JobObject) .spawn()?; @@ -196,7 +227,7 @@ after assignment unless the caller explicitly requested `CREATE_SUSPENDED`. This is a shim to allow wrappers to handle the kill-on-drop flag, as it can't be read from Command. ```rust -let child = CommandWrap::with_new("watch", |command| { command.arg("ls"); }) +let child = Command::with_new("watch", |command| { command.arg("ls"); }) .wrap(KillOnDrop) .wrap(ProcessGroup::leader()) .spawn()?; @@ -206,8 +237,8 @@ drop(child); ### Your own Implementing a wrapper is done via a set of traits. -The std and Tokio sides are completely separate, due to the different underlying APIs. -Of course you can (and should) re-use/share code wherever possible if implementing both. +Command configuration is shared, but std and Tokio wrappers remain separate because their spawn and +child APIs differ. Re-use shared policy code when implementing both frontends. At minimum, you must implement `CommandWrapper` (from `process_wrap::std` and/or `process_wrap::tokio`). These provide the same functionality, but differ in the exact types specified. @@ -227,17 +258,17 @@ The trait provides extension or hook points into the lifecycle of a `Command`: incorporate all or part of the second, concretely typed wrapper. By default, this does nothing (that is, only the first registered wrapper instance of a type applies). -- **`fn pre_spawn(&mut self, command: &mut Command, core: &CommandWrap)`** is called before the - command is spawned, and gives mutable access to it. It also gives mutable access to the wrapper - instance, so state can be stored if needed. The `core` reference gives access to data from other - wrappers; for example, that's how `CreationFlags` on Windows works along with `JobObject`. Noop by - default. +- **`fn pre_spawn(&mut self, command: &mut tokio::process::Command, core: &Command)`** is called + before the command is spawned, and gives mutable access to that attempt's native command. It also + gives mutable access to the wrapper instance, so state can be stored if needed. The `core` + reference gives access to data from other wrappers; for example, that's how `CreationFlags` on + Windows works along with `JobObject`. Noop by default. -- **`fn post_spawn(&mut self, command: &mut Command, child: &mut tokio::process::Child, core: &CommandWrap)`** +- **`fn post_spawn(&mut self, command: &mut tokio::process::Command, child: &mut tokio::process::Child, core: &Command)`** is called after spawn, and should be used for any necessary cleanups. It is offered for completeness but is expected to be less used than `wrap_child()`. Noop by default. -- **`fn wrap_child(&mut self, child: Box, core: &CommandWrap)`** is +- **`fn wrap_child(&mut self, child: Box, core: &Command)`** is called after all `post_spawn()`s have run. If your wrapper needs to override the methods on Child, then it should create an instance of its own type implementing `ChildWrapper` and return it here. Child wraps are _in order_: you may end up with a `Foo(Bar(Child))` or a `Bar(Foo(Child))` diff --git a/docs/plans/command-api-and-pty-provider.md b/docs/plans/command-api-and-pty-provider.md new file mode 100644 index 0000000..3ef16b3 --- /dev/null +++ b/docs/plans/command-api-and-pty-provider.md @@ -0,0 +1,111 @@ +# Unified command API and PTY provider + +Process-wrap 10.0 established a reliable ordered wrapper registry and child capability model. +The PTY prototypes proved the Unix and ConPTY transports but introduced `PtyCommand` as a second command-building and spawning API. +Process-wrap 11 will instead make command configuration exact and transport-independent, with PTY selected by the same `.wrap(Pty).spawn()` flow as every other concern. + +## Shared command family + +Introduce one generic command implementation selected by sealed blocking and Tokio frontend markers. +Re-export backend-selected `Command` aliases from the existing `std` and `tokio` modules. +Retain `CommandWrap` as an alias while preferring `Command` in new documentation. +Keep backend-specific `spawn` implementations and child traits because blocking and Tokio child operations have different contracts. +Keep both Cargo frontend features additive and independently usable. + +Move program, argument, environment, cwd, wrapper registry, and common construction logic into the shared command implementation. +Keep `with_new`, with its closure receiving the process-wrap command instead of a native command. +Expose native-shaped tracked configuration methods so inferred existing closure bodies remain valid. +Preserve compatibility command accessors as process-wrap facade views rather than native escape hatches. +Add explicitly named native mutation and conversion methods. + +## Exact and native-only state + +Represent ordinary command configuration as cloneable tracked intent. +Preserve ordered regular and raw Windows argument operations without Unicode normalization. +Preserve inherited versus cleared environment state, environment mutations, and cwd exactly. +Materialize a fresh native command from tracked intent for each spawn attempt. + +Retain conversion from native std and Tokio commands as a native-only compatibility state. +Transition tracked state to native-only when callers explicitly request native mutation or configure state that cannot be reconstructed, such as arbitrary `Stdio` handles or `pre_exec` callbacks. +Allow native-only commands to use exact native spawning and explicit custom spawners. +Reject native-only state from portable providers rather than reconstructing, ignoring, or guessing at it. + +## Attempt and wrapper lifecycle + +Create a per-spawn attempt facade from tracked command state. +Change `pre_spawn` to mutate that facade while retaining read-only access to live peer wrappers. +Change `post_spawn` to receive the attempt and the frontend child capability trait rather than requiring a native child. +Run `post_spawn` for native, custom, and provider children. +Retain ordered `wrap_child` application and typed duplicate-wrapper extension. +Preserve wrapper slots and command state across success, error, and panic paths. + +Move built-in process group, process session, signal-mask, creation-flag, and kill-on-drop setup onto tracked attempt policy where possible. +Use object-safe child capabilities for process handles and provider-owned suspension rather than concrete custom-child downcasts. +Let custom wrappers compose based on representable command state and available child capabilities instead of concrete type allowlists. + +## Spawn providers + +Add an object-safe frontend-specific provider capability exposed by a command wrapper. +Select the native provider when no alternate provider is registered. +Reject multiple alternate providers before hooks or operating-system allocation. +Run provider availability checks before other provider validation so unsupported-platform errors retain precedence. +Reject incompatible immutable configuration before hooks. +Run ordered pre-spawn hooks, then validate the resulting attempt before provider allocation. +Have providers return boxed frontend children and armed cleanup transactions. +Run ordered post-spawn and child-wrapping hooks before committing the provider transaction. +Ensure provider cleanup runs after every later error or panic without replacing the original failure. + +Treat `spawn_with` and `spawn_with_child` as explicit caller-selected transports. +Reject either method when a wrapper provider is registered instead of bypassing it. +Run capability-level post-spawn and ordinary child wrapping for both methods. + +## Tokio PTY wrapper + +Replace `PtyCommand`, its duplicate command intent and wrapper registry, `PtyMarker`, and tuple-returning spawn with a Tokio-only `Pty` provider wrapper. +Store terminal size on `Pty` and merge duplicate registration through typed extension. +Keep ordinary `spawn` returning the boxed Tokio child contract. +Install a private child layer which owns the PTY controller beneath arbitrary outer wrappers. +Add one-shot controller extraction by traversing the child chain without unwrapping it. +Keep PTY input and output on the controller and keep native Tokio pipe accessors absent. + +## Unix PTY transport + +Reuse the existing PTY allocation and Tokio I/O implementation for Android, DragonFly BSD, FreeBSD, illumos, Linux, macOS, NetBSD, OpenBSD, and Solaris. +Preserve close-on-exec, nonblocking master I/O, slave duplication, controlling-terminal setup, foreground process groups, Linux EIO normalization, and cleanup behavior. +Apply slave stdio and terminal callbacks only to the fresh attempt command. +Close parent slave descriptors before post-spawn and child-wrapping hooks. +Preserve shared master ownership by input and output, weak resize ownership, merged terminal output, and independent child-wait and output-EOF lifecycles. + +Integrate real `Pty` registration with `ProcessGroup`, `ProcessSession`, `ResetSigmask`, and `KillOnDrop`. +Preserve group-aware supervision for leader/session modes. +Reject attached groups and simultaneous explicit group/session registration. +Return unsupported-platform errors before terminal-size validation or hook effects. + +## Exact Windows command model + +Reuse the existing Windows argument, environment, program-resolution, and cwd modules against shared tracked command intent. +Preserve interleaved regular and raw argument semantics, CRT quoting, WTF-16 data, and stable validation errors. +Preserve native environment inheritance, explicit Unicode blocks, case-insensitive key replacement, deterministic ordering, and drive pseudo-variables. +Preserve deterministic executable resolution and direct batch-script rejection. +Carry creation flags, explicit versus temporary suspension, JobObject, and KillOnDrop through tracked policy and child capabilities. + +## ConPTY transport + +Reuse the existing dynamic ConPTY API resolution, startup attributes, named pipes, manual `CreateProcessW`, custom child, controller, and cleanup modules. +Integrate them as the Tokio `Pty` provider rather than a separate spawn method. +Preserve unsupported-runtime precedence and exact command line, environment, cwd, flag, and handle-inheritance semantics. +Retain process and primary-thread handles through JobObject assignment and provider-owned suspension finalization. +Keep the cleanup guard armed through all wrapper hooks and disarm it only when the provider transaction commits. +Preserve cancellation-safe repeated waits, post-exit kill behavior, resize, merged I/O, direct-child and job-object kill-on-drop behavior, and off-reactor pseudoconsole closure. + +## Public documentation and migration + +Prefer the backend module `Command` aliases while retaining `CommandWrap` compatibility. +Show inferred `with_new` closures continuing to use native-shaped configuration calls. +Explain why backend typing remains at spawn and child boundaries even though command configuration is shared. +Use only `.wrap(Pty).spawn()` and one-shot controller extraction in PTY examples. +Preserve the crate’s adaptability motivation and complete supported Unix platform list. +Preserve the terminal stream, ownership, VEOF, draining, macOS lifecycle, and process-supervision rationale. +Explain that `pty` remains non-default because it selects Tokio and PTY dependencies. +Remove the rejected public PTY builder, tuple spawn, marker names, fallback-to-pipes wording, and CI-runner prose. +Document native-only escape behavior and the migration from the former PTY prototype. diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..c5ffe89 --- /dev/null +++ b/src/command.rs @@ -0,0 +1,972 @@ +#![cfg_attr(not(any(feature = "std", feature = "tokio1")), allow(dead_code))] + +use std::{ + any::Any, + ffi::{OsStr, OsString}, + fmt, + marker::PhantomData, + path::{Path, PathBuf}, + process::Stdio, +}; + +/// Blocking standard-library process frontend. +#[doc(hidden)] +#[derive(Debug)] +pub struct Blocking; + +/// Asynchronous Tokio process frontend. +#[doc(hidden)] +#[derive(Debug)] +pub struct Tokio1; + +mod private { + pub trait Sealed {} + + #[cfg(feature = "std")] + impl Sealed for super::Blocking {} + + #[cfg(feature = "tokio1")] + impl Sealed for super::Tokio1 {} +} + +/// Backend implementation detail for [`Command`]. +#[doc(hidden)] +pub trait Backend: private::Sealed + 'static { + /// The frontend's native command type. + type NativeCommand: NativeCommand; + + /// Create the frontend-specific wrapper registry. + fn new_registry() -> Box; +} + +/// Native command operations shared by the supported frontends. +#[doc(hidden)] +pub trait NativeCommand: fmt::Debug + Sized + 'static { + /// Create a command for `program`. + fn new(program: &OsStr) -> Self; + + /// Add a regular argument. + fn arg(&mut self, arg: &OsStr); + + /// Add a raw Windows command-line fragment. + #[cfg(windows)] + fn raw_arg(&mut self, arg: &OsStr); + + /// Set an environment variable. + fn env(&mut self, key: &OsStr, value: &OsStr); + + /// Remove an environment variable. + fn env_remove(&mut self, key: &OsStr); + + /// Clear explicitly configured and inherited environment variables. + fn env_clear(&mut self); + + /// Set the current directory. + fn current_dir(&mut self, dir: &Path); + + /// Configure standard input. + fn stdin(&mut self, stdio: Stdio); + + /// Configure standard output. + fn stdout(&mut self, stdio: Stdio); + + /// Configure standard error. + fn stderr(&mut self, stdio: Stdio); + + /// Get the program. + fn get_program(&self) -> &OsStr; + + /// Get the arguments. + fn get_args(&self) -> Box + '_>; + + /// Get explicitly configured environment changes. + fn get_envs(&self) -> Box)> + '_>; + + /// Get the current directory. + fn get_current_dir(&self) -> Option<&Path>; +} + +#[cfg(feature = "std")] +impl NativeCommand for std::process::Command { + fn new(program: &OsStr) -> Self { + Self::new(program) + } + + fn arg(&mut self, arg: &OsStr) { + self.arg(arg); + } + + #[cfg(windows)] + fn raw_arg(&mut self, arg: &OsStr) { + use std::os::windows::process::CommandExt; + CommandExt::raw_arg(self, arg); + } + + fn env(&mut self, key: &OsStr, value: &OsStr) { + self.env(key, value); + } + + fn env_remove(&mut self, key: &OsStr) { + self.env_remove(key); + } + + fn env_clear(&mut self) { + self.env_clear(); + } + + fn current_dir(&mut self, dir: &Path) { + self.current_dir(dir); + } + + fn stdin(&mut self, stdio: Stdio) { + self.stdin(stdio); + } + + fn stdout(&mut self, stdio: Stdio) { + self.stdout(stdio); + } + + fn stderr(&mut self, stdio: Stdio) { + self.stderr(stdio); + } + + fn get_program(&self) -> &OsStr { + self.get_program() + } + + fn get_args(&self) -> Box + '_> { + Box::new(self.get_args()) + } + + fn get_envs(&self) -> Box)> + '_> { + Box::new(self.get_envs()) + } + + fn get_current_dir(&self) -> Option<&Path> { + self.get_current_dir() + } +} + +#[cfg(feature = "tokio1")] +impl NativeCommand for tokio::process::Command { + fn new(program: &OsStr) -> Self { + Self::new(program) + } + + fn arg(&mut self, arg: &OsStr) { + self.arg(arg); + } + + #[cfg(windows)] + fn raw_arg(&mut self, arg: &OsStr) { + self.raw_arg(arg); + } + + fn env(&mut self, key: &OsStr, value: &OsStr) { + self.env(key, value); + } + + fn env_remove(&mut self, key: &OsStr) { + self.env_remove(key); + } + + fn env_clear(&mut self) { + self.env_clear(); + } + + fn current_dir(&mut self, dir: &Path) { + self.current_dir(dir); + } + + fn stdin(&mut self, stdio: Stdio) { + self.stdin(stdio); + } + + fn stdout(&mut self, stdio: Stdio) { + self.stdout(stdio); + } + + fn stderr(&mut self, stdio: Stdio) { + self.stderr(stdio); + } + + fn get_program(&self) -> &OsStr { + self.as_std().get_program() + } + + fn get_args(&self) -> Box + '_> { + Box::new(self.as_std().get_args()) + } + + fn get_envs(&self) -> Box)> + '_> { + Box::new(self.as_std().get_envs()) + } + + fn get_current_dir(&self) -> Option<&Path> { + self.as_std().get_current_dir() + } +} + +#[derive(Clone, Debug)] +pub(crate) enum CommandArg { + Regular(OsString), + #[cfg(windows)] + Raw(OsString), +} + +impl CommandArg { + fn value(&self) -> &OsStr { + match self { + Self::Regular(value) => value, + #[cfg(windows)] + Self::Raw(value) => value, + } + } +} + +#[derive(Clone, Debug)] +pub(crate) enum EnvChange { + Set(OsString, OsString), + Remove(OsString), +} + +impl EnvChange { + fn key(&self) -> &OsStr { + match self { + Self::Set(key, _) | Self::Remove(key) => key, + } + } + + fn value(&self) -> Option<&OsStr> { + match self { + Self::Set(_, value) => Some(value), + Self::Remove(_) => None, + } + } +} + +#[cfg(not(windows))] +fn env_keys_equal(left: &OsStr, right: &OsStr) -> bool { + left == right +} + +#[cfg(windows)] +fn env_keys_equal(left: &OsStr, right: &OsStr) -> bool { + use std::os::windows::ffi::OsStrExt; + + #[link(name = "kernel32")] + unsafe extern "system" { + #[link_name = "CompareStringOrdinal"] + fn compare_string_ordinal( + string1: *const u16, + count1: i32, + string2: *const u16, + count2: i32, + ignore_case: i32, + ) -> i32; + } + + let left = left.encode_wide().collect::>(); + let right = right.encode_wide().collect::>(); + let (Ok(left_len), Ok(right_len)) = (i32::try_from(left.len()), i32::try_from(right.len())) + else { + return false; + }; + + // SAFETY: both pointers remain valid for their explicit lengths during the call. The API does not + // require NUL termination when lengths are supplied. + unsafe { compare_string_ordinal(left.as_ptr(), left_len, right.as_ptr(), right_len, 1) == 2 } +} + +struct EnvChanges<'a> { + changes: &'a [EnvChange], + index: usize, +} + +impl<'a> Iterator for EnvChanges<'a> { + type Item = (&'a OsStr, Option<&'a OsStr>); + + fn next(&mut self) -> Option { + while let Some(change) = self.changes.get(self.index) { + self.index += 1; + if self.changes[self.index..] + .iter() + .any(|later| env_keys_equal(change.key(), later.key())) + { + continue; + } + + return Some((change.key(), change.value())); + } + + None + } +} + +#[derive(Clone, Debug)] +pub(crate) struct CommandIntent { + pub(crate) program: OsString, + pub(crate) args: Vec, + pub(crate) env_clear: bool, + pub(crate) env: Vec, + pub(crate) current_dir: Option, +} + +impl CommandIntent { + fn new(program: impl AsRef) -> Self { + Self { + program: program.as_ref().to_owned(), + args: Vec::new(), + env_clear: false, + env: Vec::new(), + current_dir: None, + } + } + + fn env_remove(&mut self, key: &OsStr) { + if self.env_clear { + self.env.retain(|change| !env_keys_equal(change.key(), key)); + } else { + self.env.push(EnvChange::Remove(key.to_owned())); + } + } + + fn get_envs(&self) -> EnvChanges<'_> { + EnvChanges { + changes: &self.env, + index: 0, + } + } + + pub(crate) fn materialize(&self) -> N { + let mut command = N::new(&self.program); + + for arg in &self.args { + match arg { + CommandArg::Regular(arg) => command.arg(arg), + #[cfg(windows)] + CommandArg::Raw(arg) => command.raw_arg(arg), + } + } + + if self.env_clear { + command.env_clear(); + } + for change in &self.env { + match change { + EnvChange::Set(key, value) => command.env(key, value), + EnvChange::Remove(key) => command.env_remove(key), + } + } + + if let Some(dir) = &self.current_dir { + command.current_dir(dir); + } + + command + } +} + +#[derive(Debug)] +struct NativeCommandView { + program: OsString, + args: Vec, + env: Vec<(OsString, Option)>, + current_dir: Option, +} + +impl NativeCommandView { + fn capture(command: &N) -> Self { + Self { + program: command.get_program().to_owned(), + args: command.get_args().map(OsStr::to_owned).collect(), + env: command + .get_envs() + .map(|(key, value)| (key.to_owned(), value.map(OsStr::to_owned))) + .collect(), + current_dir: command.get_current_dir().map(Path::to_owned), + } + } + + fn get_args(&self) -> Box + '_> { + Box::new(self.args.iter().map(OsString::as_os_str)) + } + + fn get_envs(&self) -> Box)> + '_> { + Box::new( + self.env + .iter() + .map(|(key, value)| (key.as_os_str(), value.as_deref())), + ) + } +} + +struct NativeOnlyCommand { + command: Option, + view: NativeCommandView, +} + +impl NativeOnlyCommand { + fn new(command: N) -> Self { + Self { + view: NativeCommandView::capture(&command), + command: Some(command), + } + } + + fn command_mut(&mut self) -> &mut N { + self.command + .as_mut() + .expect("native command access cannot occur while a spawn lifecycle is active") + } + + fn take(&mut self) -> N { + let command = self + .command + .take() + .expect("a native-only command is present when its spawn lifecycle begins"); + self.view = NativeCommandView::capture(&command); + command + } + + fn restore(&mut self, command: N) { + debug_assert!(self.command.is_none()); + self.command = Some(command); + } + + fn into_command(self) -> N { + self.command + .expect("a command cannot be consumed while its spawn lifecycle is active") + } +} + +impl fmt::Debug for NativeOnlyCommand { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("NativeOnly") + .field("command", &self.command) + .field("view", &self.view) + .finish() + } +} + +enum CommandState { + Tracked(CommandIntent), + NativeOnly(NativeOnlyCommand), +} + +impl fmt::Debug for CommandState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Tracked(intent) => f.debug_tuple("Tracked").field(intent).finish(), + Self::NativeOnly(command) => command.fmt(f), + } + } +} + +/// A configurable process command with composable wrappers. +/// +/// The backend type is normally selected through `process_wrap::std::Command` or +/// `process_wrap::tokio::Command`. Command construction and configuration are shared; spawning and +/// child behavior remain specific to the selected frontend. +pub struct Command { + state: CommandState, + wrappers: Box, + backend: PhantomData B>, +} + +impl fmt::Debug for Command { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Command") + .field("state", &self.state) + .finish_non_exhaustive() + } +} + +impl Command { + /// Create a command for `program`. + pub fn new(program: impl AsRef) -> Self { + Self { + state: CommandState::Tracked(CommandIntent::new(program)), + wrappers: B::new_registry(), + backend: PhantomData, + } + } + + /// Create a command and configure it with a closure. + pub fn with_new(program: impl AsRef, init: impl FnOnce(&mut Self)) -> Self { + let mut command = Self::new(program); + init(&mut command); + command + } + + /// Get a compatibility view of this process-wrap command. + pub fn command(&self) -> &Self { + self + } + + /// Get a mutable compatibility view of this process-wrap command. + pub fn command_mut(&mut self) -> &mut Self { + self + } + + /// Discard all wrappers and return this process-wrap command. + pub fn into_command(mut self) -> Self { + self.wrappers = B::new_registry(); + self + } + + /// Add an argument. + pub fn arg(&mut self, arg: impl AsRef) -> &mut Self { + let arg = arg.as_ref(); + match &mut self.state { + CommandState::Tracked(intent) => intent.args.push(CommandArg::Regular(arg.to_owned())), + CommandState::NativeOnly(command) => command.command_mut().arg(arg), + } + self + } + + /// Add multiple arguments. + pub fn args(&mut self, args: I) -> &mut Self + where + I: IntoIterator, + S: AsRef, + { + for arg in args { + self.arg(arg); + } + self + } + + /// Add a raw command-line fragment without quoting or escaping. + /// + /// This method is only available on Windows. + #[cfg(windows)] + pub fn raw_arg(&mut self, arg: impl AsRef) -> &mut Self { + let arg = arg.as_ref(); + match &mut self.state { + CommandState::Tracked(intent) => intent.args.push(CommandArg::Raw(arg.to_owned())), + CommandState::NativeOnly(command) => command.command_mut().raw_arg(arg), + } + self + } + + /// Set an environment variable. + pub fn env(&mut self, key: impl AsRef, value: impl AsRef) -> &mut Self { + let key = key.as_ref(); + let value = value.as_ref(); + match &mut self.state { + CommandState::Tracked(intent) => intent + .env + .push(EnvChange::Set(key.to_owned(), value.to_owned())), + CommandState::NativeOnly(command) => command.command_mut().env(key, value), + } + self + } + + /// Set multiple environment variables. + pub fn envs(&mut self, vars: I) -> &mut Self + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + for (key, value) in vars { + self.env(key, value); + } + self + } + + /// Remove an environment variable from the child environment. + pub fn env_remove(&mut self, key: impl AsRef) -> &mut Self { + let key = key.as_ref(); + match &mut self.state { + CommandState::Tracked(intent) => intent.env_remove(key), + CommandState::NativeOnly(command) => command.command_mut().env_remove(key), + } + self + } + + /// Clear explicitly configured variables and prevent inheriting the parent environment. + pub fn env_clear(&mut self) -> &mut Self { + match &mut self.state { + CommandState::Tracked(intent) => { + intent.env_clear = true; + intent.env.clear(); + } + CommandState::NativeOnly(command) => command.command_mut().env_clear(), + } + self + } + + /// Set the child process's current directory. + pub fn current_dir(&mut self, dir: impl AsRef) -> &mut Self { + let dir = dir.as_ref(); + match &mut self.state { + CommandState::Tracked(intent) => intent.current_dir = Some(dir.to_owned()), + CommandState::NativeOnly(command) => command.command_mut().current_dir(dir), + } + self + } + + /// Configure standard input and make the command native-only. + pub fn stdin(&mut self, stdio: Stdio) -> &mut Self { + self.native_mut().stdin(stdio); + self + } + + /// Configure standard output and make the command native-only. + pub fn stdout(&mut self, stdio: Stdio) -> &mut Self { + self.native_mut().stdout(stdio); + self + } + + /// Configure standard error and make the command native-only. + pub fn stderr(&mut self, stdio: Stdio) -> &mut Self { + self.native_mut().stderr(stdio); + self + } + + /// Get the configured program. + pub fn get_program(&self) -> &OsStr { + match &self.state { + CommandState::Tracked(intent) => &intent.program, + CommandState::NativeOnly(command) => match &command.command { + Some(command) => command.get_program(), + None => &command.view.program, + }, + } + } + + /// Get the configured arguments. + pub fn get_args(&self) -> Box + '_> { + match &self.state { + CommandState::Tracked(intent) => Box::new(intent.args.iter().map(CommandArg::value)), + CommandState::NativeOnly(command) => match &command.command { + Some(command) => command.get_args(), + None => command.view.get_args(), + }, + } + } + + /// Get explicitly configured environment changes. + pub fn get_envs(&self) -> Box)> + '_> { + match &self.state { + CommandState::Tracked(intent) => Box::new(intent.get_envs()), + CommandState::NativeOnly(command) => match &command.command { + Some(command) => command.get_envs(), + None => command.view.get_envs(), + }, + } + } + + /// Get the configured current directory. + pub fn get_current_dir(&self) -> Option<&Path> { + match &self.state { + CommandState::Tracked(intent) => intent.current_dir.as_deref(), + CommandState::NativeOnly(command) => match &command.command { + Some(command) => command.get_current_dir(), + None => command.view.current_dir.as_deref(), + }, + } + } + + /// Mutably access the frontend's native command. + /// + /// Calling this permanently makes the command native-only. Alternate portable transports cannot + /// recover exact portable intent after arbitrary native mutation. + pub fn native_mut(&mut self) -> &mut B::NativeCommand { + if let CommandState::Tracked(intent) = &self.state { + let command = intent.materialize::(); + self.state = CommandState::NativeOnly(NativeOnlyCommand::new(command)); + } + + match &mut self.state { + CommandState::NativeOnly(command) => command.command_mut(), + CommandState::Tracked(_) => unreachable!("tracked command was materialized above"), + } + } + + /// Consume this command and return the frontend's native command. + pub fn into_native(self) -> B::NativeCommand { + match self.state { + CommandState::Tracked(intent) => intent.materialize::(), + CommandState::NativeOnly(command) => command.into_command(), + } + } + + pub(crate) fn from_native(command: B::NativeCommand) -> Self { + Self { + state: CommandState::NativeOnly(NativeOnlyCommand::new(command)), + wrappers: B::new_registry(), + backend: PhantomData, + } + } + + pub(crate) fn registry(&self) -> &R { + self.wrappers + .downcast_ref() + .expect("the backend always creates its matching wrapper registry") + } + + pub(crate) fn registry_mut(&mut self) -> &mut R { + self.wrappers + .downcast_mut() + .expect("the backend always creates its matching wrapper registry") + } + + pub(crate) fn with_native( + &mut self, + invoke: impl FnOnce(&mut Self, &mut B::NativeCommand) -> std::io::Result, + ) -> std::io::Result { + match &mut self.state { + CommandState::Tracked(intent) => { + let mut native = intent.materialize::(); + invoke(self, &mut native) + } + CommandState::NativeOnly(command) => { + let mut native = command.take(); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + invoke(self, &mut native) + })); + match &mut self.state { + CommandState::NativeOnly(command) => command.restore(native), + CommandState::Tracked(_) => { + unreachable!("a spawn lifecycle cannot replace native-only command state") + } + } + match result { + Ok(result) => result, + Err(payload) => std::panic::resume_unwind(payload), + } + } + } + } +} + +#[cfg(all(feature = "std", unix))] +impl Command { + /// Set the child process's user ID and make the command native-only. + pub fn uid(&mut self, id: u32) -> &mut Self { + use ::std::os::unix::process::CommandExt; + CommandExt::uid(self.native_mut(), id); + self + } + + /// Set the child process's group ID and make the command native-only. + pub fn gid(&mut self, id: u32) -> &mut Self { + use ::std::os::unix::process::CommandExt; + CommandExt::gid(self.native_mut(), id); + self + } + + /// Set the child process's `argv[0]` and make the command native-only. + pub fn arg0(&mut self, arg: impl AsRef) -> &mut Self { + use ::std::os::unix::process::CommandExt; + CommandExt::arg0(self.native_mut(), arg); + self + } + + /// Set the child process's process group and make the command native-only. + pub fn process_group(&mut self, pgroup: i32) -> &mut Self { + use ::std::os::unix::process::CommandExt; + CommandExt::process_group(self.native_mut(), pgroup); + self + } + + /// Register a callback to run in the child after `fork` and make the command native-only. + /// + /// # Safety + /// + /// The callback runs in the child process after `fork` and before `exec`. It may only perform + /// operations which are valid in that constrained environment. In particular, allocating or + /// acquiring locks can be unsound when another thread held the corresponding state across `fork`. + pub unsafe fn pre_exec(&mut self, f: F) -> &mut Self + where + F: FnMut() -> ::std::io::Result<()> + Send + Sync + 'static, + { + use ::std::os::unix::process::CommandExt; + // SAFETY: the caller accepts the native `pre_exec` contract documented above. + unsafe { CommandExt::pre_exec(self.native_mut(), f) }; + self + } +} + +#[cfg(all(feature = "std", windows))] +impl Command { + /// Set Windows process creation flags and make the command native-only. + pub fn creation_flags(&mut self, flags: u32) -> &mut Self { + use ::std::os::windows::process::CommandExt; + CommandExt::creation_flags(self.native_mut(), flags); + self + } +} + +#[cfg(feature = "tokio1")] +impl Command { + /// Configure whether dropping the Tokio child kills it and make the command native-only. + pub fn kill_on_drop(&mut self, kill_on_drop: bool) -> &mut Self { + self.native_mut().kill_on_drop(kill_on_drop); + self + } +} + +#[cfg(all(feature = "tokio1", feature = "process-group", unix))] +pub(crate) fn tokio_process_group(command: &mut tokio::process::Command, pgroup: i32) { + let set_process_group = move || { + // SAFETY: `setpgid` is called in the child with its own PID and does not retain pointers. + if unsafe { nix::libc::setpgid(0, pgroup) } == -1 { + Err(::std::io::Error::last_os_error()) + } else { + Ok(()) + } + }; + // SAFETY: the callback only invokes `setpgid`, which is valid between `fork` and `exec`. + unsafe { command.pre_exec(set_process_group) }; +} + +#[cfg(all(feature = "tokio1", unix))] +impl Command { + /// Set the child process's user ID and make the command native-only. + pub fn uid(&mut self, id: u32) -> &mut Self { + self.native_mut().uid(id); + self + } + + /// Set the child process's group ID and make the command native-only. + pub fn gid(&mut self, id: u32) -> &mut Self { + self.native_mut().gid(id); + self + } + + /// Set the child process's `argv[0]` and make the command native-only. + pub fn arg0(&mut self, arg: impl AsRef) -> &mut Self { + self.native_mut().arg0(arg); + self + } + + /// Register a callback to run in the child after `fork` and make the command native-only. + /// + /// # Safety + /// + /// The callback runs in the child process after `fork` and before `exec`. It may only perform + /// operations which are valid in that constrained environment. In particular, allocating or + /// acquiring locks can be unsound when another thread held the corresponding state across `fork`. + pub unsafe fn pre_exec(&mut self, f: F) -> &mut Self + where + F: FnMut() -> ::std::io::Result<()> + Send + Sync + 'static, + { + // SAFETY: the caller accepts the native `pre_exec` contract documented above. + unsafe { self.native_mut().pre_exec(f) }; + self + } +} + +#[cfg(all(feature = "tokio1", windows))] +impl Command { + /// Set Windows process creation flags and make the command native-only. + pub fn creation_flags(&mut self, flags: u32) -> &mut Self { + self.native_mut().creation_flags(flags); + self + } +} + +#[cfg(all(test, windows))] +mod windows_tests { + use std::{ + ffi::{OsStr, OsString}, + os::windows::ffi::OsStringExt, + path::Path, + process::Stdio, + }; + + use super::{CommandArg, CommandIntent, NativeCommand}; + + #[derive(Debug, Eq, PartialEq)] + enum RecordedArg { + Regular(OsString), + Raw(OsString), + } + + #[derive(Debug)] + struct RecordedCommand { + program: OsString, + args: Vec, + } + + impl NativeCommand for RecordedCommand { + fn new(program: &OsStr) -> Self { + Self { + program: program.to_owned(), + args: Vec::new(), + } + } + + fn arg(&mut self, arg: &OsStr) { + self.args.push(RecordedArg::Regular(arg.to_owned())); + } + + fn raw_arg(&mut self, arg: &OsStr) { + self.args.push(RecordedArg::Raw(arg.to_owned())); + } + + fn env(&mut self, _key: &OsStr, _value: &OsStr) {} + + fn env_remove(&mut self, _key: &OsStr) {} + + fn env_clear(&mut self) {} + + fn current_dir(&mut self, _dir: &Path) {} + + fn stdin(&mut self, _stdio: Stdio) {} + + fn stdout(&mut self, _stdio: Stdio) {} + + fn stderr(&mut self, _stdio: Stdio) {} + + fn get_program(&self) -> &OsStr { + &self.program + } + + fn get_args(&self) -> Box + '_> { + Box::new(self.args.iter().map(|arg| match arg { + RecordedArg::Regular(value) | RecordedArg::Raw(value) => value.as_os_str(), + })) + } + + fn get_envs(&self) -> Box)> + '_> { + Box::new(std::iter::empty()) + } + + fn get_current_dir(&self) -> Option<&Path> { + None + } + } + + #[test] + fn materialization_preserves_raw_argument_kinds_and_wtf16() { + let raw = OsString::from_wide(&[b' ' as u16, 0xd800, b' ' as u16]); + let regular_surrogate = OsString::from_wide(&[0xdfff]); + let intent = CommandIntent { + program: OsString::from("tool"), + args: vec![ + CommandArg::Regular(OsString::from("regular")), + CommandArg::Raw(raw.clone()), + CommandArg::Regular(regular_surrogate.clone()), + ], + env_clear: false, + env: Vec::new(), + current_dir: None, + }; + + let command = intent.materialize::(); + + assert_eq!( + command.args, + [ + RecordedArg::Regular(OsString::from("regular")), + RecordedArg::Raw(raw), + RecordedArg::Regular(regular_surrogate), + ] + ); + } +} diff --git a/src/generic_wrap.rs b/src/generic_wrap.rs index 18911c2..3c0114f 100644 --- a/src/generic_wrap.rs +++ b/src/generic_wrap.rs @@ -4,399 +4,381 @@ )] macro_rules! Wrap { - ($command:ty, $child:ty, $childer:ident, $first_child_wrapper:expr) => { - trait ErasedCommandWrapper: ::std::fmt::Debug + Send + Sync { - fn as_command_wrapper_mut(&mut self) -> &mut dyn CommandWrapper; - fn as_any(&self) -> &dyn ::std::any::Any; - fn as_any_mut(&mut self) -> &mut dyn ::std::any::Any; - } - - impl ErasedCommandWrapper for W { - fn as_command_wrapper_mut(&mut self) -> &mut dyn CommandWrapper { - self - } - - fn as_any(&self) -> &dyn ::std::any::Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn ::std::any::Any { - self - } - } - - /// A wrapper around a `Command` that allows for additional functionality to be added. - /// - /// This is the core type of the `process-wrap` crate. It is a wrapper around a - #[doc = concat!("[`", stringify!($command), "`].")] - #[derive(Debug)] - pub struct CommandWrap { - command: $command, - wrappers: ::indexmap::IndexMap< - ::std::any::TypeId, - Option>, - >, - } - - impl CommandWrap { - /// Create from a program name and a closure to configure the command. - /// - /// This is a convenience method that creates a new `Command` and then calls the closure - /// to configure it. The `Command` is then wrapped and returned. - /// - #[doc = concat!("Alternatively, use `From`/`Into` to convert a [`", stringify!($command),"`] to a [`", stringify!(CommandWrap), "`].")] - pub fn with_new( - program: impl AsRef<::std::ffi::OsStr>, - init: impl FnOnce(&mut $command), - ) -> Self { - let mut command = <$command>::new(program); - init(&mut command); - Self { - command, - wrappers: ::indexmap::IndexMap::new(), - } - } - - /// Get a reference to the wrapped command. - pub fn command(&self) -> &$command { - &self.command - } - - /// Get a mutable reference to the wrapped command. - pub fn command_mut(&mut self) -> &mut $command { - &mut self.command - } - - /// Get the wrapped command. - pub fn into_command(self) -> $command { - self.command - } - - /// Add a wrapper to the command. - /// - /// This is a lazy method, and the wrapper is not actually applied until `spawn` is - /// called. - /// - /// Only one wrapper of a given type can be applied to a command. If `wrap` is called - /// twice with the same type, the existing wrapper receives the newly registered wrapper - /// through its typed `extend` hook and can merge its configuration. If the hook does - /// nothing, the _new_ wrapper is silently discarded. - /// - /// Returns `&mut self` for chaining. - pub fn wrap(&mut self, wrapper: W) -> &mut Self { - let typeid = ::std::any::TypeId::of::(); - let mut wrapper = Some(wrapper); - let extant = self.wrappers.entry(typeid).or_insert_with(|| { - Some(Box::new(wrapper.take().unwrap()) as Box) - }); - if let Some(wrapper) = wrapper { - extant - .as_mut() - .expect("wrap() cannot run while the matching wrapper's hook is active") - .as_any_mut() - .downcast_mut::() - .expect("downcasting is guaranteed to succeed due to wrap()'s internals") - .extend(wrapper); - } - - self - } - - #[inline] - fn with_wrapper_at( - &mut self, - index: usize, - invoke: impl FnOnce( - &mut dyn CommandWrapper, - &CommandWrap, - ) -> ::std::io::Result, - ) -> ::std::io::Result { - let mut wrapper = self - .wrappers - .get_index_mut(index) - .expect("wrapper indices cannot disappear during ordered hook traversal") - .1 - .take() - .expect("each wrapper is present when its lifecycle hook begins"); - - let result = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| { - invoke(wrapper.as_command_wrapper_mut(), self) - })); - - let slot = self - .wrappers - .get_index_mut(index) - .expect("wrapper registrations cannot disappear while their hooks run") - .1; - debug_assert!(slot.is_none()); - *slot = Some(wrapper); - - match result { - Ok(result) => result, - Err(payload) => ::std::panic::resume_unwind(payload), - } - } - - #[inline] - fn run_pre_spawn(&mut self, command: &mut $command) -> ::std::io::Result<()> { - for index in 0..self.wrappers.len() { - #[cfg(feature = "tracing")] - { - let id = self - .wrappers - .get_index(index) - .expect("wrapper indices cannot disappear during ordered hook traversal").0; - ::tracing::debug!(?id, "pre_spawn"); - } - self.with_wrapper_at(index, |wrapper, core| { - wrapper.pre_spawn(command, core) - })?; - } - - Ok(()) - } - - #[inline] - fn run_wrap_child( - &mut self, - mut child: Box, - ) -> ::std::io::Result> { - for index in 0..self.wrappers.len() { - #[cfg(feature = "tracing")] - { - let id = self - .wrappers - .get_index(index) - .expect("wrapper indices cannot disappear during ordered hook traversal").0; - ::tracing::debug!(?id, "wrap_child"); - } - child = self.with_wrapper_at(index, |wrapper, core| { - wrapper.wrap_child(child, core) - })?; - } - - Ok(child) - } - - #[inline] - fn with_command( - &mut self, - invoke: impl FnOnce( - &mut CommandWrap, - &mut $command, - ) -> ::std::io::Result, - ) -> ::std::io::Result { - let mut command = ::std::mem::replace(&mut self.command, <$command>::new("")); - let result = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| { - invoke(self, &mut command) - })); - - self.command = command; - - match result { - Ok(result) => result, - Err(payload) => ::std::panic::resume_unwind(payload), - } - } - - #[inline] - fn spawn_inner( - &mut self, - command: &mut $command, - spawner: impl FnOnce(&mut $command) -> ::std::io::Result<$child>, - ) -> ::std::io::Result> { - self.run_pre_spawn(command)?; - - let mut child = spawner(command)?; - for index in 0..self.wrappers.len() { - #[cfg(feature = "tracing")] - { - let id = self - .wrappers - .get_index(index) - .expect("wrapper indices cannot disappear during ordered hook traversal").0; - ::tracing::debug!(?id, "post_spawn"); - } - self.with_wrapper_at(index, |wrapper, core| { - wrapper.post_spawn(command, &mut child, core) - })?; - } - - let child = Box::new( - #[allow(clippy::redundant_closure_call)] - $first_child_wrapper(child), - ) as Box; - - self.run_wrap_child(child) - } - - #[inline] - fn spawn_with_child_inner( - &mut self, - command: &mut $command, - spawner: impl FnOnce( - &mut $command, - ) -> ::std::io::Result>, - ) -> ::std::io::Result> { - self.run_pre_spawn(command)?; - let child = spawner(command)?; - self.run_wrap_child(child) - } - - /// Spawn the command, returning a `Child` that can be interacted with. - /// - /// In order, this runs all the `pre_spawn` hooks, then spawns the command, then runs - /// all the `post_spawn` hooks, then stacks all the `wrap_child`s. As it returns a boxed - /// trait object, only the methods from the trait are available directly; however you - /// may downcast to the concrete type of the last applied wrapper if you need to. - pub fn spawn(&mut self) -> ::std::io::Result> { - self.spawn_with(|command| command.spawn()) - } - - /// Spawn the command using a custom native-child spawner function. - /// - /// This is like [`spawn`](Self::spawn), but instead of calling `command.spawn()` - /// directly, it calls the provided closure to create the native child process. This is - /// useful when you need to use a platform-specific spawning mechanism that still returns - #[doc = concat!("a [`", stringify!($child), "`].")] - /// - /// The lifecycle is the same as `spawn`: all `pre_spawn` hooks run first, then - /// the provided closure is called, then `post_spawn` hooks, then `wrap_child`. - pub fn spawn_with( - &mut self, - spawner: impl FnOnce(&mut $command) -> ::std::io::Result<$child>, - ) -> ::std::io::Result> { - self.with_command(|core, command| core.spawn_inner(command, spawner)) - } - - /// Spawn the command using a custom boxed-child spawner function. - /// - /// This is the spawning path for custom child implementations which do not return the - #[doc = concat!("native [`", stringify!($child), "`] type. The closure must return a boxed [`", stringify!($childer), "`] trait object.")] - /// - /// All `pre_spawn` hooks run first, then the provided closure is called, then - /// `wrap_child` hooks are applied. `post_spawn` is intentionally skipped because that - #[doc = concat!("hook requires a native [`", stringify!($child), "`]. Use [`spawn_with`](Self::spawn_with) when the spawner returns one.")] - pub fn spawn_with_child( - &mut self, - spawner: impl FnOnce( - &mut $command, - ) -> ::std::io::Result>, - ) -> ::std::io::Result> { - self.with_command(|core, command| { - core.spawn_with_child_inner(command, spawner) - }) - } - - /// Check if a wrapper of a given type is present. - pub fn has_wrap(&self) -> bool { - let typeid = ::std::any::TypeId::of::(); - self.wrappers.contains_key(&typeid) - } - - /// Get a reference to a wrapper of a given type. - /// - /// This is useful for getting access to the state of a wrapper, generally from within - /// another wrapper. - /// - /// Returns `None` if the wrapper is not present. While a wrapper's lifecycle hook is - /// running, that active wrapper remains registered but is temporarily unavailable through - /// this method; peer wrappers remain available. To merely check registration, use - /// `has_wrap` instead. - pub fn get_wrap(&self) -> Option<&W> { - let typeid = ::std::any::TypeId::of::(); - self.wrappers - .get(&typeid) - .and_then(Option::as_deref) - .map(|wrapper| { - wrapper - .as_any() - .downcast_ref() - .expect("downcasting is guaranteed to succeed due to wrap()'s internals") - }) - } - } - - impl From for CommandWrap { - fn from(command: $command) -> Self { - Self { - command, - wrappers: ::indexmap::IndexMap::new(), - } - } - } - - /// A trait for adding functionality to a `Command`. - /// - /// This trait provides extension or hook points into the lifecycle of a `Command`. See the - /// [crate-level doc](crate) for an overview. - /// - /// All methods are optional, so a minimal impl may be: - /// - /// ```rust,ignore - /// #[derive(Debug)] - /// pub struct YourWrapper; - #[doc = concat!("impl ", stringify!(CommandWrapper), " for YourWrapper {}\n```")] - pub trait CommandWrapper: ::std::fmt::Debug + Send + Sync { - /// Called on a first instance if a second of the same type is added. - /// - /// Only one wrapper of a given type can exist within a Wrap at a time. By default, - /// later registrations are discarded. In some cases it is useful to merge their - /// configuration instead. This method is called on the stored wrapper with the newly - /// registered wrapper of the same concrete type. - /// - /// Because `other` is `Self`, implementations can inspect or move its type-specific - /// fields directly without downcasting. - /// - /// Default impl: no-op. - fn extend(&mut self, _other: Self) - where - Self: Sized, - { - } - - /// Called before the command is spawned, to mutate it as needed. - /// - /// This is where to modify the command before it is spawned. It also gives mutable - /// access to the wrapper instance, so state can be stored if needed. The `core` - /// reference gives access to data from other wrappers; for example, that's how - /// `CreationFlags` on Windows works along with `JobObject`. - /// - /// Defaut impl: no-op. - fn pre_spawn(&mut self, _command: &mut $command, _core: &CommandWrap) -> Result<()> { - Ok(()) - } - - /// Called after spawn, but before the child is wrapped. - /// - /// The `core` reference gives access to data from other wrappers; for example, that's - /// how `CreationFlags` on Windows works along with `JobObject`. - /// - /// Default: no-op. - fn post_spawn(&mut self, _command: &mut $command, _child: &mut $child, _core: &CommandWrap) -> Result<()> { - Ok(()) - } - - /// Called to wrap a child into this command wrapper's child wrapper. - /// - /// If the wrapper needs to override the methods on Child, then it should create an - /// instance of its own type implementing `ChildWrapper` and return it here. Child wraps - /// are _in order_: you may end up with a `Foo(Bar(Child))` or a `Bar(Foo(Child))` - /// depending on if `.wrap(Foo).wrap(Bar)` or `.wrap(Bar).wrap(Foo)` was called. - /// - /// The `core` reference gives access to data from other wrappers; for example, that's - /// how `CreationFlags` on Windows works along with `JobObject`. - /// - /// Default: no-op (ie, returns the child unchanged). - fn wrap_child( - &mut self, - child: Box, - _core: &CommandWrap, - ) -> Result> { - Ok(child) - } - } - }; + ($backend:ty, $command:ty, $child:ty, $childer:ident, $first_child_wrapper:expr) => { + trait ErasedCommandWrapper: ::std::fmt::Debug + Send + Sync { + fn as_command_wrapper_mut(&mut self) -> &mut dyn CommandWrapper; + fn as_any(&self) -> &dyn ::std::any::Any; + fn as_any_mut(&mut self) -> &mut dyn ::std::any::Any; + } + + impl ErasedCommandWrapper for W { + fn as_command_wrapper_mut(&mut self) -> &mut dyn CommandWrapper { + self + } + + fn as_any(&self) -> &dyn ::std::any::Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn ::std::any::Any { + self + } + } + + #[derive(Debug, Default)] + struct WrapperRegistry { + wrappers: ::indexmap::IndexMap< + ::std::any::TypeId, + Option>, + >, + } + + impl crate::command::Backend for $backend { + type NativeCommand = $command; + + fn new_registry() -> Box { + Box::new(WrapperRegistry::default()) + } + } + + /// A configurable process command with composable wrappers. + pub type Command = crate::command::Command<$backend>; + + /// Backwards-compatible name for [`Command`]. + pub type CommandWrap = Command; + + impl crate::command::Command<$backend> { + fn wrapper_registry(&self) -> &WrapperRegistry { + self.registry() + } + + fn wrapper_registry_mut(&mut self) -> &mut WrapperRegistry { + self.registry_mut() + } + + /// Add a wrapper to the command. + /// + /// This is a lazy method, and the wrapper is not actually applied until `spawn` is + /// called. + /// + /// Only one wrapper of a given type can be applied to a command. If `wrap` is called + /// twice with the same type, the existing wrapper receives the newly registered wrapper + /// through its typed `extend` hook and can merge its configuration. If the hook does + /// nothing, the _new_ wrapper is silently discarded. + /// + /// Returns `&mut self` for chaining. + pub fn wrap(&mut self, wrapper: W) -> &mut Self { + let typeid = ::std::any::TypeId::of::(); + let mut wrapper = Some(wrapper); + let extant = self + .wrapper_registry_mut() + .wrappers + .entry(typeid) + .or_insert_with(|| { + Some(Box::new(wrapper.take().unwrap()) as Box) + }); + if let Some(wrapper) = wrapper { + extant + .as_mut() + .expect("wrap() cannot run while the matching wrapper's hook is active") + .as_any_mut() + .downcast_mut::() + .expect("downcasting is guaranteed to succeed due to wrap()'s internals") + .extend(wrapper); + } + + self + } + + #[inline] + fn with_wrapper_at( + &mut self, + index: usize, + invoke: impl FnOnce(&mut dyn CommandWrapper, &CommandWrap) -> ::std::io::Result, + ) -> ::std::io::Result { + let mut wrapper = self + .wrapper_registry_mut() + .wrappers + .get_index_mut(index) + .expect("wrapper indices cannot disappear during ordered hook traversal") + .1 + .take() + .expect("each wrapper is present when its lifecycle hook begins"); + + let result = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| { + invoke(wrapper.as_command_wrapper_mut(), self) + })); + + let slot = self + .wrapper_registry_mut() + .wrappers + .get_index_mut(index) + .expect("wrapper registrations cannot disappear while their hooks run") + .1; + debug_assert!(slot.is_none()); + *slot = Some(wrapper); + + match result { + Ok(result) => result, + Err(payload) => ::std::panic::resume_unwind(payload), + } + } + + #[inline] + fn run_pre_spawn(&mut self, command: &mut $command) -> ::std::io::Result<()> { + let len = self.wrapper_registry().wrappers.len(); + for index in 0..len { + #[cfg(feature = "tracing")] + { + let id = self + .wrapper_registry() + .wrappers + .get_index(index) + .expect("wrapper indices cannot disappear during ordered hook traversal") + .0; + ::tracing::debug!(?id, "pre_spawn"); + } + self.with_wrapper_at(index, |wrapper, core| { + wrapper.pre_spawn(command, core) + })?; + } + + Ok(()) + } + + #[inline] + fn run_wrap_child( + &mut self, + mut child: Box, + ) -> ::std::io::Result> { + let len = self.wrapper_registry().wrappers.len(); + for index in 0..len { + #[cfg(feature = "tracing")] + { + let id = self + .wrapper_registry() + .wrappers + .get_index(index) + .expect("wrapper indices cannot disappear during ordered hook traversal") + .0; + ::tracing::debug!(?id, "wrap_child"); + } + child = self.with_wrapper_at(index, |wrapper, core| { + wrapper.wrap_child(child, core) + })?; + } + + Ok(child) + } + + #[inline] + fn spawn_inner( + &mut self, + command: &mut $command, + spawner: impl FnOnce(&mut $command) -> ::std::io::Result<$child>, + ) -> ::std::io::Result> { + self.run_pre_spawn(command)?; + + let mut child = spawner(command)?; + let len = self.wrapper_registry().wrappers.len(); + for index in 0..len { + #[cfg(feature = "tracing")] + { + let id = self + .wrapper_registry() + .wrappers + .get_index(index) + .expect("wrapper indices cannot disappear during ordered hook traversal") + .0; + ::tracing::debug!(?id, "post_spawn"); + } + self.with_wrapper_at(index, |wrapper, core| { + wrapper.post_spawn(command, &mut child, core) + })?; + } + + let child = Box::new( + #[allow(clippy::redundant_closure_call)] + $first_child_wrapper(child), + ) as Box; + + self.run_wrap_child(child) + } + + #[inline] + fn spawn_with_child_inner( + &mut self, + command: &mut $command, + spawner: impl FnOnce( + &mut $command, + ) -> ::std::io::Result>, + ) -> ::std::io::Result> { + self.run_pre_spawn(command)?; + let child = spawner(command)?; + self.run_wrap_child(child) + } + + /// Spawn the command, returning a child that can be interacted with. + /// + /// In order, this runs all the `pre_spawn` hooks, then spawns the command, then runs + /// all the `post_spawn` hooks, then stacks all the `wrap_child`s. As it returns a boxed + /// trait object, only the methods from the trait are available directly; however you + /// may downcast to the concrete type of the last applied wrapper if you need to. + pub fn spawn(&mut self) -> ::std::io::Result> { + self.spawn_with(|command| command.spawn()) + } + + /// Spawn the command using a custom native-child spawner function. + /// + /// This is like [`spawn`](Self::spawn), but instead of calling `command.spawn()` + /// directly, it calls the provided closure to create the native child process. This is + /// useful when you need to use a platform-specific spawning mechanism that still returns + #[doc = concat!("a [`", stringify!($child), "`].")] + /// + /// The lifecycle is the same as `spawn`: all `pre_spawn` hooks run first, then + /// the provided closure is called, then `post_spawn` hooks, then `wrap_child`. + pub fn spawn_with( + &mut self, + spawner: impl FnOnce(&mut $command) -> ::std::io::Result<$child>, + ) -> ::std::io::Result> { + self.with_native(|core, command| core.spawn_inner(command, spawner)) + } + + /// Spawn the command using a custom boxed-child spawner function. + /// + /// This is the spawning path for custom child implementations which do not return the + #[doc = concat!("native [`", stringify!($child), "`] type. The closure must return a boxed [`", stringify!($childer), "`] trait object.")] + /// + /// All `pre_spawn` hooks run first, then the provided closure is called, then + /// `wrap_child` hooks are applied. `post_spawn` is intentionally skipped because that + #[doc = concat!("hook requires a native [`", stringify!($child), "`]. Use [`spawn_with`](Self::spawn_with) when the spawner returns one.")] + pub fn spawn_with_child( + &mut self, + spawner: impl FnOnce( + &mut $command, + ) -> ::std::io::Result>, + ) -> ::std::io::Result> { + self.with_native(|core, command| { + core.spawn_with_child_inner(command, spawner) + }) + } + + /// Check if a wrapper of a given type is present. + pub fn has_wrap(&self) -> bool { + let typeid = ::std::any::TypeId::of::(); + self.wrapper_registry().wrappers.contains_key(&typeid) + } + + /// Get a reference to a wrapper of a given type. + /// + /// This is useful for getting access to the state of a wrapper, generally from within + /// another wrapper. + /// + /// Returns `None` if the wrapper is not present. While a wrapper's lifecycle hook is + /// running, that active wrapper remains registered but is temporarily unavailable through + /// this method; peer wrappers remain available. To merely check registration, use + /// `has_wrap` instead. + pub fn get_wrap(&self) -> Option<&W> { + let typeid = ::std::any::TypeId::of::(); + self.wrapper_registry() + .wrappers + .get(&typeid) + .and_then(Option::as_deref) + .map(|wrapper| { + wrapper + .as_any() + .downcast_ref() + .expect("downcasting is guaranteed to succeed due to wrap()'s internals") + }) + } + } + + impl From<$command> for crate::command::Command<$backend> { + fn from(command: $command) -> Self { + Self::from_native(command) + } + } + + /// A trait for adding functionality to a command. + /// + /// This trait provides extension or hook points into the lifecycle of a command. See the + /// [crate-level doc](crate) for an overview. + /// + /// All methods are optional, so a minimal impl may be: + /// + /// ```rust,ignore + /// #[derive(Debug)] + /// pub struct YourWrapper; + #[doc = concat!("impl ", stringify!(CommandWrapper), " for YourWrapper {}\n```")] + pub trait CommandWrapper: ::std::fmt::Debug + Send + Sync { + /// Called on a first instance if a second of the same type is added. + /// + /// Only one wrapper of a given type can exist within a Wrap at a time. By default, + /// later registrations are discarded. In some cases it is useful to merge their + /// configuration instead. This method is called on the stored wrapper with the newly + /// registered wrapper of the same concrete type. + /// + /// Because `other` is `Self`, implementations can inspect or move its type-specific + /// fields directly without downcasting. + /// + /// Default impl: no-op. + fn extend(&mut self, _other: Self) + where + Self: Sized, + { + } + + /// Called before the command is spawned, to mutate it as needed. + /// + /// This is where to modify the native command for one spawn attempt. It also gives mutable + /// access to the wrapper instance, so state can be stored if needed. The `core` + /// reference gives access to data from other wrappers; for example, that's how + /// `CreationFlags` on Windows works along with `JobObject`. + /// + /// Default impl: no-op. + fn pre_spawn( + &mut self, + _command: &mut $command, + _core: &CommandWrap, + ) -> ::std::io::Result<()> { + Ok(()) + } + + /// Called after spawn, but before the child is wrapped. + /// + /// The `core` reference gives access to data from other wrappers; for example, that's + /// how `CreationFlags` on Windows works along with `JobObject`. + /// + /// Default: no-op. + fn post_spawn( + &mut self, + _command: &mut $command, + _child: &mut $child, + _core: &CommandWrap, + ) -> ::std::io::Result<()> { + Ok(()) + } + + /// Called to wrap a child into this command wrapper's child wrapper. + /// + /// If the wrapper needs to override the methods on Child, then it should create an + /// instance of its own type implementing `ChildWrapper` and return it here. Child wraps + /// are _in order_: you may end up with a `Foo(Bar(Child))` or a `Bar(Foo(Child))` + /// depending on if `.wrap(Foo).wrap(Bar)` or `.wrap(Bar).wrap(Foo)` was called. + /// + /// The `core` reference gives access to data from other wrappers; for example, that's + /// how `CreationFlags` on Windows works along with `JobObject`. + /// + /// Default: no-op (ie, returns the child unchanged). + fn wrap_child( + &mut self, + child: Box, + _core: &CommandWrap, + ) -> ::std::io::Result> { + Ok(child) + } + } + }; } pub(crate) use Wrap; diff --git a/src/lib.rs b/src/lib.rs index 792df40..fdfef00 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//! Composable wrappers over process::Command. +//! Composable process command wrappers. //! //! # Quick start //! @@ -11,7 +11,7 @@ //! # fn main() -> std::io::Result<()> { //! use process_wrap::std::*; //! -//! let mut command = CommandWrap::with_new("watch", |command| { command.arg("ls"); }); +//! let mut command = Command::with_new("watch", |command| { command.arg("ls"); }); //! #[cfg(unix)] { command.wrap(ProcessGroup::leader()); } //! #[cfg(windows)] { command.wrap(JobObject); } //! let mut child = command.spawn()?; @@ -27,42 +27,55 @@ //! //! # Overview //! -//! This crate provides a composable set of wrappers over `process::Command` (either from std or -//! from Tokio). It is a more flexible and composable successor to the `command-group` crate, and is -//! meant to be adaptable to additional use cases: for example spawning processes in PTYs currently -//! requires a different crate (such as `pty-process`) which won't function with `command-group`. -//! Implementing a PTY wrapper for `process-wrap` would instead keep the same API and be composable -//! with the existing process group/session implementations. +//! This crate provides a composable process-wrap-owned [`Command`] configuration shared by the std +//! and Tokio frontends. It is a more flexible and composable successor to the `command-group` crate, +//! and is meant to be adaptable to additional use cases: for example spawning processes in PTYs +//! currently requires a different crate (such as `pty-process`) which won't function with +//! `command-group`. Implementing a PTY wrapper for `process-wrap` would instead keep the same API and +//! be composable with the existing process group/session implementations. //! //! # Usage //! -//! The core API is [`CommandWrap`](std::CommandWrap) and [`CommandWrap`](tokio::CommandWrap), -//! which can be constructed either directly from an existing `process::Command`: +//! The core APIs are `process_wrap::std::Command` and `process_wrap::tokio::Command`. Both are +//! aliases for one backend-typed command family: construction and configuration are shared, while +//! spawning and child behavior use the selected frontend. `CommandWrap` remains an alias in both +//! modules for compatibility. //! //! ```rust //! use process_wrap::std::*; -//! use std::process::Command; //! let mut command = Command::new("ls"); //! command.arg("-l"); -//! let mut command = CommandWrap::from(command); //! #[cfg(unix)] { command.wrap(ProcessGroup::leader()); } //! #[cfg(windows)] { command.wrap(JobObject); } //! ``` //! -//! ...or with a somewhat more ergonomic closure pattern: +//! The closure constructor remains available, and its inferred argument is now process-wrap's +//! command: //! //! ```rust //! use process_wrap::std::*; -//! let mut command = CommandWrap::with_new("ls", |command| { command.arg("-l"); }); +//! let mut command = Command::with_new("ls", |command| { command.arg("-l"); }); //! #[cfg(unix)] { command.wrap(ProcessGroup::leader()); } //! #[cfg(windows)] { command.wrap(JobObject); } //! ``` //! +//! Existing native commands can still be converted with `Command::from`. They retain exact native +//! behavior but are native-only: alternate portable transports cannot reconstruct arbitrary native +//! state. `native_mut()` is the explicit mutable escape hatch, and `into_native()` consumes the +//! process-wrap command when the rest of its lifecycle belongs to the native API. Stable native +//! configuration methods remain on the facade where their behavior can be preserved and make the +//! command native-only. The standard library's supplementary-groups setter remains unstable and is +//! not mirrored by the facade. Tokio's process-group setter is likewise omitted at the declared Tokio +//! floor because it cannot exactly replace process-group state already stored in a native-only Tokio +//! command. Use `ProcessGroup` for tracked Tokio commands, or configure a `std::process::Command` +//! before converting it into Tokio and process-wrap. An immutable Tokio `as_std()` view requires the +//! explicit `native_mut().as_std()` transition; Tokio 1.38.2 has no mutable inner-std accessor. +//! //! If targetting a single platform, then a fluent style is possible: //! //! ```rust //! use process_wrap::std::*; -//! CommandWrap::with_new("ls", |command| { command.arg("-l"); }) +//! Command::with_new("ls", |command| { command.arg("-l"); }) //! .wrap(ProcessGroup::leader()); //! ``` //! @@ -89,7 +102,7 @@ //! //! ```rust //! use process_wrap::tokio::*; -//! let mut command = CommandWrap::with_new("ls", |command| { command.arg("-l"); }); +//! let mut command = Command::with_new("ls", |command| { command.arg("-l"); }); //! command.wrap(KillOnDrop); //! ``` //! @@ -97,7 +110,7 @@ //! //! ```rust,ignore //! use process_wrap::std::*; -//! let mut command = CommandWrap::with_new("ls", |command| { command.arg("-l"); }); +//! let mut command = Command::with_new("ls", |command| { command.arg("-l"); }); //! command.wrap(CreationFlags(CREATE_NO_WINDOW)); //! ``` //! @@ -109,12 +122,12 @@ //! # Extension //! //! The crate is designed to be extensible, and new wrappers can be added by implementing the -//! required traits. The std and Tokio sides are completely separate, due to the different -//! underlying APIs. Of course you can (and should) re-use/share code wherever possible if -//! implementing both. +//! required traits. Command configuration is shared, but std and Tokio wrapper traits remain +//! separate because their spawn and child APIs differ. Re-use shared policy code when implementing +//! both frontends. //! -//! At minimum, you must implement [`CommandWrapper`](crate::std::CommandWrapper) and/or -//! [`CommandWrapper`](crate::tokio::CommandWrapper). These provide the same functionality +//! At minimum, you must implement `process_wrap::std::CommandWrapper` and/or +//! `process_wrap::tokio::CommandWrapper`. These provide the same functionality //! (and indeed internally are generated using a common macro), but differ in the exact types used. //! Here's the most basic impl (shown for Tokio): //! @@ -132,19 +145,19 @@ //! incorporate all or part of the second, concretely typed wrapper. By default, this does nothing //! (that is, only the first registered wrapper instance of a type applies). //! -//! - **`fn pre_spawn(&mut self, command: &mut Command, core: &CommandWrap)`** is called before -//! the command is spawned, and gives mutable access to it. It also gives mutable access to the -//! wrapper instance, so state can be stored if needed. The `core` reference gives access to data -//! from other wrappers; for example, that's how `CreationFlags` on Windows works along with -//! `JobObject`. By default does nothing. +//! - **`fn pre_spawn(&mut self, command: &mut tokio::process::Command, core: &Command)`** is called +//! before the command is spawned, and gives mutable access to that attempt's native command. It +//! also gives mutable access to the wrapper instance, so state can be stored if needed. The `core` +//! reference gives access to data from other wrappers; for example, that's how `CreationFlags` on +//! Windows works along with `JobObject`. By default does nothing. //! -//! - **`fn post_spawn(&mut self, child: &mut tokio::process::Child, core: &CommandWrap)`** is -//! called after spawn, and should be used for any necessary cleanups. It is offered for +//! - **`fn post_spawn(&mut self, command: &mut tokio::process::Command, child: &mut tokio::process::Child, core: &Command)`** +//! is called after spawn, and should be used for any necessary cleanups. It is offered for //! completeness but is expected to be less used than `wrap_child()`. By default does nothing. //! -//! - **`fn wrap_child(&mut self, child: Box, core: &CommandWrap)`** is +//! - **`fn wrap_child(&mut self, child: Box, core: &Command)`** is //! called after all `post_spawn()`s have run. If your wrapper needs to override the methods on -//! Child, then it should create an instance of its own type implementing `TokioChildWrapper` and +//! Child, then it should create an instance of its own type implementing `ChildWrapper` and //! return it here. Child wraps are _in order_: you may end up with a `Foo(Bar(Child))` or a //! `Bar(Foo(Child))` depending on if `.wrap(Foo).wrap(Bar)` or `.wrap(Bar).wrap(Foo)` was called. //! If your functionality is order-dependent, make sure to specify so in your documentation! By @@ -193,7 +206,7 @@ //! when calling `.wait()` on the `ChildWrapper`. //! //! ```rust -//! # use process_wrap::std::{ChildWrapper, CommandWrap, CommandWrapper}; +//! # use process_wrap::std::{ChildWrapper, Command as WrappedCommand, CommandWrap, CommandWrapper}; //! # use std::{ //! # fs::File, //! # io, mem, @@ -280,56 +293,22 @@ //! } //! ``` //! -//! Now we're cleaning up after ourselves, but there is one last issue: if you actually call -//! `.wait()`, then your program will deadlock! This is because `io::copy` copies data until `rx` -//! returns an EOF, but that only happens after *all* copies of `tx` are dropped. Currently, our -//! `Command` is holding onto `tx` even after calling `.spawn()`, so unless we manually drop the -//! `Command` (freeing both copies of `tx`) before calling `.wait()`, our program will deadlock! -//! We can fix this by telling `Command` to drop `tx` right after spawning the child — by this -//! point, the `ChildWrapper` will have already inherited the copies of `tx` that it needs, so -//! dropping `tx` from `Command` should be totally safe. We'll get `Command` to "drop" `tx` by -//! setting its `stdin` and `stdout` to `Stdio::null()` in `CommandWrapper::post_spawn()`. -//! -//! ```rust -//! # use process_wrap::std::{CommandWrap, CommandWrapper}; -//! # use std::{ -//! # io, -//! # path::PathBuf, -//! # process::{Child, Command, Stdio}, -//! # thread::JoinHandle, -//! # }; -//! # #[derive(Debug)] -//! # struct LogFile { -//! # path: PathBuf, -//! # thread: Option>, -//! # } -//! # -//! impl CommandWrapper for LogFile { -//! // ... snip ... -//! fn post_spawn( -//! &mut self, -//! command: &mut Command, -//! _child: &mut Child, -//! _core: &CommandWrap, -//! ) -> io::Result<()> { -//! command.stdout(Stdio::null()).stderr(Stdio::null()); -//! -//! Ok(()) -//! } -//! // ... snip ... -//! } -//! ``` +//! The tracked process-wrap command does not retain the `tx` handles from this hook. Each spawn uses +//! a fresh native attempt command, and that attempt is dropped before `spawn()` returns. The child has +//! already inherited the descriptors it needs, so the background reader sees EOF once the child and +//! its descendants release their copies. Native-only commands retain their native command state by +//! definition; wrappers which install one-attempt resources should therefore use tracked commands. //! //! Finally, we can test that our new command-wrapper works: //! //! ```rust -//! # use process_wrap::std::{ChildWrapper, CommandWrap, CommandWrapper}; +//! # use process_wrap::std::{ChildWrapper, Command as WrappedCommand, CommandWrap, CommandWrapper}; //! # use std::{ //! # error::Error, //! # fs::{self, File}, //! # io, mem, //! # path::PathBuf, -//! # process::{Child, Command, ExitStatus, Stdio}, +//! # process::{Child, Command, ExitStatus}, //! # thread::{self, JoinHandle}, //! # }; //! # use tempfile::NamedTempFile; @@ -361,17 +340,6 @@ //! # Ok(()) //! # } //! # -//! # fn post_spawn( -//! # &mut self, -//! # command: &mut Command, -//! # _child: &mut Child, -//! # _core: &CommandWrap, -//! # ) -> io::Result<()> { -//! # command.stdout(Stdio::null()).stderr(Stdio::null()); -//! # -//! # Ok(()) -//! # } -//! # //! # fn wrap_child( //! # &mut self, //! # child: Box, @@ -424,11 +392,11 @@ //! # //! fn main() -> Result<(), Box> { //! #[cfg(windows)] -//! let mut command = CommandWrap::with_new("cmd", |command| { +//! let mut command = WrappedCommand::with_new("cmd", |command| { //! command.args(["/c", "echo Hello && echo World 1>&2"]); //! }); //! #[cfg(unix)] -//! let mut command = CommandWrap::with_new("sh", |command| { +//! let mut command = WrappedCommand::with_new("sh", |command| { //! command.args(["-c", "echo Hello && echo World 1>&2"]); //! }); //! @@ -472,8 +440,13 @@ #![cfg_attr(docsrs, feature(doc_cfg))] #![warn(missing_docs)] +mod command; pub(crate) mod generic_wrap; +pub use command::Command; +#[doc(hidden)] +pub use command::{Backend, Blocking, NativeCommand, Tokio1}; + #[cfg(feature = "std")] pub mod std; diff --git a/src/std.rs b/src/std.rs index 8f9451a..f05b6aa 100644 --- a/src/std.rs +++ b/src/std.rs @@ -9,7 +9,7 @@ //! ``` #[doc(inline)] -pub use core::{ChildWrapper, CommandWrap, CommandWrapper}; +pub use core::{ChildWrapper, Command, CommandWrap, CommandWrapper}; #[cfg(all(windows, feature = "creation-flags"))] #[doc(inline)] pub use creation_flags::CreationFlags; diff --git a/src/std/core.rs b/src/std/core.rs index 90c000b..e01ceeb 100644 --- a/src/std/core.rs +++ b/src/std/core.rs @@ -1,7 +1,9 @@ use std::{ any::Any, io::{Read, Result}, - process::{Child, ChildStderr, ChildStdin, ChildStdout, Command, ExitStatus, Output}, + process::{ + Child, ChildStderr, ChildStdin, ChildStdout, Command as NativeCommand, ExitStatus, Output, + }, }; #[cfg(windows)] @@ -13,7 +15,13 @@ use nix::{ unistd::Pid, }; -crate::generic_wrap::Wrap!(Command, Child, ChildWrapper, |child| child); +crate::generic_wrap::Wrap!( + crate::Blocking, + NativeCommand, + Child, + ChildWrapper, + |child| child +); /// Wrapper for `std::process::Child`. /// @@ -375,7 +383,6 @@ fn read2( err_v: &mut Vec, ) -> Result<()> { use nix::{ - errno::Errno, libc, poll::{PollFd, PollFlags, PollTimeout, poll}, }; @@ -428,6 +435,8 @@ fn read2( #[cfg(target_os = "linux")] fn set_nonblocking(fd: BorrowedFd, nonblocking: bool) -> Result<()> { + use nix::errno::Errno; + let v = nonblocking as libc::c_int; let res = unsafe { libc::ioctl(fd.as_raw_fd(), libc::FIONBIO, &v) }; diff --git a/src/tokio.rs b/src/tokio.rs index 4eb8f75..36bd61d 100644 --- a/src/tokio.rs +++ b/src/tokio.rs @@ -9,7 +9,7 @@ //! ``` #[doc(inline)] -pub use core::{ChildWrapper, CommandWrap, CommandWrapper}; +pub use core::{ChildWrapper, Command, CommandWrap, CommandWrapper}; #[cfg(all(windows, feature = "creation-flags"))] #[doc(inline)] pub use creation_flags::CreationFlags; diff --git a/src/tokio/core.rs b/src/tokio/core.rs index 2859aa7..c7e40c1 100644 --- a/src/tokio/core.rs +++ b/src/tokio/core.rs @@ -17,10 +17,12 @@ use nix::{ }; use tokio::{ io::{AsyncRead, AsyncReadExt}, - process::{Child, ChildStderr, ChildStdin, ChildStdout, Command}, + process::{Child, ChildStderr, ChildStdin, ChildStdout, Command as NativeCommand}, }; -crate::generic_wrap::Wrap!(Command, Child, ChildWrapper, |child| child); +crate::generic_wrap::Wrap!(crate::Tokio1, NativeCommand, Child, ChildWrapper, |child| { + child +}); /// Wrapper for `tokio::process::Child`. /// diff --git a/src/tokio/process_group.rs b/src/tokio/process_group.rs index 17c0a94..6b380a5 100644 --- a/src/tokio/process_group.rs +++ b/src/tokio/process_group.rs @@ -85,7 +85,7 @@ impl ProcessGroupChild { impl CommandWrapper for ProcessGroup { #[cfg_attr(feature = "tracing", instrument(level = "debug", skip(self)))] fn pre_spawn(&mut self, command: &mut Command, _core: &CommandWrap) -> Result<()> { - command.process_group(self.leader.as_raw()); + crate::command::tokio_process_group(command, self.leader.as_raw()); Ok(()) } diff --git a/tests/command_facade.rs b/tests/command_facade.rs new file mode 100644 index 0000000..09bfcf9 --- /dev/null +++ b/tests/command_facade.rs @@ -0,0 +1,487 @@ +#[cfg(feature = "std")] +mod std_frontend { + use std::{ffi::OsStr, io}; + + use process_wrap::std::{Command, CommandWrap, CommandWrapper}; + + #[derive(Debug)] + struct AttemptArgument; + + impl CommandWrapper for AttemptArgument { + fn pre_spawn( + &mut self, + command: &mut std::process::Command, + _core: &CommandWrap, + ) -> io::Result<()> { + command.arg("attempt"); + Ok(()) + } + } + + #[derive(Debug)] + struct InspectFacade; + + impl CommandWrapper for InspectFacade { + fn pre_spawn( + &mut self, + _command: &mut std::process::Command, + core: &CommandWrap, + ) -> io::Result<()> { + assert_eq!(core.command().get_program(), OsStr::new("tool")); + assert_eq!( + core.command().get_args().collect::>(), + [OsStr::new("native")] + ); + Ok(()) + } + } + + #[derive(Debug)] + struct Marker; + + impl CommandWrapper for Marker {} + + #[test] + fn inferred_with_new_uses_process_wrap_command() { + let mut command = Command::with_new("tool", |command| { + command.arg("first").env("MODE", "tracked"); + }); + command.command_mut().arg("second"); + + assert_eq!(command.command().get_program(), OsStr::new("tool")); + assert_eq!( + command.get_args().collect::>(), + [OsStr::new("first"), OsStr::new("second")] + ); + + let alias: CommandWrap = command; + assert_eq!(alias.get_program(), OsStr::new("tool")); + } + + #[test] + fn into_command_discards_wrappers() { + let mut command = Command::new("tool"); + command.wrap(Marker); + assert!(command.has_wrap::()); + + let command = command.into_command(); + assert!(!command.has_wrap::()); + } + + #[test] + fn tracked_attempt_mutations_do_not_accumulate() { + let mut command = Command::with_new("tool", |command| { + command.arg("base"); + }); + command.wrap(AttemptArgument); + + for _ in 0..2 { + let error = command + .spawn_with(|native| { + assert_eq!( + native.get_args().collect::>(), + [OsStr::new("base"), OsStr::new("attempt")] + ); + native.arg("one-off"); + Err(io::Error::other("expected test error")) + }) + .unwrap_err(); + assert_eq!(error.to_string(), "expected test error"); + } + + assert_eq!(command.get_args().collect::>(), [OsStr::new("base")]); + } + + #[test] + fn native_only_facade_remains_readable_during_hooks() { + let mut native = std::process::Command::new("tool"); + native.arg("native"); + let mut command = Command::from(native); + command.wrap(InspectFacade); + + let error = command + .spawn_with(|_| Err(io::Error::other("expected test error"))) + .unwrap_err(); + assert_eq!(error.to_string(), "expected test error"); + } + + #[test] + fn native_only_commands_preserve_exact_mutations() { + let mut native = std::process::Command::new("tool"); + native.arg("native"); + let mut command = Command::from(native); + command.arg("facade"); + command.native_mut().arg("escape"); + + command + .spawn_with(|native| { + assert_eq!( + native.get_args().collect::>(), + [ + OsStr::new("native"), + OsStr::new("facade"), + OsStr::new("escape") + ] + ); + native.arg("persisted-attempt"); + Err(io::Error::other("expected test error")) + }) + .unwrap_err(); + + command + .spawn_with(|native| { + assert_eq!( + native.get_args().collect::>(), + [ + OsStr::new("native"), + OsStr::new("facade"), + OsStr::new("escape"), + OsStr::new("persisted-attempt") + ] + ); + Err(io::Error::other("expected test error")) + }) + .unwrap_err(); + } + + #[test] + fn tracked_environment_and_cwd_materialize_exactly() { + let cwd = std::env::current_dir().unwrap(); + let mut command = Command::new("tool"); + command + .env("PROCESS_WRAP_REMOVED", "before-clear") + .env_clear() + .env("PROCESS_WRAP_PRESENT", "after-clear") + .env_remove("PROCESS_WRAP_ABSENT") + .current_dir(&cwd); + + let tracked_env = command.get_envs().collect::>(); + assert_eq!( + tracked_env, + [( + OsStr::new("PROCESS_WRAP_PRESENT"), + Some(OsStr::new("after-clear")) + )] + ); + + command + .spawn_with(|native| { + let env = native.get_envs().collect::>(); + assert!( + !env.iter() + .any(|(key, _)| *key == OsStr::new("PROCESS_WRAP_REMOVED")) + ); + assert!(env.iter().any(|(key, value)| { + *key == OsStr::new("PROCESS_WRAP_PRESENT") + && *value == Some(OsStr::new("after-clear")) + })); + assert!( + !env.iter() + .any(|(key, _)| *key == OsStr::new("PROCESS_WRAP_ABSENT")) + ); + assert_eq!(native.get_current_dir(), Some(cwd.as_path())); + Err(io::Error::other("expected test error")) + }) + .unwrap_err(); + } + + #[cfg(unix)] + #[test] + fn unix_native_methods_remain_available() { + let mut command = Command::new("tool"); + command.uid(0).gid(0).arg0("argv-zero").process_group(0); + // SAFETY: the test callback performs no operations in the child. + unsafe { command.pre_exec(|| Ok(())) }; + + assert_eq!(command.native_mut().get_program(), OsStr::new("tool")); + } + + #[cfg(windows)] + #[test] + fn tracked_raw_arguments_preserve_order() { + let mut command = Command::new("tool"); + command.arg("regular-1").raw_arg(" raw ").arg("regular-2"); + + assert_eq!( + command.get_args().collect::>(), + [ + OsStr::new("regular-1"), + OsStr::new(" raw "), + OsStr::new("regular-2") + ] + ); + } + + #[cfg(windows)] + #[test] + fn windows_native_methods_and_environment_keys_remain_available() { + let mut command = Command::new("tool"); + command.creation_flags(0); + + let mut environment = Command::new("tool"); + environment + .env("Process_Wrap_Case", "first") + .env("PROCESS_WRAP_CASE", "second"); + assert_eq!( + environment.get_envs().collect::>(), + [(OsStr::new("PROCESS_WRAP_CASE"), Some(OsStr::new("second")))] + ); + } +} + +#[cfg(feature = "tokio1")] +mod tokio_frontend { + use std::{ffi::OsStr, io}; + + use process_wrap::tokio::{Command, CommandWrap, CommandWrapper}; + + #[derive(Debug)] + struct AttemptArgument; + + impl CommandWrapper for AttemptArgument { + fn pre_spawn( + &mut self, + command: &mut tokio::process::Command, + _core: &CommandWrap, + ) -> io::Result<()> { + command.arg("attempt"); + Ok(()) + } + } + + #[derive(Debug)] + struct InspectFacade; + + impl CommandWrapper for InspectFacade { + fn pre_spawn( + &mut self, + _command: &mut tokio::process::Command, + core: &CommandWrap, + ) -> io::Result<()> { + assert_eq!(core.command().get_program(), OsStr::new("tool")); + assert_eq!( + core.command().get_args().collect::>(), + [OsStr::new("native")] + ); + Ok(()) + } + } + + #[derive(Debug)] + struct Marker; + + impl CommandWrapper for Marker {} + + #[test] + fn inferred_with_new_uses_process_wrap_command() { + let mut command = Command::with_new("tool", |command| { + command.arg("first").env("MODE", "tracked"); + }); + command.command_mut().arg("second"); + + assert_eq!(command.command().get_program(), OsStr::new("tool")); + assert_eq!( + command.get_args().collect::>(), + [OsStr::new("first"), OsStr::new("second")] + ); + + let alias: CommandWrap = command; + assert_eq!(alias.get_program(), OsStr::new("tool")); + } + + #[test] + fn into_command_discards_wrappers() { + let mut command = Command::new("tool"); + command.wrap(Marker); + assert!(command.has_wrap::()); + + let command = command.into_command(); + assert!(!command.has_wrap::()); + } + + #[test] + fn tracked_attempt_mutations_do_not_accumulate() { + let mut command = Command::with_new("tool", |command| { + command.arg("base"); + }); + command.wrap(AttemptArgument); + + for _ in 0..2 { + let error = command + .spawn_with(|native| { + assert_eq!( + native.as_std().get_args().collect::>(), + [OsStr::new("base"), OsStr::new("attempt")] + ); + native.arg("one-off"); + Err(io::Error::other("expected test error")) + }) + .unwrap_err(); + assert_eq!(error.to_string(), "expected test error"); + } + + assert_eq!(command.get_args().collect::>(), [OsStr::new("base")]); + } + + #[test] + fn native_only_facade_remains_readable_during_hooks() { + let mut native = tokio::process::Command::new("tool"); + native.arg("native"); + let mut command = Command::from(native); + command.wrap(InspectFacade); + + let error = command + .spawn_with(|_| Err(io::Error::other("expected test error"))) + .unwrap_err(); + assert_eq!(error.to_string(), "expected test error"); + } + + #[test] + fn native_only_commands_preserve_exact_mutations() { + let mut native = tokio::process::Command::new("tool"); + native.arg("native"); + let mut command = Command::from(native); + command.arg("facade"); + command.native_mut().arg("escape"); + + command + .spawn_with(|native| { + assert_eq!( + native.as_std().get_args().collect::>(), + [ + OsStr::new("native"), + OsStr::new("facade"), + OsStr::new("escape") + ] + ); + native.arg("persisted-attempt"); + Err(io::Error::other("expected test error")) + }) + .unwrap_err(); + + command + .spawn_with(|native| { + assert_eq!( + native.as_std().get_args().collect::>(), + [ + OsStr::new("native"), + OsStr::new("facade"), + OsStr::new("escape"), + OsStr::new("persisted-attempt") + ] + ); + Err(io::Error::other("expected test error")) + }) + .unwrap_err(); + } + + #[test] + fn tracked_environment_and_cwd_materialize_exactly() { + let cwd = std::env::current_dir().unwrap(); + let mut command = Command::new("tool"); + command + .env("PROCESS_WRAP_REMOVED", "before-clear") + .env_clear() + .env("PROCESS_WRAP_PRESENT", "after-clear") + .env_remove("PROCESS_WRAP_ABSENT") + .current_dir(&cwd); + + let tracked_env = command.get_envs().collect::>(); + assert_eq!( + tracked_env, + [( + OsStr::new("PROCESS_WRAP_PRESENT"), + Some(OsStr::new("after-clear")) + )] + ); + + command + .spawn_with(|native| { + let env = native.as_std().get_envs().collect::>(); + assert!( + !env.iter() + .any(|(key, _)| *key == OsStr::new("PROCESS_WRAP_REMOVED")) + ); + assert!(env.iter().any(|(key, value)| { + *key == OsStr::new("PROCESS_WRAP_PRESENT") + && *value == Some(OsStr::new("after-clear")) + })); + assert!( + !env.iter() + .any(|(key, _)| *key == OsStr::new("PROCESS_WRAP_ABSENT")) + ); + assert_eq!(native.as_std().get_current_dir(), Some(cwd.as_path())); + Err(io::Error::other("expected test error")) + }) + .unwrap_err(); + } + + #[test] + fn tokio_native_methods_remain_available() { + let mut command = Command::new("tool"); + command.kill_on_drop(false); + assert_eq!( + command.native_mut().as_std().get_program(), + OsStr::new("tool") + ); + } + + #[cfg(unix)] + #[test] + fn unix_native_methods_remain_available() { + let mut command = Command::new("tool"); + command.uid(0).gid(0).arg0("argv-zero"); + // SAFETY: the test callback performs no operations in the child. + unsafe { command.pre_exec(|| Ok(())) }; + + assert_eq!( + command.native_mut().as_std().get_program(), + OsStr::new("tool") + ); + } + + #[cfg(windows)] + #[test] + fn tracked_raw_arguments_preserve_order() { + let mut command = Command::new("tool"); + command.arg("regular-1").raw_arg(" raw ").arg("regular-2"); + + assert_eq!( + command.get_args().collect::>(), + [ + OsStr::new("regular-1"), + OsStr::new(" raw "), + OsStr::new("regular-2") + ] + ); + } + + #[cfg(windows)] + #[test] + fn windows_native_methods_and_environment_keys_remain_available() { + let mut command = Command::new("tool"); + command.creation_flags(0); + + let mut environment = Command::new("tool"); + environment + .env("Process_Wrap_Case", "first") + .env("PROCESS_WRAP_CASE", "second"); + assert_eq!( + environment.get_envs().collect::>(), + [(OsStr::new("PROCESS_WRAP_CASE"), Some(OsStr::new("second")))] + ); + } +} + +#[cfg(all(feature = "std", feature = "tokio1"))] +#[test] +fn both_frontend_aliases_coexist() { + let blocking = process_wrap::std::Command::new("blocking"); + let asynchronous = process_wrap::tokio::Command::new("asynchronous"); + + assert_eq!(blocking.get_program(), std::ffi::OsStr::new("blocking")); + assert_eq!( + asynchronous.get_program(), + std::ffi::OsStr::new("asynchronous") + ); +}