processkit 1.0.0

Async child-process management for tokio: whole-tree kill-on-drop (no orphans), plus streaming, pipelines, timeouts, and supervision
Documentation
[package]
name = "processkit"
version = "1.0.0"
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.
exclude = [
  ".claude/",
  ".github/",
  "ideas/",
  "decisions/",
  ".editorconfig",
  ".gitattributes",
  "AGENTS.md",
  "CLAUDE.md",
  "cliff.toml",
  "deny.toml",
  "rust-toolchain.toml",
  # 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"

# ---------------------------------------------------------------------------
# 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.
tokio = { version = "1", 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).
async-trait = "0.1"
# 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", 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.
tracing = { version = "0.1", 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.14", 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", features = ["derive"], optional = true }
serde_json = { version = "1", 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
# is the one feature with a real build cost (a Windows FFI dependency) 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: it is the only feature pulling an extra
# dependency (on Windows, the ProcessStatus FFI used solely for the peak-memory
# readout), and metrics are a specialized add-on most callers don't need.
stats = ["windows-sys/Win32_System_ProcessStatus"]
# Whole-tree resource caps: `ResourceLimits`, the `memory_max`/`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"

[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.
tokio = { version = "1", 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"

[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"