From d2791d3db7a21d1d9b278eddd6525223deca3266 Mon Sep 17 00:00:00 2001 From: js Date: Mon, 16 Feb 2026 13:28:43 +0000 Subject: [PATCH] improve: stream distrobuilder output to build logs in real-time Added on_output callback to _run_doas_command so build logs update live during the distrobuilder process instead of only after completion. Co-Authored-By: Claude Opus 4.6 --- admin_panel/api/apps.py | 15 ++++----------- admin_panel/api/servers.py | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/admin_panel/api/apps.py b/admin_panel/api/apps.py index c2d5ff9..6c70173 100644 --- a/admin_panel/api/apps.py +++ b/admin_panel/api/apps.py @@ -452,18 +452,11 @@ def _build_image_job(build_id, app_name, version, target_server, server_url, rel ) cmd = ["doas", "sh", "-c", shell_cmd] - returncode, output = _run_doas_command(cmd, doas_password, timeout=600) + # Stream output to cache in real-time + def on_build_output(line): + _append_build_log(build_id, line) - # Parse output, skip password prompt lines - output_lines = [] - for line in output.split("\n"): - stripped = line.strip() - if not stripped or "password" in stripped.lower(): - continue - output_lines.append(stripped) - - if output_lines: - _append_build_log(build_id, *output_lines) + returncode, output = _run_doas_command(cmd, doas_password, timeout=600, on_output=on_build_output) if returncode != 0: _append_build_log(build_id, "✗ Build failed") diff --git a/admin_panel/api/servers.py b/admin_panel/api/servers.py index a8066b8..144d9f9 100644 --- a/admin_panel/api/servers.py +++ b/admin_panel/api/servers.py @@ -365,8 +365,12 @@ def get_setup_status(): return status -def _run_doas_command(cmd, doas_password, timeout=120): - """Run a command with doas using pty.fork() for a real controlling terminal.""" +def _run_doas_command(cmd, doas_password, timeout=120, on_output=None): + """Run a command with doas using pty.fork() for a real controlling terminal. + + Args: + on_output: Optional callback(line: str) called for each line of output in real-time. + """ import pty import select @@ -379,9 +383,22 @@ def _run_doas_command(cmd, doas_password, timeout=120): # Parent process output = b"" + line_buffer = b"" password_sent = False deadline = time.time() + timeout + def _process_lines(data): + """Process buffered data into lines and call on_output callback.""" + nonlocal line_buffer + if not on_output: + return + line_buffer += data + while b"\n" in line_buffer: + line, line_buffer = line_buffer.split(b"\n", 1) + text = line.decode("utf-8", errors="replace").strip() + if text and "password" not in text.lower(): + on_output(text) + try: while time.time() < deadline: ready, _, _ = select.select([master_fd], [], [], 1) @@ -391,6 +408,7 @@ def _run_doas_command(cmd, doas_password, timeout=120): if not chunk: break output += chunk + _process_lines(chunk) if not password_sent and b"password" in output.lower(): time.sleep(0.1) os.write(master_fd, (doas_password + "\n").encode()) @@ -409,15 +427,28 @@ def _run_doas_command(cmd, doas_password, timeout=120): d = os.read(master_fd, 4096) if d: output += d + _process_lines(d) else: break except OSError: break else: break + # Flush remaining line buffer + if on_output and line_buffer: + text = line_buffer.decode("utf-8", errors="replace").strip() + if text and "password" not in text.lower(): + on_output(text) + line_buffer = b"" rc = os.WEXITSTATUS(result[1]) if os.WIFEXITED(result[1]) else 1 return rc, output.decode("utf-8", errors="replace") + # Flush remaining line buffer + if on_output and line_buffer: + text = line_buffer.decode("utf-8", errors="replace").strip() + if text and "password" not in text.lower(): + on_output(text) + # Final wait _, status = os.waitpid(pid, 0) rc = os.WEXITSTATUS(status) if os.WIFEXITED(status) else 1