aboutsummaryrefslogtreecommitdiff
path: root/app/backends/common.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-28 02:43:27 -0400
committerhistoria <historiavg@proton.me>2026-08-28 02:43:27 -0400
commit5c3db8f500ff206f3a675d8f4184cb0d61f94804 (patch)
treeeab36bc6ff0e55bd27871d9154f1bc0764f20af6 /app/backends/common.py
parenta7c653313d2bb1e185cfbc3f0f52c2fe33218600 (diff)
downloadtts-audiobook-generator-5c3db8f500ff206f3a675d8f4184cb0d61f94804.tar.gz
feat: cuda arch detection for shorter builds, build failure detection
Diffstat (limited to 'app/backends/common.py')
-rw-r--r--app/backends/common.py46
1 files changed, 40 insertions, 6 deletions
diff --git a/app/backends/common.py b/app/backends/common.py
index 4edfd61..4c768b8 100644
--- a/app/backends/common.py
+++ b/app/backends/common.py
@@ -347,7 +347,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) -> int:
+ *, emit=None, cancel=None, on_cancel=None,
+ stall_timeout: Optional[float] = 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
@@ -360,7 +361,16 @@ def run_console_subprocess(argv: List[str], cwd: Optional[Path] = None,
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
- period) and 130 is returned. Returns the process exit code.
+ period) and 130 is returned.
+
+ STALL_TIMEOUT (EMIT path only) is a no-output watchdog in seconds: when
+ the child produces no new output line for that long, it is treated as
+ wedged (a build whose compiler hung, a download that stopped moving) —
+ the process group is terminated, an [ERROR] line is emitted, and 124 is
+ returned so callers can report a stall distinctly from a plain failure.
+ None (the default) waits forever, as before.
+
+ Returns the process exit code.
"""
import subprocess
if emit is None:
@@ -387,12 +397,18 @@ def run_console_subprocess(argv: List[str], cwd: Optional[Path] = None,
return 1
cancelled = False
+ stalled = False
+ # Written by the reader thread, read by the poll loop below: a plain
+ # float assignment is atomic enough under the GIL (no torn reads).
+ last_output = time.monotonic()
def _reader() -> None:
+ nonlocal last_output
try:
for raw in iter(proc.stdout.readline, b""):
if not raw:
break
+ last_output = time.monotonic()
text = raw.decode("utf-8", errors="replace")
for line in text.splitlines():
if line:
@@ -421,6 +437,13 @@ def run_console_subprocess(argv: List[str], cwd: Optional[Path] = None,
break
if proc.poll() is not None:
break
+ 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.")
+ _terminate_process_group(proc)
+ break
time.sleep(0.1)
try:
reader.join(timeout=5)
@@ -429,6 +452,8 @@ def run_console_subprocess(argv: List[str], cwd: Optional[Path] = None,
reader.join(timeout=0)
if cancelled:
return 130
+ if stalled:
+ return 124
return proc.returncode
@@ -577,20 +602,29 @@ def _origin_default_branch(checkout: Path) -> str:
def run_console_subprocess_quiet(argv: List[str],
- cwd: Optional[Path] = None):
+ cwd: Optional[Path] = None,
+ timeout: Optional[float] = None):
"""Run ARGV silently and return the completed result.
Unlike run_console_subprocess (which streams or returns only an exit
code) this captures stdout and needs the process object itself, for the
small git probes (rev-parse, symbolic-ref) whose *output* matters and
- whose failure is a normal, non-fatal outcome. Returns None when the
- process could not be started.
+ whose failure is a normal, non-fatal outcome. TIMEOUT bounds the wait
+ (e.g. for hardware probes like nvidia-smi that can hang on a wedged
+ driver); a timeout kills the child and returns a failed result, not an
+ exception. Returns None when the process could not be started.
"""
import subprocess
try:
return subprocess.run(
argv, capture_output=True,
- cwd=str(cwd) if cwd is not None else None, check=False)
+ cwd=str(cwd) if cwd is not None else None, check=False,
+ timeout=timeout)
+ except subprocess.TimeoutExpired:
+ class _TimedOut:
+ returncode = -1
+ stdout = b""
+ return _TimedOut()
except OSError:
return None