Skip to content

fix(desktop): hide PowerShell windows during Windows update and fix update race condition - #61899

Closed
aEgoist wants to merge 1 commit into
NousResearch:mainfrom
aEgoist:fix/windows-update-powerShell-windows
Closed

fix(desktop): hide PowerShell windows during Windows update and fix update race condition#61899
aEgoist wants to merge 1 commit into
NousResearch:mainfrom
aEgoist:fix/windows-update-powerShell-windows

Conversation

@aEgoist

@aEgoist aEgoist commented Jul 10, 2026

Copy link
Copy Markdown

Summary

Fixes two related issues in the Windows update flow (apps/desktop/electron/main.ts) that cause multiple PowerShell windows to appear on screen during an update. See #61898.

Problems

  1. Visible PowerShell windows: When spawning hermes-setup.exe in applyUpdates and handOffWindowsBootstrapRecovery, windowsHide was set to false, so PowerShell windows became visible.

  2. Race condition: applyUpdates used a finally block that reset updateInFlight to false immediately on return. But the app waits UPDATE_HANDOFF_DWELL_MS (2.5s) before quitting. In that window a re-triggered update saw updateInFlight === false and spawned another updater process producing many stacked PowerShell windows.

Changes

  • Set windowsHide: true in both spawn sites.
  • Replace the finally block with a catch block so updateInFlight is only reset on error, keeping the guard held through the 2.5s quit dwell on the success path.
  • Reset updateInFlight explicitly on the POSIX in-app and manual-command early-return paths (which do not quit the app).
  • Add an updateInFlight guard to handOffWindowsBootstrapRecovery to prevent concurrent recovery hand-offs.

Notes

The Rust updater (apps/bootstrap-installer/src-tauri/src/update.rs) already uses CREATE_NO_WINDOW (0x08000000); this change brings main.ts in line with that behavior.

…ace condition

Two issues were fixed:

1. **Hidden PowerShell windows**: Changed windowsHide from alse to 	rue
   when spawning hermes-setup.exe in both �pplyUpdates and
   handOffWindowsBootstrapRecovery functions. This prevents multiple visible
   PowerShell windows from appearing during the update process.

2. **Race condition fix**: Changed the inally block to catch block in
   �pplyUpdates so that updateInFlight is only reset on error, not on
   successful handoff. The previous implementation reset the flag immediately
   when the function returned, but the app waits 2.5 seconds before quitting.
   This window allowed multiple update triggers. Also added updateInFlight
   guard to handOffWindowsBootstrapRecovery to prevent concurrent updates.
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/desktop Electron desktop app (apps/desktop/*) platform/windows Native Windows-specific behavior or breakage sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows labels Jul 10, 2026

@giggling-ginger giggling-ginger left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

This PR correctly targets #61898 (Windows update PowerShell windows + updateInFlight race during the 2.5s handoff dwell). The direction is right, but it is not ready to merge as written: it introduces a soft-failure lockout and breaks an existing desktop electron test.

What looks good

  • Success-path race: replacing finally with catch so updateInFlight stays true through UPDATE_HANDOFF_DWELL_MS before app.quit() is the right fix for stacked updater spawns.
  • Explicit updateInFlight = false on the POSIX in-app and manual-command early returns (those paths do not quit).
  • windowsHide: true on both hermes-setup handoff sites is consistent with the Tauri updater's CREATE_NO_WINDOW approach for console children.
  • Guard on handOffWindowsBootstrapRecovery against concurrent handoffs.

Blocking issues

1. Soft-failure path never clears updateInFlight (regression)

!lock.unlocked returns { ok: false, ... } without throwing. Under the old finally, the flag was always cleared. Under the new catch, it is not.

After one aborted update (another process holds the venv), every later applyUpdates call hits:

if (updateInFlight) {
  throw new Error('An update is already in progress.')
}

…for the rest of the process lifetime, even though the handoff never happened and the app is still running.

Please reset updateInFlight = false on every non-handoff return inside the try (at least the lock-abort path). Same class of issue on handOffWindowsBootstrapRecovery: if spawn / releaseBackendLockForUpdate throws after updateInFlight = true, there is no catch/finally to clear the flag.

2. Existing test still requires windowsHide: false — and now fails

PR only touches main.ts and does not update:

apps/desktop/electron/windows-child-process.test.ts

test('intentional or interactive desktop child processes stay documented', () => {
  assert.match(source, /windowsHide: false/)
  ...
})

After this change, main.ts has zero windowsHide: false sites, so that assertion fails.

Tests run locally

Suite Result
electron/windows-child-process.test.ts (via tsx --test) FAIL — 4 pass / 1 fail
Failing test intentional or interactive desktop child processes stay documentedassert.match(source, /windowsHide: false/)
src/store/updates.test.ts (vitest) PASS (25/25) — renderer/store only; does not exercise main-process handoff
GitHub checks on this PR none reported at review time

No Windows E2E was run here (Linux host). The race + windowsHide behavior still needs a packaged Windows smoke if possible.

Suggested fix before merge

  1. On lock-abort (and any other “stay alive” return), set updateInFlight = false before returning.
  2. Wrap recovery handoff in try/catch (or try/finally) so exceptions clear the flag.
  3. Update windows-child-process.test.ts to assert the two handoff sites use windowsHide: true (or drop the blanket windowsHide: false grep if those were the only intentional sites).
  4. Re-run npm run test:desktop:platforms (or at least tsx --test electron/windows-child-process.test.ts).

Until (1) and (3) land, this does not fully close #61898 without risk of bricking in-session retries and red CI on the desktop platform suite.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for targeting both updater-console visibility and the success-path handoff race. The premise is still present on current main: applyUpdates() and recovery both use windowsHide: false (apps/desktop/electron/main.ts:2599, 2677), and applyUpdates() clears its guard in finally before the 2.5-second dwell (2623-2629).

Problems

  • Replacing finally with catch leaves the normal lock-abort return (apps/desktop/electron/main.ts:2572-2585) with updateInFlight === true. Because that path returns rather than throws, retries then fail at the guard in applyUpdates().
  • apps/desktop/electron/windows-child-process.test.ts:105 requires windowsHide: false. This PR changes the only two such occurrences, so the existing test no longer describes the intended behavior and will fail.

Suggested changes

  • Clear the guard on every path that keeps the application alive, including the !lock.unlocked return; retain it only once a quit handoff is scheduled.
  • Update the child-process test to assert windowsHide: true at both updater handoff sites, and add coverage for a lock-abort followed by a retry.
  • Give the new recovery guard equivalent failure cleanup.

Automated hermes-sweeper review.

@@ -2620,8 +2623,9 @@ async function applyUpdates(opts = {}) {
}, UPDATE_HANDOFF_DWELL_MS)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!lock.unlocked returns { ok: false, ... } instead of throwing, so this catch does not clear the guard on that path. Clear updateInFlight before every non-handoff return (including the lock-abort return), otherwise all later update attempts in this still-running process fail as already in progress.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 11, 2026
@teknium1 teknium1 added the area/install-update Installer, updater, packaging, wheels, doctor label Jul 19, 2026
@aEgoist aEgoist closed this by deleting the head repository Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/install-update Installer, updater, packaging, wheels, doctor comp/desktop Electron desktop app (apps/desktop/*) P2 Medium — degraded but workaround exists platform/windows Native Windows-specific behavior or breakage sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants