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 <noreply@anthropic.com>
This commit is contained in:
js 2026-02-16 13:28:43 +00:00
parent 9a0a6b501a
commit d2791d3db7
2 changed files with 37 additions and 13 deletions

View File

@ -452,18 +452,11 @@ def _build_image_job(build_id, app_name, version, target_server, server_url, rel
) )
cmd = ["doas", "sh", "-c", shell_cmd] 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 returncode, output = _run_doas_command(cmd, doas_password, timeout=600, on_output=on_build_output)
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)
if returncode != 0: if returncode != 0:
_append_build_log(build_id, "✗ Build failed") _append_build_log(build_id, "✗ Build failed")

View File

@ -365,8 +365,12 @@ def get_setup_status():
return status return status
def _run_doas_command(cmd, doas_password, timeout=120): 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.""" """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 pty
import select import select
@ -379,9 +383,22 @@ def _run_doas_command(cmd, doas_password, timeout=120):
# Parent process # Parent process
output = b"" output = b""
line_buffer = b""
password_sent = False password_sent = False
deadline = time.time() + timeout 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: try:
while time.time() < deadline: while time.time() < deadline:
ready, _, _ = select.select([master_fd], [], [], 1) ready, _, _ = select.select([master_fd], [], [], 1)
@ -391,6 +408,7 @@ def _run_doas_command(cmd, doas_password, timeout=120):
if not chunk: if not chunk:
break break
output += chunk output += chunk
_process_lines(chunk)
if not password_sent and b"password" in output.lower(): if not password_sent and b"password" in output.lower():
time.sleep(0.1) time.sleep(0.1)
os.write(master_fd, (doas_password + "\n").encode()) 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) d = os.read(master_fd, 4096)
if d: if d:
output += d output += d
_process_lines(d)
else: else:
break break
except OSError: except OSError:
break break
else: else:
break 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 rc = os.WEXITSTATUS(result[1]) if os.WIFEXITED(result[1]) else 1
return rc, output.decode("utf-8", errors="replace") 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 # Final wait
_, status = os.waitpid(pid, 0) _, status = os.waitpid(pid, 0)
rc = os.WEXITSTATUS(status) if os.WIFEXITED(status) else 1 rc = os.WEXITSTATUS(status) if os.WIFEXITED(status) else 1