Skip to main content

A Rust-backed subprocess wrapper with split stdout/stderr streaming

Project description

running-process

PyPI Crates.io codecov

running-process is what you wished python's subprocess was. Blazing fast, highly concurrent, huge feature list, dead process tracking, pty support. Built in Rust with a thin python api.

v1 Broker Docs

The v1 broker work is documented as a stable spec alongside the implementation:

Platform Build Lint Unit Test Integration Test
Linux x86 Build Lint Unit Test Integration Test
Linux ARM Build Lint Unit Test Integration Test
Windows x86 Build Lint Unit Test Integration Test
Windows ARM Build Lint Unit Test Integration Test
macOS x86 Build Lint Unit Test Integration Test
macOS ARM Build Lint Unit Test Integration Test

Why?

This project started off as a fix for python's sub process module. It was in python originally, but then moved to OS specific rust. Now it's blazing fast: using OS threads, atomics and proper signaling back to the python api. This library also allows stderr and stdout stream reading in parallel, something subprocess lacks. It also has cross platform process tracking, pty generation. It has zombie process tracking. It also has builtin expect for keyword event triggers, idle tracking (great for agent CLI's that dont' notifiy when they are done, they just stop sending data).

This libary is design for speed and correctness and portability. Usually terminal utilities are for windows or linux/mac. This is designed to run everywhere.

PTY Support Matrix

PTY support is a guaranteed part of the package contract on:

  • Windows
  • Linux
  • macOS

On those platforms, RunningProcess.pseudo_terminal(...), wait_for_expect(...), and wait_for_idle(...) are core functionality rather than optional extras.

Pty.is_available() remains as a compatibility shim and only reports False on unsupported platforms.

Terminal Graphics Capabilities

Rust callers can inspect terminal graphics support with running_process::current_terminal_capabilities() or the pure running_process::detect_terminal_capabilities(...) helper. The result reports Sixel, Kitty graphics, and iTerm2 File= image support as structured capability records with status, evidence, source, and risks metadata.

The detector intentionally distinguishes terminal hosts from shells. cmd.exe, PowerShell, Git Bash, bash, zsh, and fish are command interpreters; they do not prove graphics support. The terminal host or multiplexer does: Windows Terminal, xterm, foot, Konsole, WezTerm, Kitty, iTerm2, tmux, GNU screen, and similar programs provide the relevant evidence. Weak aliases such as TERM=xterm-256color are reported as unknown unless a live probe or stronger host signal confirms support.

CLI Helpers

The package installs a running-process wrapper CLI for supervised command execution:

running-process --timeout 30 -- python -m pytest tests/test_cli.py
running-process --find-leaks -- python worker.py

--find-leaks tags the wrapped process tree with a unique originator marker and reports any descendants still alive after the direct child exits.

Cleanup Manifests

The running-process-cleanup binary reads v1 broker CacheManifest files without requiring a broker or daemon to be running. Manifests are written in two places:

  • each daemon cache root: .running-process-manifest.pb
  • the central registry: $XDG_DATA_HOME/running-process/manifests/ on Linux, ~/Library/Application Support/running-process/manifests/ on macOS, and %APPDATA%\running-process\manifests\ on Windows

Destructive commands are dry-run by default. Add --confirm to delete selected roots:

running-process-cleanup list --json
running-process-cleanup verify --json
running-process-cleanup prune --dormant-after 30d
running-process-cleanup prune --dormant-after 30d --confirm
running-process-cleanup prune --keep-current --keep-last 2
running-process-cleanup uninstall zccache --keep-config
running-process-cleanup instances --json

For GitHub Actions cache restores, run verification after actions/cache@v4 restores daemon state. Manifests from a prior runner boot are reported as stale:

- uses: actions/cache@v4
  with:
    path: ~/.local/share/running-process
    key: running-process-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}

- name: Verify restored running-process manifests
  run: running-process-cleanup verify --json

Pipe-backed API

from running_process import RunningProcess

process = RunningProcess(
    ["python", "-c", "import sys; print('out'); print('err', file=sys.stderr)"]
)

process.wait()

print(process.stdout)          # stdout only
print(process.stderr)          # stderr only
print(process.combined_output) # combined compatibility view

Captured data values stay plain str | bytes. Live stream handles are exposed separately:

if process.stdout_stream.available():
    print(process.stdout_stream.drain())

Process priority is a first-class launch option:

from running_process import CpuPriority, RunningProcess

process = RunningProcess(
    ["python", "-c", "import time; time.sleep(1)"],
    nice=CpuPriority.LOW,
)

nice= behavior:

  • accepts either a raw int niceness or a platform-neutral CpuPriority
  • on Unix, it maps directly to process niceness
  • on Windows, positive values map to below-normal or idle priority classes and negative values map to above-normal or high priority classes
  • 0 leaves the default scheduler priority unchanged
  • positive values are the portable default; negative values may require elevated privileges
  • the enum intentionally stops at HIGH; there is no realtime tier

Available helpers:

  • get_next_stdout_line(timeout)
  • get_next_stderr_line(timeout)
  • get_next_line(timeout) for combined compatibility reads
  • stream_iter(timeout) or for stdout, stderr, exit_code in process
  • drain_stdout()
  • drain_stderr()
  • drain_combined()
  • stdout_stream.available()
  • stderr_stream.available()
  • combined_stream.available()

stream_iter(...) yields tuple-like ProcessOutputEvent(stdout, stderr, exit_code) records. Only one stream payload is populated per nonterminal item. When both pipes are drained, it yields (EOS, EOS, exit_code) if the child has already exited, or (EOS, EOS, None) followed by a final (EOS, EOS, exit_code) if the child closed both pipes before it exited.

RunningProcess.run(...) supports common subprocess.run(...) style cases including:

  • capture_output=True
  • text=True
  • encoding=...
  • errors=...
  • shell=True
  • env=...
  • nice=...
  • stdin=subprocess.DEVNULL
  • input=... in text or bytes form

Unsupported subprocess.run(...) kwargs now fail loudly instead of being silently ignored.

Expect API

expect(...) is available on both the pipe-backed and PTY-backed process APIs.

import re
import subprocess
from running_process import RunningProcess

process = RunningProcess(
    ["python", "-c", "print('prompt>'); import sys; print('echo:' + sys.stdin.readline().strip())"],
    stdin=subprocess.PIPE,
)

process.expect("prompt>", timeout=5, action="hello\n")
match = process.expect(re.compile(r"echo:(.+)"), timeout=5)
print(match.groups)

Supported action= forms:

  • str or bytes: write to stdin
  • "interrupt": send Ctrl-C style interrupt when supported
  • "terminate"
  • "kill"

Pipe-backed expect(...) matches line-delimited output. If the child writes prompts without trailing newlines, use the PTY API instead.

PTY API

Use RunningProcess.pseudo_terminal(...) for interactive terminal sessions. It is chunk-oriented by design and preserves carriage returns and terminal control flow instead of normalizing it away.

from running_process import ExpectRule, RunningProcess

pty = RunningProcess.pseudo_terminal(
    ["python", "-c", "import sys; sys.stdout.write('name?'); sys.stdout.flush(); print('hello ' + sys.stdin.readline().strip())"],
    text=True,
    expect=[ExpectRule("name?", "world\n")],
    expect_timeout=5,
)

print(pty.output)

PTY behavior:

  • accepts str and list[str] commands
  • auto-splits simple string commands into argv when shell syntax is not present
  • uses shell mode automatically when shell metacharacters are present
  • is guaranteed on supported Windows, Linux, and macOS builds
  • keeps output chunk-buffered by default
  • preserves \r for redraw-style terminal output
  • supports write(...), read(...), drain(), available(), expect(...), resize(...), and send_interrupt()
  • supports nice=... at launch
  • supports interrupt_and_wait(...) for staged interrupt escalation
  • supports wait_for_idle(...) with activity filtering
  • exposes exit_reason, interrupt_count, interrupted_by_caller, and exit_status

wait_for_idle(...) has two modes:

  • default fast path: built-in PTY activity rules and optional process metrics
  • slow path: IdleDetection(idle_reached=...), where your Python callback receives an IdleInfoDiff delta and returns IdleDecision.DEFAULT, IdleDecision.ACTIVE, IdleDecision.BEGIN_IDLE, or IdleDecision.IS_IDLE

There is also a compatibility alias: RunningProcess.psuedo_terminal(...).

Rust consumers should make the same transport choice explicitly: use NativeProcess for one-shot noninteractive work and InteractivePtySession / NativePtyProcess only for real terminal sessions. See Rust PTY guidance.

You can also inspect the intended interactive launch semantics without launching a child:

from running_process import RunningProcess

spec = RunningProcess.interactive_launch_spec("console_isolated")
print(spec.ctrl_c_owner)
print(spec.creationflags)

Supported launch specs:

  • pseudo_terminal
  • console_shared
  • console_isolated

For an actual launch, use RunningProcess.interactive(...):

process = RunningProcess.interactive(
    ["python", "-c", "print('hello from interactive mode')"],
    mode="console_shared",
    nice=5,
)
process.wait()

Abnormal Exits

By default, nonzero exits stay subprocess-like: you get a return code and can inspect exit_status.

process = RunningProcess(["python", "-c", "import sys; sys.exit(3)"])
process.wait()
print(process.exit_status)

If you want abnormal exits to raise, opt in:

from running_process import ProcessAbnormalExit, RunningProcess

try:
    RunningProcess.run(
        ["python", "-c", "import sys; sys.exit(3)"],
        capture_output=True,
        raise_on_abnormal_exit=True,
    )
except ProcessAbnormalExit as exc:
    print(exc.status.summary)

Notes:

  • keyboard interrupts still raise KeyboardInterrupt
  • kill -9 / SIGKILL is classified as an abnormal signal exit
  • possible OOM conditions are exposed as a hint on exit_status.possible_oom
  • OOM cannot be identified perfectly across platforms from exit status alone, so it is best-effort rather than guaranteed

Text and bytes

Pipe mode is byte-safe internally:

  • invalid UTF-8 does not break capture
  • text mode decodes with UTF-8 and errors="replace" by default
  • binary mode returns bytes unchanged
  • \r\n is normalized as a line break in pipe mode
  • bare \r is preserved

PTY mode is intentionally more conservative:

  • output is handled as chunks, not lines
  • redraw-oriented \r is preserved
  • no automatic terminal-output normalization is applied

Development

./install
./lint
./test

./install bootstraps rustup into the shared user locations (~/.cargo and ~/.rustup, or CARGO_HOME / RUSTUP_HOME if you override them), then installs the exact toolchain pinned in rust-toolchain.toml. Toolchain installs are serialized with a lock so concurrent repo bootstraps do not race the same shared version. Rust build commands run through uvx soldr, so there is no separate soldr install step to maintain.

./lint applies cargo fmt and Ruff autofixes before running the remaining lint checks, so fixable issues are rewritten in place.

./test runs the Rust tests, rebuilds the native extension with the unoptimized dev profile, runs the non-live Python tests, and then runs the @pytest.mark.live coverage that exercises real OS process and signal behavior.

On local developer machines, ./test also runs the Linux Docker preflight so Windows and macOS development catches Linux wheel, lint, and non-live pytest regressions before push. GitHub-hosted Actions skip that Docker-only preflight and run the native platform suite directly.

For a live-only test run with the timeout crash watchdog and automatic thread dumps still enabled, use:

uv run -m ci.test --live-only

For a narrower live-only selection, pass pytest targets and selectors through the same entrypoint:

uv run -m ci.test --live-only tests/test_pty_support.py interrupt

For direct Cargo build commands, use uvx soldr directly:

uvx soldr cargo check --workspace
uvx soldr cargo test --workspace
uvx soldr cargo package -p running-process --no-verify

Keep maturin, cargo fmt, and cargo clippy on their normal entrypoints. This repo's high-level scripts already choose the compatible path for those tools.

On Windows, native rebuilds that compile bundled C code should run from a Visual Studio developer shell. When the environment is ambiguous, point maturin at the MSVC toolchain binaries directly rather than relying on the generic cargo proxy.

For local extension rebuilds, prefer:

uv run build.py

That defaults to building a dev-profile wheel and reinstalling it into the repo's uv environment, which keeps the native extension in site-packages instead of copying it into src/. For publish-grade artifacts, use:

uv run build.py --release

Release

Releases are cut by the Auto Release GitHub Actions workflow. Bump project.version in pyproject.toml (and match workspace.package.version in Cargo.toml), push the commit to main, and the workflow will:

  • Build wheels for linux x86/arm, macOS x86/arm, and Windows x86/arm and publish them to PyPI via trusted publishing.
  • Publish running-process-{proto, core, client, py} to crates.io in dependency order (requires the repo secret CARGO_REGISTRY_TOKEN).
  • Build standalone runpm and running-process-daemon binaries for each target and attach them — alongside the wheels, install.sh, install.ps1, and SHA256SUMS — to a new GitHub Release.

You can also fire the workflow manually with gh workflow run auto-release.yml, or by pushing a vX.Y.Z tag.

The standalone binaries can be installed without pip:

curl -LsSf https://github.com/zackees/running-process/releases/latest/download/install.sh | sh
powershell -ExecutionPolicy Bypass -c "irm https://github.com/zackees/running-process/releases/latest/download/install.ps1 | iex"

Process Containment

ContainedProcessGroup ensures all child processes are killed when the group is dropped, using OS-level mechanisms (Job Objects on Windows, process groups + SIGKILL on Unix).

from running_process import ContainedProcessGroup

with ContainedProcessGroup() as group:
    proc = group.spawn(["sleep", "3600"])
# all children killed on exit, even on crash

Crash-resilient orphan discovery

When a parent crashes, its in-process registry is lost. ContainedProcessGroup can stamp every child with an environment variable that survives parent death:

from running_process import ContainedProcessGroup, find_processes_by_originator

# At launch: tag children with your tool name
with ContainedProcessGroup(originator="MYTOOL") as group:
    proc = group.spawn(["long-running-worker"])

# Later (from any process, any session): find orphans
stale = find_processes_by_originator("MYTOOL")
for info in stale:
    if not info.parent_alive:
        print(f"Orphaned PID {info.pid} from dead parent {info.parent_pid}")

The env var RUNNING_PROCESS_ORIGINATOR=TOOL:PID is inherited by all descendants. The scanner uses process start times to guard against PID reuse.

Detached Launches

Use launch_detached(...) when a caller needs to start a daemon-tracked shell command and return immediately:

from running_process import launch_detached

handle = launch_detached(
    "python worker.py",
    cwd=".",
    env={"WORKER_MODE": "background"},
    originator="mytool:session-1",
)
print(handle.pid)

This path uses the running-process daemon for launch/tracking. It is separate from running_process.daemon.spawn_daemon(...), which keeps the trampoline-based process-name behavior.

Tracked PID Cleanup

RunningProcess, InteractiveProcess, and PTY-backed launches register their live PIDs in a SQLite database. The default location is:

  • Windows: %LOCALAPPDATA%\\running-process\\tracked-pids.sqlite3
  • Override: RUNNING_PROCESS_PID_DB=/custom/path/tracked-pids.sqlite3

If a bad run leaves child processes behind, terminate everything still tracked in the database:

python scripts/terminate_tracked_processes.py

Notes

  • stdout and stderr are no longer merged by default.
  • combined_output exists for compatibility when you need the merged view.
  • RunningProcess(..., use_pty=True) is no longer the preferred path; use RunningProcess.pseudo_terminal(...) for PTY sessions.
  • On supported Windows builds, PTY support is provided by the native Rust extension rather than a Python winpty fallback.
  • The test suite checks that running_process.__version__, package metadata, and manifest versions stay in sync.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

running_process-4.4.0.tar.gz (721.7 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

running_process-4.4.0-cp310-abi3-win_arm64.whl (1.3 MB view details)

Uploaded CPython 3.10+Windows ARM64

running_process-4.4.0-cp310-abi3-win_amd64.whl (1.4 MB view details)

Uploaded CPython 3.10+Windows x86-64

running_process-4.4.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.7 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ x86-64

running_process-4.4.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (2.7 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ ARM64

running_process-4.4.0-cp310-abi3-macosx_11_0_arm64.whl (1.4 MB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

running_process-4.4.0-cp310-abi3-macosx_10_12_x86_64.whl (1.5 MB view details)

Uploaded CPython 3.10+macOS 10.12+ x86-64

File details

Details for the file running_process-4.4.0.tar.gz.

File metadata

  • Download URL: running_process-4.4.0.tar.gz
  • Upload date:
  • Size: 721.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for running_process-4.4.0.tar.gz
Algorithm Hash digest
SHA256 51ca97d17c1eb6a781fefa93be4b709408825d7e2f17a57dfe0f264f85359d61
MD5 194940fa58678592bb75bfcace1cbb2f
BLAKE2b-256 7de7e6bd05c0abcded143e5320b5583593a7923a1ab6d35347f3fd48682528d8

See more details on using hashes here.

Provenance

The following attestation bundles were made for running_process-4.4.0.tar.gz:

Publisher: auto-release.yml on zackees/running-process

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file running_process-4.4.0-cp310-abi3-win_arm64.whl.

File metadata

File hashes

Hashes for running_process-4.4.0-cp310-abi3-win_arm64.whl
Algorithm Hash digest
SHA256 dc76d0a60ff6a208a0917661f6ee9f4e01391c3785b915518ecaaa8158077a21
MD5 0da76dbf016330223adf81f23acec9a7
BLAKE2b-256 f4dcd5307e14c767b26dd90a5b4eaa961d63b8ad395f37be24823d7b3c7b9aa2

See more details on using hashes here.

Provenance

The following attestation bundles were made for running_process-4.4.0-cp310-abi3-win_arm64.whl:

Publisher: auto-release.yml on zackees/running-process

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file running_process-4.4.0-cp310-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for running_process-4.4.0-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 0e665d966bb160ee542345cb988066ba20d6ee9b2ee4de7501dbd2f3abf18e47
MD5 f486759940b43c6cd7ce9641133d7347
BLAKE2b-256 15b5b74d426adda17f37b6b9d96df14bc1d7c76446d2d54957542343b1cf915c

See more details on using hashes here.

Provenance

The following attestation bundles were made for running_process-4.4.0-cp310-abi3-win_amd64.whl:

Publisher: auto-release.yml on zackees/running-process

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file running_process-4.4.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for running_process-4.4.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 73dfe6c568dc3480ff286fc13447dec02e4742d4c4bf98d3b6b2e9cb6cad0c9c
MD5 304ed3d17b08ec4bbdf42f293781e056
BLAKE2b-256 1f02e4fa3f26416685fed383904f64cad9c8da174b22017eec6a28e63c2e0962

See more details on using hashes here.

Provenance

The following attestation bundles were made for running_process-4.4.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: auto-release.yml on zackees/running-process

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file running_process-4.4.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for running_process-4.4.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 9942ebbf4805f41af7656c8099fbcbf13ec03639ff25d0a4b3151640831894a1
MD5 801bf2dcfb97b79c06a012a2e2b919bd
BLAKE2b-256 0bd7b09a975e4773ca03605328032c3a3fb9e5ac3db875be7b3f75965520c025

See more details on using hashes here.

Provenance

The following attestation bundles were made for running_process-4.4.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: auto-release.yml on zackees/running-process

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file running_process-4.4.0-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for running_process-4.4.0-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 2f902fa79f5aac481b959e1cd314260936e782e728545afbd90543a23134efa2
MD5 ee90c4f9c4e3f2a604c72cd29c70ca34
BLAKE2b-256 98f51ed20306efc10fbf4148642e5e5c6eb9f88dd83925e4dc64063a828f4eaf

See more details on using hashes here.

Provenance

The following attestation bundles were made for running_process-4.4.0-cp310-abi3-macosx_11_0_arm64.whl:

Publisher: auto-release.yml on zackees/running-process

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file running_process-4.4.0-cp310-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for running_process-4.4.0-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 e37fac436c8d37c933e2eb9ae00bbd4160703f616b142265935ab8fc89a31136
MD5 4e80b1d75bb9446405237b518e6dc14b
BLAKE2b-256 7688773650081581af667e674360939a1ccc1f7ecb546649ccdf7b4f6ddc820c

See more details on using hashes here.

Provenance

The following attestation bundles were made for running_process-4.4.0-cp310-abi3-macosx_10_12_x86_64.whl:

Publisher: auto-release.yml on zackees/running-process

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page