feat(build): live-stream Linux build output; ns7zip via in-guest clone
- SshTransport.run_streaming: incremental channel recv, forwards stdout/stderr live (no more one-block-at-end). _linux_build uses it with PYTHONUNBUFFERED so the guest python flushes promptly. - _tail_lines split out for failure messages (no double print). - Windows live streaming deferred (TODO 8.2) — pypsrp execute_ps returns only at completion. - build-ns7zip.yml: use-git-clone:'true' (in-guest), repo-url -> https://gitea.emulab.it/... (guest-reachable; host SSH alias was host-only). Requires GiteaPAT in the LocalSystem keyring or a public nsis-plugin-ns7zip. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,8 @@ on stderr output. With paramiko there is no equivalent issue.
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -123,6 +125,64 @@ class SshTransport:
|
||||
raise TransportCommandError(result.returncode, result.stdout, result.stderr)
|
||||
return result
|
||||
|
||||
def run_streaming(
|
||||
self, command: str, *, check: bool = True, timeout: float | None = None
|
||||
) -> SshResult:
|
||||
"""Run ``command`` streaming stdout/stderr live to this process.
|
||||
|
||||
Unlike :meth:`run` (which blocks on a full channel read), output
|
||||
is forwarded chunk-by-chunk as it arrives so long builds show
|
||||
progress instead of one block at the end. Captured output is
|
||||
still returned in :class:`SshResult`.
|
||||
"""
|
||||
client = self._connect()
|
||||
try:
|
||||
chan = client.get_transport().open_session() # type: ignore[union-attr]
|
||||
if timeout:
|
||||
chan.settimeout(timeout)
|
||||
chan.exec_command(command)
|
||||
except Exception as exc:
|
||||
raise TransportConnectError(f"SSH exec_command failed: {exc}") from exc
|
||||
|
||||
out_parts: list[str] = []
|
||||
err_parts: list[str] = []
|
||||
try:
|
||||
while True:
|
||||
progressed = False
|
||||
while chan.recv_ready():
|
||||
d = chan.recv(65536).decode("utf-8", errors="replace")
|
||||
out_parts.append(d)
|
||||
sys.stdout.write(d)
|
||||
sys.stdout.flush()
|
||||
progressed = True
|
||||
while chan.recv_stderr_ready():
|
||||
d = chan.recv_stderr(65536).decode("utf-8", errors="replace")
|
||||
err_parts.append(d)
|
||||
sys.stderr.write(d)
|
||||
sys.stderr.flush()
|
||||
progressed = True
|
||||
if (
|
||||
chan.exit_status_ready()
|
||||
and not chan.recv_ready()
|
||||
and not chan.recv_stderr_ready()
|
||||
):
|
||||
break
|
||||
if not progressed:
|
||||
time.sleep(0.05)
|
||||
rc = chan.recv_exit_status()
|
||||
except Exception as exc:
|
||||
raise TransportConnectError(f"SSH stream failed: {exc}") from exc
|
||||
finally:
|
||||
with contextlib.suppress(Exception):
|
||||
chan.close()
|
||||
|
||||
result = SshResult(
|
||||
stdout="".join(out_parts), stderr="".join(err_parts), returncode=rc
|
||||
)
|
||||
if check and not result.ok:
|
||||
raise TransportCommandError(result.returncode, result.stdout, result.stderr)
|
||||
return result
|
||||
|
||||
def copy(self, local_path: str | Path, remote_path: str) -> None:
|
||||
"""Upload via SFTP."""
|
||||
client = self._connect()
|
||||
|
||||
Reference in New Issue
Block a user