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:
parent
9a0a6b501a
commit
d2791d3db7
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue