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:
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
Reference in New Issue
Block a user