Skip to main content

Stream subprocess output with timeouts, tree-kill, and sane defaults.

Project description

procstream

Stream subprocess output in Python — with timeouts, tree-kill, and sane defaults.

from procstream import run

for line in run(["pytest", "-q"]).stream():
    prefix = "!!" if line.is_stderr else "  "
    print(prefix, line.text)

Why

subprocess.run() buffers everything until the child exits. subprocess.Popen with pipes works, but reading stdout and stderr concurrently without deadlock is one of those things you have to get right every single time, across every project. Timeouts that kill the whole process tree (not just the direct child) are another recurring footgun.

procstream wraps that pattern into a small, typed, stdlib-only API.

Install

pip install procstream

Python 3.9+. No runtime dependencies.

What it gives you

  • Streaming output — iterate lines as they arrive, tagged by stream (stdout / stderr), in arrival order.
  • Timeouts that actually clean up — the whole process group is sent SIGTERM, then SIGKILL if it ignores the hint. No orphaned grandchildren.
  • Tree-killterminate_tree() / kill_tree() for manual cancel, POSIX and Windows.
  • Per-line callbackson_stdout=print if you don't want to write the iterator loop yourself.
  • Context managerwith run(...) as p: guarantees the tree dies if your code raises.
  • Result bufferingwait() always returns captured stdout / stderr, even if you streamed them.
  • Stdlib only — no psutil, no surprise deps.

Usage

Simple: capture output

from procstream import run

r = run(["node", "--version"]).wait()
print(r.returncode, r.stdout.strip())

Streaming: react to lines as they arrive

for line in run(["npm", "install"]).stream():
    if line.is_stderr:
        log.warning(line.text)
    else:
        log.info(line.text)

Lines come out in the same interleaved order the child emitted them. line.stream is "stdout" or "stderr".

Callbacks: no loop required

run(
    ["make", "build"],
    on_stdout=print,
    on_stderr=lambda t: print("!!", t),
).wait()

Timeout: kills the whole tree

from procstream import run, TimeoutExpired

try:
    run(["flaky-script"], timeout=30).wait()
except TimeoutExpired as e:
    print("killed after", e.timeout, "seconds")
    # Partial output is still available:
    print(e.result.stdout)

When the deadline fires, procstream sends SIGTERM to the process group and waits up to 2 seconds. If the child still hasn't exited, it sends SIGKILL. Grandchildren die with it.

Manual cancel

p = run(["long-running-server", "--port", "8080"])
# ... elsewhere
p.terminate_tree(grace=5.0)   # SIGTERM, wait, SIGKILL if needed
# or
p.kill_tree()                  # immediate SIGKILL

Context manager: cancel on exception

with run(["watcher"]) as p:
    for line in p.stream():
        if "ERROR" in line.text:
            raise RuntimeError("saw error, bailing")
# Tree is terminated on the way out.

API

run(cmd, *, cwd=None, env=None, shell=False, timeout=None, encoding="utf-8", errors="replace", on_stdout=None, on_stderr=None) -> Process

Spawn a command and return a Process. The child starts immediately.

Process

member description
pid child's pid
returncode None while running, int once finished
running True while the child is alive
stream() iterator of Line in arrival order (consume only once)
wait() block until completion, return Result; raises TimeoutExpired if killed
terminate_tree(grace=5.0) SIGTERM group, escalate to SIGKILL if still alive
kill_tree() SIGKILL group immediately
context manager with run(...) as p: — terminates on exit

Line

member description
text: str line content, newline stripped
stream: "stdout" | "stderr" origin pipe
is_stderr: bool shortcut

Result

member description
returncode: int exit status
stdout: str all stdout lines joined with \n
stderr: str all stderr lines joined with \n
combined: str stdout followed by stderr (use stream() for real order)
ok: bool returncode == 0

TimeoutExpired

Raised from wait() (or at the end of stream()) when timeout fires. Exposes pid, timeout, and a partial result.

Platform notes

  • POSIX (macOS, Linux): child starts in a new session via start_new_session=True. Signals go to the whole process group with os.killpg.
  • Windows: child starts with CREATE_NEW_PROCESS_GROUP. terminate_tree() sends CTRL_BREAK_EVENT; force-kill falls back to taskkill /F /T /PID <pid>.

License

MIT.

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

procstream-0.1.0.tar.gz (9.8 kB view details)

Uploaded Source

Built Distribution

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

procstream-0.1.0-py3-none-any.whl (9.3 kB view details)

Uploaded Python 3

File details

Details for the file procstream-0.1.0.tar.gz.

File metadata

  • Download URL: procstream-0.1.0.tar.gz
  • Upload date:
  • Size: 9.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for procstream-0.1.0.tar.gz
Algorithm Hash digest
SHA256 2356bb462da169cf3360819676a7a2ada29a4e1de49f585d49e9f45685371bc0
MD5 548b3163bbba69833f5f72b2ece1fd76
BLAKE2b-256 7ceb740cd0d1184d8a6cac486d60d8eb0da3324c869317817f14e396cf849fe2

See more details on using hashes here.

File details

Details for the file procstream-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: procstream-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 9.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for procstream-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a10be7b776906a16eaba2dccba8380596f6e6ae6b6e1de7db968fc5134f57ef7
MD5 ff274deffbced3aeca06a253ef175c71
BLAKE2b-256 4ab7728ba9fadb85600c51dd325430875b43e5bdd16a7b89781ec0d47b16458e

See more details on using hashes here.

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