diff options
| author | historia <historiavg@proton.me> | 2026-09-01 14:32:05 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-09-01 14:32:05 -0400 |
| commit | 6cfcd564c0684c52618235e6366f4a81c02b9a5b (patch) | |
| tree | 55321760a8103bc6b5d79489fac4135a60e6e3ba /app/backends/common.py | |
| parent | dc6e7cd43029da62dabe2513fb5aa8a34df1bd6d (diff) | |
| download | tts-audiobook-generator-6cfcd564c0684c52618235e6366f4a81c02b9a5b.tar.gz | |
slop refactor/dedup
Diffstat (limited to 'app/backends/common.py')
| -rw-r--r-- | app/backends/common.py | 67 |
1 files changed, 53 insertions, 14 deletions
diff --git a/app/backends/common.py b/app/backends/common.py index d25a03e..24746b6 100644 --- a/app/backends/common.py +++ b/app/backends/common.py @@ -193,12 +193,32 @@ def wav_dir_preview(directory: Path) -> Tuple[str, str]: return ("no .wav files", "info") +def port_of(url: str, fallback: int) -> int: + """URL's explicit port, else FALLBACK (invalid URLs fall back too).""" + try: + return urllib.parse.urlsplit(url).port or fallback + except ValueError: + return fallback + + def url_with_port(url: str, port: int) -> str: - """Return URL with its port replaced/inserted as PORT.""" + """Return URL with its port replaced/inserted as PORT. + + Preserves the userinfo ("user:pass@host") and brackets IPv6 hosts + ("[::1]"), which a plain f"{host}:{port}" rebuild would mangle. + """ parts = urllib.parse.urlsplit(url) host = parts.hostname or "127.0.0.1" + if ":" in host and not host.startswith("["): + host = f"[{host}]" + netloc = f"{host}:{port}" + if parts.username: + cred = parts.username + if parts.password: + cred = f"{cred}:{parts.password}" + netloc = f"{cred}@{netloc}" return urllib.parse.urlunsplit( - (parts.scheme or "http", f"{host}:{port}", parts.path, "", "")) + (parts.scheme or "http", netloc, parts.path, "", "")) def normalize_remote_url(value: str) -> str: @@ -408,14 +428,27 @@ def run_console_subprocess(argv: List[str], cwd: Optional[Path] = None, def _reader() -> None: nonlocal last_output try: - for raw in iter(proc.stdout.readline, b""): - if not raw: + pending = "" + while True: + # read1 (not read/readline) returns whatever a single pipe + # read yields, without waiting to fill the buffer, and \r + # is treated as a line break: carriage-return progress bars + # (git clone --progress, tqdm/HuggingFace downloads) then + # surface incrementally and keep the stall watchdog fed — + # a readline-based reader blocked until the next \n would + # let a healthy download starve to the timeout. + chunk = proc.stdout.read1(4096) + if not chunk: break last_output = time.monotonic() - text = raw.decode("utf-8", errors="replace") - for line in text.splitlines(): + pending += chunk.decode("utf-8", errors="replace") + parts = re.split(r"[\r\n]+", pending) + pending = parts.pop() + for line in parts: if line: emit(line) + if pending.strip(): + emit(pending) except (OSError, ValueError): pass @@ -448,11 +481,10 @@ def run_console_subprocess(argv: List[str], cwd: Optional[Path] = None, _terminate_process_group(proc) break time.sleep(0.1) - try: - reader.join(timeout=5) - finally: - if reader.is_alive(): - reader.join(timeout=0) + # The reader is a daemon: a join timeout here only means the child + # closed its stdout but the thread is still draining — there is + # nothing useful left to wait for. + reader.join(timeout=5) if cancelled: return 130 if stalled: @@ -476,10 +508,17 @@ def _terminate_process_group(proc) -> None: """ import signal if sys.platform == "win32": + # TerminateProcess hits a single pid; children the server spawned + # would survive as orphans. taskkill /T walks and kills the whole + # process tree, then the poll loop reaps the direct child. try: - proc.terminate() - except OSError: - pass + subprocess.run(["taskkill", "/T", "/F", "/PID", str(proc.pid)], + capture_output=True, timeout=10) + except (OSError, subprocess.SubprocessError): + try: + proc.terminate() + except OSError: + pass deadline = time.time() + 10 while time.time() < deadline: if proc.poll() is not None: |
