prole/knoe/core/stream_exec.py
chrisfu 4ee2b259c9 Checkpoint: rename installer to knoe + harden db build context
- Add build-context helper to copy Docker context safely (ignore runtime data, keep symlinks)

- Update UI and core actions to use ~/.prole/build and shared copy helper

- Add/adjust tests and scripts; introduce knoe ops helpers and update manifests

Co-authored-by: Junie <junie@jetbrains.com>
2026-03-22 01:45:21 -07:00

97 lines
2.5 KiB
Python

from __future__ import annotations
import errno
import os
import pty
import select
import subprocess
from collections.abc import Sequence
from typing import Callable
StreamCallback = Callable[[str], None]
def run_streaming_cmd(
cmd: str | Sequence[str],
cwd: str | None = None,
env: dict | None = None,
stdin_text: str | None = None,
on_stdout: StreamCallback | None = None,
on_stderr: StreamCallback | None = None,
) -> int:
"""Run a command with PTY-backed stdout/stderr and stream output chunks live."""
argv: list[str]
if isinstance(cmd, str):
argv = ["bash", "-lc", cmd]
else:
argv = list(cmd)
out_master, out_slave = pty.openpty()
err_master, err_slave = pty.openpty()
stdin_handle = subprocess.PIPE if stdin_text is not None else None
proc = None
try:
proc = subprocess.Popen(
argv,
cwd=cwd,
env=env,
stdin=stdin_handle,
stdout=out_slave,
stderr=err_slave,
text=False,
close_fds=True,
)
finally:
os.close(out_slave)
os.close(err_slave)
if stdin_text is not None and proc.stdin:
try:
proc.stdin.write(stdin_text.encode("utf-8", errors="replace"))
proc.stdin.close()
except Exception:
pass
callbacks: dict[int, StreamCallback | None] = {
out_master: on_stdout,
err_master: on_stderr,
}
try:
while callbacks:
ready, _, _ = select.select(list(callbacks.keys()), [], [], 0.1)
if not ready:
if proc.poll() is not None:
# Drain any remaining data after process exit.
ready = list(callbacks.keys())
else:
continue
for fd in ready:
try:
chunk = os.read(fd, 4096)
except OSError as exc:
if exc.errno == errno.EIO:
chunk = b""
else:
raise
if not chunk:
os.close(fd)
callbacks.pop(fd, None)
continue
callback = callbacks.get(fd)
if callback:
callback(chunk.decode("utf-8", errors="replace"))
return proc.wait()
finally:
for fd in list(callbacks.keys()):
try:
os.close(fd)
except OSError:
pass