diff options
Diffstat (limited to 'app/backends/common.py')
| -rw-r--r-- | app/backends/common.py | 38 |
1 files changed, 33 insertions, 5 deletions
diff --git a/app/backends/common.py b/app/backends/common.py index b5d6576..a12a366 100644 --- a/app/backends/common.py +++ b/app/backends/common.py @@ -372,7 +372,8 @@ def write_prompt_text(wav_dir: Path, def run_console_subprocess(argv: List[str], cwd: Optional[Path] = None, *, emit=None, cancel=None, on_cancel=None, stall_timeout: Optional[float] = None, - env: Optional[Dict[str, str]] = None) -> int: + env: Optional[Dict[str, str]] = None, + on_chunk=None) -> int: """Run a subprocess, streaming output to the console or to EMIT. With EMIT None the child inherits the real terminal and its output @@ -382,6 +383,15 @@ def run_console_subprocess(argv: List[str], cwd: Optional[Path] = None, updates like git's or tqdm's surface as lines), and each line is passed to EMIT — the in-TUI task view path. + ON_CHUNK (a ``callable(bytes)``) asks for the child's output *and* a + look at its raw bytes: the run then takes the piped path even without + EMIT, forwarding every chunk verbatim to the real terminal (so + carriage-return progress still animates) and handing each chunk to + ON_CHUNK — the hook pip_install uses to sniff pip's output for a + corrupted-wheel failure while the user watches it live. ON_CHUNK must + not raise and must not write to the terminal (the forwarding here + already does). + CANCEL is an optional ``threading.Event``: once set, ON_CANCEL (if given) is called (e.g. to touch a ``--cancel-file``), then the child's process group is terminated (SIGTERM, escalating to SIGKILL after a grace @@ -401,7 +411,7 @@ def run_console_subprocess(argv: List[str], cwd: Optional[Path] = None, Returns the process exit code. """ import subprocess - if emit is None: + if emit is None and on_chunk is None: try: result = subprocess.run( argv, cwd=str(cwd) if cwd is not None else None, env=env) @@ -423,7 +433,11 @@ def run_console_subprocess(argv: List[str], cwd: Optional[Path] = None, try: proc = subprocess.Popen(argv, **popen_kwargs) except OSError as exc: - emit(f"[ERROR] Could not run {' '.join(argv)}: {exc}") + message = f"[ERROR] Could not run {' '.join(argv)}: {exc}" + if emit is not None: + emit(message) + else: + print(message) return 1 cancelled = False @@ -448,6 +462,16 @@ def run_console_subprocess(argv: List[str], cwd: Optional[Path] = None, if not chunk: break last_output = time.monotonic() + if on_chunk is not None: + on_chunk(chunk) + if emit is None: + # The on_chunk console path: forward each chunk + # verbatim so carriage-return progress bars still + # animate exactly as they did when the child owned + # the terminal. + sys.stdout.write(chunk.decode("utf-8", errors="replace")) + sys.stdout.flush() + continue pending += chunk.decode("utf-8", errors="replace") parts = re.split(r"[\r\n]+", pending) pending = parts.pop() @@ -483,8 +507,12 @@ def run_console_subprocess(argv: List[str], cwd: Optional[Path] = None, if (stall_timeout is not None and time.monotonic() - last_output > stall_timeout): stalled = True - emit(f"[ERROR] No output for {int(stall_timeout)}s — assuming " - "the process hung; stopping it.") + message = (f"[ERROR] No output for {int(stall_timeout)}s — " + "assuming the process hung; stopping it.") + if emit is not None: + emit(message) + else: + print(message) _terminate_process_group(proc) break time.sleep(0.1) |
