Skip to content

windows: fix live streaming for Update-all and companion staging - #5

Merged
sinwe merged 7 commits into
mainfrom
fix/windows-streaming
Sep 13, 2026
Merged

sinwe merged 7 commits into
mainfrom
fix/windows-streaming

Conversation

@sinwe

@sinwe sinwe commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • applier_windows.go's runWindowsUpdateInstall used a direct bytes.Buffer + cmd.Run, bypassing runCapped's OutputSink/linetee tap entirely -- the admin live-output pane stayed empty for the whole PowerShell download+install, then jumped straight to the final result. Switched to runCapped(ctx, cmd) so each line streams as it happens, same as apt does on Linux.
  • selfupdate_windows.go's stageCompanionUpdate did a plain Go download with no output at all. Added resolving.../downloading.../download complete, staging... narration via the existing emitFromContext helper.

This is the root cause of "Windows doesn't stream on Update all" -- confirmed streaming already works correctly on Ubuntu, WSL Ubuntu, and Raspberry Pi (all go through the same runCapped path for apt), so this was Windows-specific.

Test plan

  • go build/go vet clean on Linux and GOOS=windows.
  • go test ./... clean except the two pre-existing, sandbox-only Unix-socket-path failures unrelated to this change.
  • Real Windows host: click "Update all" and confirm the Windows Update install output now streams live instead of appearing all at once at the end.

runWindowsUpdateInstall used direct bytes.Buffer + cmd.Run, bypassing
runCapped's OutputSink/linetee, so admin live output stayed empty
until the whole PowerShell download+install finished. Switch to
runCapped(ctx, cmd) so each Write-Output line is tee'd to the
aggregator via StreamOutput as it happens, like apt on Linux.
stageCompanionUpdate did pure Go download with no runCapped, so live
pane stayed empty until done. Emit resolving/downloading/staged lines
via existing emitFromContext helper.
timeout.exe refuses to run at all without a real console attached:
"ERROR: Input redirection is not supported, exiting the process
immediately." Confirmed live: every retry-delay call in this script
hit this when install.bat was re-invoked non-interactively by the
companion (a Windows Service, no console) for a self-update. The
errors were silently swallowed (redirected to nul) in most call
sites, but the practical effect was real -- every one of these delays
silently did nothing, back-to-back, instead of actually waiting:

- download_binary's antivirus-lock retry loop fired all 5 attempts
  instantly with no gap, giving Windows Defender's scan zero time to
  release a freshly-downloaded exe before giving up.
- stop_if_running's 30-iteration stop-wait loop blew through in
  milliseconds instead of ~30s, risking a premature force-kill of a
  service that was genuinely still stopping.
- All three uninstall_* paths' post-stop delay before sc delete.

ping needs no console at all (confirmed live, ~1s for `-n 2` against
127.0.0.1 regardless of console presence) -- pinging loopback never
touches the network, so this isn't a connectivity check, purely a
side-effect delay, the standard console-free substitute for exactly
this timeout.exe limitation.
Connect/Disconnect change what the "connected"/"offline" badges show
for a host, independently of any registry mutation (enroll/report/
approve) -- the only things that fired AdminHub.Notify before this.
So a reconnect (e.g. every agent/companion in the fleet, right after
an aggregator restart) was invisible on the admin page until something
else happened to trigger a reload, or the operator refreshed manually.
Wired handleCompanionStream to notify on both, reusing the existing
SSE-push-then-reload mechanism already in place for registry changes.
Confirmed live: "self-update failed (409): agent already has an action
in flight" with no way to recover. pending/agentPending only got cleared
by the specific connection's own deferred Disconnect -- if that
connection instead died uncleanly (crash, network drop, anything that
never delivers a clean TCP close), that cleanup either never ran, or ran
after Connect had already replaced the entry and so no-opped (its
cur.ch == ch guard correctly refusing to touch the new entry). Either
way, the stale marker blocked every future Push for that agent with
ErrActionInFlight/"agent stream busy" forever, since nothing else ever
cleared it short of restarting the whole aggregator process.

Connect now clears the relevant marker itself whenever it replaces an
existing entry (main slot or agentStreams) -- a fresh connection taking
over is exactly the right, safe point to declare whatever the old one
had in flight unreachable, regardless of whether that old connection's
own goroutine ever notices it's gone.

Also corrected a stale doc comment on Connect describing the
existing-companion/new-agent case as "rejected outright" -- that
predated the agentStreams feature; it's actually accepted into
agentStreams now, same as the code already does.
…required

Confirmed live: Windows Gaming Services' own proxy DLL and Microsoft
Edge's background auto-updater both re-queue an entry in
PendingFileRenameOperations after nearly every single boot, forever --
a naive "list is non-empty" check made "Reboot required" (and the
"Needs attention" badge that follows from it) permanently true on this
host regardless of whether a reboot actually just happened, on
practically any real Windows machine with Edge or Gaming Services
installed (i.e. nearly all of them).

Added an ignore-list for known-routine path patterns, checked per-entry
(including the empty second half of a delete pair, which is routine by
definition -- it's never a real path on its own). Anything not matching
is still treated as a real pending change; when in doubt this errs
toward reporting reboot-required, never toward hiding one.

Split the pure matching logic into an untagged reboot_parse.go, same
convention this package already uses for packages_parse.go/
windowsupdate_parse.go -- testable with fixture data on any platform,
not just Windows.
…ntry

Confirmed live on beta5: "Reboot required" stayed stuck true even with
the ignore-list already in place. The Edge pattern had a trailing
backslash (\microsoft\edge\temp\), matching only files/folders nested
*inside* Temp -- but Edge's updater also queues the bare Temp folder
itself as one of the pending entries, with no trailing separator, which
that pattern silently never matched. Dropped the trailing backslash so
it matches the folder path itself too, not just its contents.
@sinwe
sinwe merged commit 6531df9 into main Sep 13, 2026
10 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant