processkit 2.2.2

Async child-process management for tokio: whole-tree kill-on-drop (no orphans), plus streaming, pipelines, timeouts, and supervision
Documentation
[package]
name = "processkit"
version = "2.2.2"
edition = "2024"
description = "Async child-process management for tokio: whole-tree kill-on-drop (no orphans), plus streaming, pipelines, timeouts, and supervision"
license = "MIT"
repository = "https://github.com/ZelAnton/ProcessKit-rs"
documentation = "https://docs.rs/processkit"
homepage = "https://github.com/ZelAnton/ProcessKit-rs"
readme = "README.md"
# Up to 5 keywords (≤20 chars each): high-volume terms people actually search,
# not niche mechanism names (job-object/cgroup live in the description instead).
keywords = ["process", "subprocess", "tokio", "async", "process-group"]
# Up to 5 *valid* crates.io category slugs (https://crates.io/category_slugs).
# Was mis-tagged `command-line-utilities` — that slug is for CLI *binaries*, not
# a library; replaced with the OS-API subcategories matching the cgroup /
# Job Object / process-group machinery, plus `concurrency`.
categories = [
  "asynchronous",
  "os",
  "os::unix-apis",
  "os::windows-apis",
  "concurrency",
]
# Keep the published crate lean: ship code, docs, and license — not the
# dev/CI/agent tooling that only matters in the repo. (.claude/, ideas/, and
# decisions/ aren't listed here: they're untracked — see the global gitignore —
# so cargo's git-tracked-files package source never sees them regardless.)
exclude = [
  ".github/",
  ".editorconfig",
  ".gitattributes",
  "AGENTS.md",
  "CLAUDE.md",
  "cliff.toml",
  "deny.toml",
  "rust-toolchain.toml",
  # cargo-fuzz harness (nightly-only, its own Cargo.toml/Cargo.lock) — dev
  # tooling that never ships in the published crate; see fuzz/Cargo.toml and
  # the fuzzing-only entry point it drives, `pump::fuzz_decode_pump_lines`
  # (`cfg(fuzzing)`, src/pump.rs).
  "fuzz/",
  # 3.5 MB README banner — rendered from its absolute raw.githubusercontent URL,
  # so it never needs to ship inside the published crate.
  "cover.png",
]
# Minimum Supported Rust Version. 1.88 is required for `let`-chains
# (`if let … && let …`), used in the unix process-group/cgroup code. The `msrv`
# CI job verifies it — keep the two in sync; raise both when adopting a newer feature.
rust-version = "1.88"

# Build docs.rs with every feature so the optional surfaces — `limits`
# (ResourceLimits), the `mock` MockRunner, the `tracing` integration — are
# documented (default features alone hide them).
[package.metadata.docs.rs]
all-features = true
# `--cfg docsrs` activates `feature(doc_cfg)` (see src/lib.rs) so the rendered
# docs carry per-item "Available on crate feature `X`" badges.
rustdoc-args = ["--cfg", "docsrs"]

# Every public type must be `Debug` (consumers derive Debug on structs holding
# ours); warn-level here becomes a hard error under CI's `-D warnings`.
[lints.rust]
missing_debug_implementations = "warn"
# All `unsafe` lives in explicit `unsafe {}` blocks inside safe fns (the FFI in
# `sys/`); there are no `unsafe fn` today. Deny the day someone adds one and
# forgets to keep that discipline. (`missing_docs` is enforced lib-scoped in
# `src/lib.rs` so it doesn't burden examples/tests.)
unsafe_op_in_unsafe_fn = "deny"
# `cfg(fuzzing)` is not a Cargo feature: `cargo fuzz build` sets it
# automatically (via `RUSTFLAGS`) for every crate in the dependency graph, so
# `fuzz/fuzz_targets/*.rs` can call into a fuzzing-only entry point (see
# `src/pump.rs`'s `fuzz_decode_pump_lines`) without that entry point ever
# appearing in an ordinary build's public API. Declare it so rustc's
# `unexpected_cfgs` lint (which `-D warnings` would otherwise turn into a
# build failure on every normal `cargo build`/`clippy`) does not fire on it.
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(fuzzing)"] }

# The most valuable pedantic-clippy slice for an error-rich crate (Rust API
# Guidelines C-FAILURE): every public fallible method must document its error
# conditions and every public method that can panic must document when. warn
# here, hard error under CI's `-D warnings`. The crate's error invariants
# (Timeout captured vs Cancelled raised, NotReady ≠ Timeout) live in the guides;
# these lints force them into the per-item rustdoc docs.rs shows first.
[lints.clippy]
missing_errors_doc = "warn"
missing_panics_doc = "warn"

# ---------------------------------------------------------------------------
# Dependencies — every entry carries a "why" (see AGENTS.md "Dependency
# management"). Pin major versions, enable only the features used, keep
# Cargo.lock committed.
# ---------------------------------------------------------------------------
[dependencies]
# Async runtime + child-process management: `process` spawns children,
# `time` enforces timeouts, `io-util` streams stdin/stdout line-by-line,
# `rt` powers the background stderr-drain task, `macros` for `#[tokio::main]`
# in examples, `sync` for the oneshot/Notify used in shutdown coordination,
# `fs` for the async file handle behind `Stdin::from_file`,
# `net` for the TcpStream the `wait_for_port` readiness probe connects with.
# `1.39` floor: two of our call sites need APIs stabilized well above `1.0` —
# `JoinHandle::abort_handle` (`running/mod.rs`, `pipeline.rs`; added in
# `1.27`) and `Command::as_std_mut` (`command.rs`, `sys/windows.rs`; added in
# `1.39`). Below `1.39` those fail to compile (`E0624`/`E0599`) rather than
# just missing a feature flag — this floor makes the declared minimum honest.
# (It also sidesteps an unrelated build trap: below `1.27`ish, tokio's
# declared `mio` floor can resolve down to a `mio` still depending on
# `ntapi 0.3.7`, which fails to compile under current rustc with `E0793`,
# unaligned packed-struct reference promoted to a hard error; `1.39` pulls
# `mio 1.x`, which dropped `ntapi` entirely.)
tokio = { version = "1.39", features = ["process", "time", "io-util", "rt", "macros", "sync", "fs", "net"] }
# `async fn` in the object-safe `ProcessRunner` trait so it stays `dyn`-able
# and mockable (native async-in-trait isn't object-safe on our MSRV). `0.1.9`
# floor: below it, async-trait's codegen for `impl<R: Trait> Trait for
# Generic<R>` references the outer impl's generic param from the inner
# `async fn`'s desugared body, which rustc rejects (`E0401`,
# "can't use generic parameters from outer item") — our `RecordingRunner<R>`
# and `&R` blanket impls need the fixed codegen.
async-trait = "0.1.9"
# Structured `Error` carrying program / exit code / stderr / timeout instead
# of stringly-typed failures; `#[non_exhaustive]` so variants can grow.
thiserror = "2"
# Decode child stdout/stderr in non-UTF-8 legacy encodings (Shift-JIS,
# Windows-1252, GBK, …) for the per-stream encoding-override option; default
# stays UTF-8.
encoding_rs = "0.8"
# Adapt tokio's `Lines` reader into an `impl Stream` for the streaming-output
# helper, so callers consume stdout with the standard `Stream` combinators.
# `io-util` gates the `wrappers::LinesStream` adapter we use.
tokio-stream = { version = "0.1.1", features = ["io-util"] }
# Optional per-run observability (program/exit code/duration — deliberately
# NOT argv or env, which routinely carry secrets). Off by default; zero cost
# unless the `tracing` feature is enabled. `0.1.6` floor: below it, the
# `target: "...", field = ?value, "message"` event-macro syntax our call
# sites use isn't supported yet (`fieldset!` recursion / "no rules expected"
# macro-expansion errors).
tracing = { version = "0.1.6", optional = true }
# Optional auto-generated `MockRunner` for downstream tests. Test-only; pulled
# in solely behind the `mock` feature, never in production builds.
mockall = { version = "0.15", optional = true }
# `CancellationToken` for first-class run cancellation (core, not optional):
# structured-concurrency cancellation that tears a run's tree down. The `sync`
# module (where CancellationToken lives) is always compiled in tokio-util — no
# inner feature needed; default-features = false pulls in nothing else.
tokio-util = { version = "0.7", default-features = false }
# Optional serialization for the `record` feature's JSON cassettes
# (RecordReplayRunner): `derive` for the cassette structs, serde_json for the
# human-diffable on-disk format. Never in production builds without `record`.
serde = { version = "1.0.166", features = ["derive"], optional = true }
serde_json = { version = "1.0.100", optional = true }

[features]
# Feature flags are *visibility* gates: additive, hiding API without changing the
# core's semantics — the kill-on-drop tree guarantee is unconditional in every
# configuration. (A broader visibility split was tried and deliberately rolled
# back — decision recorded in decisions/architecture-audit-2026-06.md; full
# analysis in git history, ideas/three-layer-resource-split.md.)
#
# The default is lean: `process-control` (pure code, no extra deps) is the one
# capability most process-management callers reach for. `stats` is *opt-in* — it
# carries a real build cost (on Windows a `windows-sys` FFI feature) and a
# specialized purpose (metrics), and the crate's core (spawn / contain / capture /
# stream / pipeline) never needs it. Enable it with `features = ["stats"]` (or
# `limits`, which implies it).
default = ["process-control"]
# Resource measurement: `ProcessGroupStats`, `ProcessGroup::stats`, the per-process
# `RunningProcess::cpu_time`/`peak_memory_bytes` diagnostics, and `RunProfile` /
# `RunningProcess::profile`. Opt-in: on Windows it enables a `windows-sys` FFI
# feature (the ProcessStatus API used solely for the peak-memory readout) — no
# extra crate, unlike `mock`/`tracing`/`record` — and metrics are a specialized
# add-on most callers don't need.
stats = ["windows-sys/Win32_System_ProcessStatus"]
# Whole-tree resource caps: `ResourceLimits`, the `max_memory`/`max_processes`/
# `cpu_quota` builders on `ProcessGroupOptions`, and `Error::ResourceLimit`.
# Implies `stats` as policy — resource read and write travel together toward the
# possible `processkit-resource` split (see ideas/) — not as a code dependency.
limits = ["stats"]
# Tree control beyond contain+kill: `Signal` and
# `ProcessGroup::{signal, suspend, resume, members, adopt}`. Gated because it
# matches the layer a future `processkit-sys` split would carve out.
process-control = []
# Expose the `mockall`-generated `MockRunner` for consumers' tests.
mock = ["dep:mockall"]
# Emit a `tracing` event per command run (program, exit code, duration —
# argv/env are deliberately not logged: they routinely carry secrets).
tracing = ["dep:tracing"]
# Record/replay cassettes over the `ProcessRunner` seam: `RecordReplayRunner`
# records real `Invocation → ProcessResult` pairs to a JSON fixture and replays
# them hermetically. Off by default; pulls in serde + serde_json.
record = ["dep:serde", "dep:serde_json"]

[target.'cfg(windows)'.dependencies]
# Win32 Job Object FFI for kill-on-close process trees: CreateJobObjectW /
# SetInformationJobObject / AssignProcessToJobObject / TerminateJobObject plus
# the HANDLE type & CloseHandle. Kept on the same major as tokio/mio's copy so
# the lockfile carries a single windows-sys.
windows-sys = { version = "0.61", features = [
  "Win32_Foundation",
  "Win32_System_JobObjects",
  # JOBOBJECT_EXTENDED_LIMIT_INFORMATION + GetProcessTimes (per-process CPU);
  # also CREATE_SUSPENDED / OpenThread / ResumeThread for race-free containment
  # (spawn suspended → assign to job → resume the primary thread).
  "Win32_System_Threading",
  # CreateJobObjectW's signature references SECURITY_ATTRIBUTES.
  "Win32_Security",
  # Win32_System_ProcessStatus (K32GetProcessMemoryInfo, peak working set) is
  # enabled by the `stats` feature above — it serves only the metrics readout.
  # Thread snapshot (CreateToolhelp32Snapshot / Thread32First/Next) to find a
  # suspended child's primary thread for ResumeThread — std/tokio expose only
  # the process handle, not the PROCESS_INFORMATION thread handle.
  "Win32_System_Diagnostics_ToolHelp",
] }

[target.'cfg(unix)'.dependencies]
# Raw syscalls std can't express: the POSIX process-group backend (setpgid /
# killpg / kill for teardown and liveness probes) on every unix, plus — on Linux
# — joining cgroup.procs in pre_exec (async-signal-safe getpid/open/write/close).
libc = "0.2.149"

[dev-dependencies]
# `#[tokio::test]` plus a multi-threaded runtime to drive the async tests;
# `test-util` provides the paused clock for the supervisor's backoff-timing
# tests (virtual time instead of real sleeps); `net` binds the ephemeral
# TcpListener the `wait_for_port` integration test probes against.
# Kept in sync with the `[dependencies]` tokio floor above (`1.39`, forced by
# the `abort_handle`/`as_std_mut`/`mio`-`ntapi` build-honesty fixes — see that
# entry for why).
tokio = { version = "1.39", features = ["macros", "rt-multi-thread", "time", "io-util", "test-util", "net"] }
# Auto-cleaned, parallel-safe temp files for the `record` cassette round-trip
# tests (hand-rolled temp_dir paths leak and race under parallel test runs).
tempfile = "3"
# Property-based tests over the line pump + `encoding_rs` decoder
# (`src/pump.rs`): generates arbitrary byte streams chunked at arbitrary read
# boundaries to check line/byte counters and multibyte reassembly hold for
# any chunking, and that no chunking makes the pump panic.
proptest = "1"
# Benchmark harness for `benches/pump.rs` — baselines the output pump's hot
# paths (line splitting, decoding, tee/handler) ahead of optimizing them.
# `async_tokio` runs an async benchmark body on our own tokio runtime instead
# of criterion's default synchronous bencher; `harness = false` on the
# `[[bench]]` entry below hands `main` to criterion instead of `libtest`.
criterion = { version = "0.8", features = ["async_tokio"] }

[[bench]]
name = "pump"
harness = false

[target.'cfg(windows)'.dev-dependencies]
# Liveness probe (OpenProcess) in the integration test that proves a grandchild
# is reaped with its job — integration tests can't see the non-dev windows-sys.
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Threading"] }

[target.'cfg(unix)'.dev-dependencies]
# Liveness probe (kill(pid, 0)) and the root gate (geteuid) in the setsid /
# privilege-drop integration tests — the test crate can't see the non-dev libc.
libc = "0.2.149"