A Rust-backed subprocess wrapper with split stdout/stderr streaming
Project description
running-process
running-process is a Rust-backed subprocess runtime with a thin Python API.
The pipe-backed API keeps stdout and stderr separate, preserves raw bytes until decode time, and defaults to UTF-8 with errors="replace" when you ask for text. The PTY API is separate because terminal sessions are chunk-oriented and should not be forced through line normalization.
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.
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
intniceness or a platform-neutralCpuPriority - 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
0leaves 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 readsstream_iter(timeout)orfor stdout, stderr, exit_code in processdrain_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=Truetext=Trueencoding=...errors=...shell=Trueenv=...nice=...stdin=subprocess.DEVNULLinput=...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:
strorbytes: 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
strandlist[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
\rfor redraw-style terminal output - supports
write(...),read(...),drain(),available(),expect(...),resize(...), andsend_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, andexit_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 anIdleInfoDiffdelta and returnsIdleDecision.DEFAULT,IdleDecision.ACTIVE,IdleDecision.BEGIN_IDLE, orIdleDecision.IS_IDLE
There is also a compatibility alias: RunningProcess.psuedo_terminal(...).
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_terminalconsole_sharedconsole_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/SIGKILLis 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\nis normalized as a line break in pipe mode- bare
\ris preserved
PTY mode is intentionally more conservative:
- output is handled as chunks, not lines
- redraw-oriented
\ris 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.
./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.
If you want to invoke pytest directly, set RUNNING_PROCESS_LIVE_TESTS=1 and run uv run pytest -m live.
For direct Rust commands, prefer the repo trampolines, which prepend the shared rustup proxy location:
./_cargo check --workspace
./_cargo fmt --all --check
./_cargo clippy --workspace --all-targets -- -D warnings
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
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
stdoutandstderrare no longer merged by default.combined_outputexists for compatibility when you need the merged view.RunningProcess(..., use_pty=True)is no longer the preferred path; useRunningProcess.pseudo_terminal(...)for PTY sessions.- On supported Windows builds, PTY support is provided by the native Rust extension rather than a Python
winptyfallback. - The test suite checks that
running_process.__version__, package metadata, and manifest versions stay in sync.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file running_process-3.0.6.tar.gz.
File metadata
- Download URL: running_process-3.0.6.tar.gz
- Upload date:
- Size: 72.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7bbc0cad4faae7a921d60286ccb3de372542bfde94a7056a3d3696a394e66d58
|
|
| MD5 |
d1d5bcb849210ba90df69b9d5147a4b5
|
|
| BLAKE2b-256 |
5d77efbfd2e680bf6550a7a0119081e8f59ca311f669381675d35cfe98b478d4
|
File details
Details for the file running_process-3.0.6-cp311-cp311-win_arm64.whl.
File metadata
- Download URL: running_process-3.0.6-cp311-cp311-win_arm64.whl
- Upload date:
- Size: 1.1 MB
- Tags: CPython 3.11, Windows ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ea6764185b71eff9cd6f173896173aa99100cd723a91040ea81163300ab0456b
|
|
| MD5 |
8c7a74fff4642fbef5b2ba993eab1611
|
|
| BLAKE2b-256 |
e42e33c5d5f4db9dcd7eac798ae69ce40b9428a526b47f6dd48a766848c2f677
|
File details
Details for the file running_process-3.0.6-cp311-cp311-win_amd64.whl.
File metadata
- Download URL: running_process-3.0.6-cp311-cp311-win_amd64.whl
- Upload date:
- Size: 1.2 MB
- Tags: CPython 3.11, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2facb08fb9e0a0e7358f8b09b43d78b1b84da1793c1673150b9e8bc90400065d
|
|
| MD5 |
615e1b631c0feddc511d3d0051cc8b01
|
|
| BLAKE2b-256 |
576f95bd422d111d9108e3e8e2ca822106e957b63204355c5c32269cf94c7633
|
File details
Details for the file running_process-3.0.6-cp311-cp311-musllinux_1_2_x86_64.whl.
File metadata
- Download URL: running_process-3.0.6-cp311-cp311-musllinux_1_2_x86_64.whl
- Upload date:
- Size: 2.6 MB
- Tags: CPython 3.11, musllinux: musl 1.2+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
613778427dd49563e2de13ab39463d4a87b62097c462e3925588a78142f4f0bc
|
|
| MD5 |
7d6fcdeac3a6a6bb292f333e6e85d7a5
|
|
| BLAKE2b-256 |
b798473d7df62e9cc6b03e695793055d7faed3433c4edbd5839b487ca99d4f55
|
File details
Details for the file running_process-3.0.6-cp311-cp311-musllinux_1_2_aarch64.whl.
File metadata
- Download URL: running_process-3.0.6-cp311-cp311-musllinux_1_2_aarch64.whl
- Upload date:
- Size: 2.5 MB
- Tags: CPython 3.11, musllinux: musl 1.2+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9e7c9a79640036c43ccbcb3826dc8ac7c4271a7e4d6eacf79157597aa421ff49
|
|
| MD5 |
800b2b1f821421bd4676d2a9ed167808
|
|
| BLAKE2b-256 |
54eb79d8dca75bd942f92cdeebe937d9dd4b7fef6e8ee0022076b31479d2549f
|
File details
Details for the file running_process-3.0.6-cp311-cp311-macosx_11_0_arm64.whl.
File metadata
- Download URL: running_process-3.0.6-cp311-cp311-macosx_11_0_arm64.whl
- Upload date:
- Size: 1.2 MB
- Tags: CPython 3.11, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ebfdf6b9ce95216abd58f1f828eef716243a291c33480b3b2f0460fa16c8ec37
|
|
| MD5 |
069ecf3df19ff0f55a3c1c30dd00c056
|
|
| BLAKE2b-256 |
034ff0e7576806c90d7bd8aaf84e67d8e3f163a88b40e6733d6549592e17e0bf
|
File details
Details for the file running_process-3.0.6-cp311-cp311-macosx_10_12_x86_64.whl.
File metadata
- Download URL: running_process-3.0.6-cp311-cp311-macosx_10_12_x86_64.whl
- Upload date:
- Size: 1.3 MB
- Tags: CPython 3.11, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6f94a9ffa0b948e97b9377316e5a7ee81c764d0a8e5f585058e1deb71d84feff
|
|
| MD5 |
fd1987d1801b097aea8e6a6e64be9829
|
|
| BLAKE2b-256 |
f781d9a8300e2b896e9c954fa1629e3b1c518fae40e22b30ac440017e4d031bc
|