feat(prepare-win): stream toolchain/WU output to the console live
pypsrp's execute_ps is fully blocking, so Install-CIToolchain / Windows Update output only appeared at the end behind a "still running (Ns)" heartbeat. Add WinRmTransport.run_streaming: drive the script via pypsrp's incremental begin_invoke/poll_invoke, emitting output, Write-Host (information) and error stream lines as they arrive over the same WinRM channel. _run_with_heartbeat now prefers run_streaming (suppressing the CI_EXITCODE marker line) and falls back to the blocking heartbeat only on an unexpected pypsrp error — TransportError still propagates so the 3010 reboot handling is unaffected.
This commit is contained in:
@@ -193,3 +193,89 @@ def test_is_ready_true_when_run_succeeds(monkeypatch: pytest.MonkeyPatch) -> Non
|
||||
_install_fake_pypsrp(monkeypatch, fake)
|
||||
t = WinRmTransport("10.0.0.1", "user", "pwd")
|
||||
assert t.is_ready() is True
|
||||
|
||||
|
||||
def _install_fake_streaming(monkeypatch: pytest.MonkeyPatch, *, with_error: bool = False) -> None:
|
||||
"""Inject fake pypsrp.powershell + complex_objects for run_streaming tests."""
|
||||
class _State:
|
||||
NOT_STARTED, RUNNING, COMPLETED, FAILED, STOPPED, STOPPING, DISCONNECTED = range(7)
|
||||
|
||||
class _Streams:
|
||||
def __init__(self) -> None:
|
||||
self.information: list[Any] = []
|
||||
self.error: list[Any] = []
|
||||
|
||||
class _FakePS:
|
||||
def __init__(self, _pool: Any) -> None:
|
||||
self.state = _State.NOT_STARTED
|
||||
self.output: list[Any] = []
|
||||
self.streams = _Streams()
|
||||
self._poll = 0
|
||||
|
||||
def add_script(self, script: str) -> None:
|
||||
self.script = script
|
||||
|
||||
def begin_invoke(self) -> None:
|
||||
self.state = _State.RUNNING
|
||||
|
||||
def poll_invoke(self, *_a: Any, **_k: Any) -> None:
|
||||
self._poll += 1
|
||||
if self._poll == 1:
|
||||
self.output.append("line1")
|
||||
self.streams.information.append(types.SimpleNamespace(message_data="hostmsg"))
|
||||
elif self._poll == 2:
|
||||
self.output.append("line2")
|
||||
if with_error:
|
||||
self.streams.error.append("boom")
|
||||
self.state = _State.COMPLETED
|
||||
|
||||
def end_invoke(self) -> None:
|
||||
pass
|
||||
|
||||
class _FakePool:
|
||||
def __init__(self, wsman: Any) -> None:
|
||||
self.wsman = wsman
|
||||
|
||||
def open(self) -> None:
|
||||
pass
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
||||
co = types.ModuleType("pypsrp.complex_objects")
|
||||
co.PSInvocationState = _State # type: ignore[attr-defined]
|
||||
psmod = types.ModuleType("pypsrp.powershell")
|
||||
psmod.PowerShell = _FakePS # type: ignore[attr-defined]
|
||||
psmod.RunspacePool = _FakePool # type: ignore[attr-defined]
|
||||
monkeypatch.setitem(sys.modules, "pypsrp.complex_objects", co)
|
||||
monkeypatch.setitem(sys.modules, "pypsrp.powershell", psmod)
|
||||
monkeypatch.setattr("ci_orchestrator.transport.winrm.time.sleep", lambda _s: None)
|
||||
|
||||
|
||||
def test_run_streaming_emits_lines(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
fake = _FakeClient()
|
||||
_install_fake_pypsrp(monkeypatch, fake)
|
||||
_install_fake_streaming(monkeypatch)
|
||||
t = WinRmTransport("10.0.0.1", "u", "p")
|
||||
lines: list[str] = []
|
||||
res = t.run_streaming("Write-Output 1", lines.append)
|
||||
assert lines == ["line1", "hostmsg", "line2"]
|
||||
assert res.ok and res.stdout == "line1\nline2"
|
||||
|
||||
|
||||
def test_run_streaming_raises_on_error(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
fake = _FakeClient()
|
||||
_install_fake_pypsrp(monkeypatch, fake)
|
||||
_install_fake_streaming(monkeypatch, with_error=True)
|
||||
t = WinRmTransport("10.0.0.1", "u", "p")
|
||||
with pytest.raises(TransportCommandError):
|
||||
t.run_streaming("x", lambda _s: None)
|
||||
|
||||
|
||||
def test_run_streaming_check_false(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
fake = _FakeClient()
|
||||
_install_fake_pypsrp(monkeypatch, fake)
|
||||
_install_fake_streaming(monkeypatch, with_error=True)
|
||||
t = WinRmTransport("10.0.0.1", "u", "p")
|
||||
res = t.run_streaming("x", lambda _s: None, check=False)
|
||||
assert not res.ok and "boom" in res.stderr
|
||||
|
||||
Reference in New Issue
Block a user