fix(update): drain uv/pip stderr - undrained pipe deadlocked windows desktop updates

Two consecutive Windows E2E runs hung inside dependency install until
the job timeout: 31449642122 65 minutes in uv pip install .[all],
31453853006 43 minutes in the SQLite-repair uv sync (RUST_LOG=uv=debug
made THAT run hang earlier and with more stderr - the tell).

Root cause: scripts/desktop-update.ps1 redirects both child pipes but
only pumps stdout while the child runs; stderr is ReadToEnd()'d after
exit. uv and pip write progress to stderr. Once that pipe hits the
~64KB buffer, uv blocks on write, hermes update blocks on uv, the
hand-off blocks on hermes update: deadlock. Slower stderr producers
survive by finishing before the buffer fills, which is why the linux
sandbox never sees this.

Fix both sides of the class:
- managed_uv.py candidate sync + main.py _run_install_with_heartbeat:
  merge stderr into stdout (the pipe that IS drained). This arm heals
  EXISTING installs, whose old hand-off script drives the NEW python
  after the git reset.
- desktop-update.ps1: drain stderr concurrently via ReadToEndAsync so
  future bases never block regardless of what a child writes there.

_run_logged_subprocess and _run_npm_install_deterministic already
merge or capture both pipes; the two fixed sites were the only update-
path spawns that redirect stderr without draining it live.
This commit is contained in:
ethernet
2026-08-11 00:03:47 -04:00
parent bdb1b53c08
commit e3d22b5b29
3 changed files with 26 additions and 3 deletions
+9
View File
@@ -8060,11 +8060,20 @@ def _run_install_with_heartbeat(
t = threading.Thread(target=_heartbeat, daemon=True)
t.start()
try:
# stderr=STDOUT: uv/pip write progress to stderr. The desktop
# hand-off (scripts/desktop-update.ps1) only drains the child's
# stdout while the child runs, so a full stderr pipe (~64KB)
# blocks the installer forever — run 31449642122 hung 65 minutes
# inside this call. Merged into stdout, the output rides the pipe
# that IS drained. The fix lives here (not only in the hand-off
# script) because old installed bases run their OLD copy of the
# hand-off, but import THIS file fresh after the git reset.
subprocess.run(
cmd,
cwd=PROJECT_ROOT,
check=True,
env=env,
stderr=subprocess.STDOUT,
)
finally:
done.set()
+9
View File
@@ -780,6 +780,14 @@ def _stage_candidate_venv(
# UV_NO_CONFIG drops it and uv 0.12+ refuses --locked.
sync_env = dict(env)
sync_env.pop("UV_NO_CONFIG", None)
# stderr=STDOUT: uv writes progress to stderr. When the desktop
# hand-off (scripts/desktop-update.ps1) runs `hermes update`, it only
# drains the child's stdout while the child runs; a full stderr pipe
# (~64KB) blocks uv forever. Run 31453853006 hung 43 minutes in this
# exact call. Merging into stdout keeps the output streaming through
# the pipe that IS drained. Old installed bases run their old copy of
# the hand-off script, so the fix must live here, on the Python side
# the update refreshes before dependencies are installed.
synced = subprocess.run(
[
uv_bin,
@@ -792,6 +800,7 @@ def _stage_candidate_venv(
],
cwd=project_root,
env=sync_env,
stderr=subprocess.STDOUT,
check=False,
)
if synced.returncode != 0:
+8 -3
View File
@@ -268,8 +268,13 @@ function Invoke-StreamedHermes([string]$Exe, [string[]]$HermesArgs, [string]$Tag
$proc = [System.Diagnostics.Process]::Start($psi)
$outWriter = [System.IO.File]::CreateText($outFile)
$errWriter = [System.IO.File]::CreateText($errFile)
# Pump synchronously in small reads so the UI stays alive; stderr is
# drained at the end (hermes update is stdout-dominant).
# Drain stderr from the START, asynchronously. uv writes its progress
# to stderr; a redirected pipe nobody reads fills at ~64KB and blocks
# the child mid-update (E2E run 31453853006 deadlocked 43 minutes in
# `uv sync` exactly this way). ReadToEndAsync keeps the pipe empty
# while the loop below pumps stdout.
$errTask = $proc.StandardError.ReadToEndAsync()
# Pump stdout synchronously in small reads so the UI stays alive.
while (-not $proc.HasExited) {
while (-not $proc.StandardOutput.EndOfStream) {
$ln = $proc.StandardOutput.ReadLine()
@@ -289,7 +294,7 @@ function Invoke-StreamedHermes([string]$Exe, [string[]]$HermesArgs, [string]$Tag
if ($ln.Trim()) { Write-HandoffLog ("{0}| {1}" -f $Tag, $ln) }
}
}
$errText = $proc.StandardError.ReadToEnd()
$errText = $errTask.GetAwaiter().GetResult()
if ($errText) {
$errWriter.Write($errText)
foreach ($ln in ($errText -split "`r?`n")) {