fix(update): Windows progress server hands out its URL only once it is serving

`Start-UiServer` printed the -SelfTestUi URL (and opened the browser window)
as soon as the TcpListener was bound, but the runspace that answers /progress
starts asynchronously — BeginInvoke returns before the pipeline is open and
the script block is JIT'd, which is seconds on a loaded runner. The kernel
accepted connections into the backlog during that gap and nobody answered
them. The self-test hit it three times (#90371 and two follow-ups each
widened a timeout instead of removing the race) and it just failed an
unrelated hermes_state.py PR (run 33591547099, two 5s stale-backlog
timeouts = red).

- windows.ps1: readiness handshake after BeginInvoke — one /progress
  round-trip must succeed (≤15s) before the server is returned; on failure
  tear the listener down and continue without UI. The URL now means
  "serving", not "bound". Also fixes the browser opening to a page that never
  loads on a slow machine.
- test: 1s per-attempt probe timeout so a single dead backlog socket cannot
  consume half the readiness budget.
- CI: new `desktop_updater` classifier lane. tests/test_desktop_update_windows_*.py
  spawn the real PowerShell script; the Windows-only job now runs them only
  when scripts/desktop-update/**, the Electron updater launcher, conftest,
  pyproject, or those tests change (push/dispatch fail open). A PR that
  never touched that surface cannot be failed by its process timing.
This commit is contained in:
Teknium
2026-09-01 21:58:50 -07:00
parent 2b7132c8e5
commit bfbb34bbec
7 changed files with 132 additions and 5 deletions
@@ -35,17 +35,24 @@ def _read_progress(url: str, deadline: float) -> dict[str, object]:
``urlopen(timeout=5)`` propagating TimeoutError was exactly the Aug 2026
flake (run 32440286339). Only a listener that stays unresponsive until
the deadline fails the test.
Per-attempt timeout is 1s, not 5s: a connection the kernel accepted into
the backlog before the runspace was serving never gets answered, and a 5s
wait on it burned half the readiness budget per attempt (two stale
attempts = red, run 33591547099). The script's own readiness handshake
now keeps that gap from reaching us, but the probe should not be able to
lose the whole budget to one dead socket either way.
"""
last_exc: Exception | None = None
attempted = False
while not attempted or time.monotonic() < deadline:
attempted = True
try:
with urlopen(f"{url}progress", timeout=5) as response:
with urlopen(f"{url}progress", timeout=1) as response:
return json.loads(response.read().decode("utf-8"))
except (TimeoutError, OSError) as exc: # transient stall — retry
last_exc = exc
time.sleep(0.2)
time.sleep(0.1)
raise AssertionError(
f"/progress unresponsive until deadline (last error: {last_exc!r})"
)