Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 52 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?;
Expand All @@ -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?;
Expand All @@ -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?;
Expand All @@ -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()?;
Expand All @@ -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
Expand All @@ -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()?;
```
Expand All @@ -122,15 +153,15 @@ 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()?;
```

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()?;
```
Expand All @@ -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()?;
```
Expand All @@ -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()?;
```
Expand All @@ -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()?;
Expand All @@ -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()?;
Expand All @@ -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.
Expand All @@ -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<dyn ChildWrapper>, core: &CommandWrap)`** is
- **`fn wrap_child(&mut self, child: Box<dyn ChildWrapper>, 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))`
Expand Down
111 changes: 111 additions & 0 deletions docs/plans/command-api-and-pty-provider.md
Original file line number Diff line number Diff line change
@@ -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鈥檚 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.
Loading
Loading