From 531e5763e8f1dd6eb5c9855c184d39ff2003b1b4 Mon Sep 17 00:00:00 2001 From: Gille <4317663+helix4u@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:50:40 -0600 Subject: [PATCH] fix(desktop): hide Windows updater console during handoff (#66040) * fix(desktop): hide Windows updater console (#56884) * test(desktop): cover hidden updater handoffs behaviorally --------- Co-authored-by: Kyssta <218078013+kyssta-exe@users.noreply.github.com> --- apps/desktop/electron/main.ts | 15 ++--- apps/desktop/electron/updater-process.test.ts | 62 +++++++++++++++++++ apps/desktop/electron/updater-process.ts | 36 +++++++++++ 3 files changed, 103 insertions(+), 10 deletions(-) create mode 100644 apps/desktop/electron/updater-process.test.ts create mode 100644 apps/desktop/electron/updater-process.ts diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 289a62b7cf73..af25ae7712ab 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -126,6 +126,7 @@ import { sandboxPreflight } from './update-relaunch' import { isOfficialSshRemote, OFFICIAL_REPO_HTTPS_URL } from './update-remote' +import { spawnUpdaterProcess } from './updater-process' import { fetchMarketplaceThemes, searchMarketplaceThemes } from './vscode-marketplace' import { computeWindowOptions, @@ -2520,7 +2521,7 @@ async function applyUpdates(opts = {}) { // Detached so the updater outlives this process — it needs us GONE before // `hermes update` will run (the venv shim is locked while we live). - const child = spawn(updater, updaterArgs, { + const child = spawnUpdaterProcess(updater, updaterArgs, { cwd: HERMES_HOME, env: { ...process.env, @@ -2528,12 +2529,9 @@ async function applyUpdates(opts = {}) { PATH: pathWithHermesManagedNode(venvBin) }, detached: true, - stdio: 'ignore', - windowsHide: false + stdio: 'ignore' }) - child.unref() - // Write the update-in-progress marker IMMEDIATELY — before the 2.5s // quit dwell. The Tauri updater won't write its own marker for several // seconds (window init + manifest), and during that gap our renderer @@ -2598,7 +2596,7 @@ async function handOffWindowsBootstrapRecovery(reason) { await releaseBackendLockForUpdate(updateRoot) - const child = spawn(updater, updaterArgs, { + const child = spawnUpdaterProcess(updater, updaterArgs, { cwd: HERMES_HOME, env: { ...process.env, @@ -2606,12 +2604,9 @@ async function handOffWindowsBootstrapRecovery(reason) { PATH: pathWithHermesManagedNode(venvBin) }, detached: true, - stdio: 'ignore', - windowsHide: false + stdio: 'ignore' }) - child.unref() - // Same marker pre-write as applyUpdates — see comment there. The recovery // hand-off has the same window where the renderer can respawn a backend // before the updater writes its own marker. diff --git a/apps/desktop/electron/updater-process.test.ts b/apps/desktop/electron/updater-process.test.ts new file mode 100644 index 000000000000..ce61855dff52 --- /dev/null +++ b/apps/desktop/electron/updater-process.test.ts @@ -0,0 +1,62 @@ +import assert from 'node:assert/strict' +import type { SpawnOptions } from 'node:child_process' + +import { test } from 'vitest' + +import { spawnUpdaterProcess } from './updater-process' + +test('spawnUpdaterProcess hides the updater console and detaches the child on Windows', () => { + const calls: Array<{ args: string[]; command: string; options: SpawnOptions }> = [] + let unrefCalls = 0 + + const child = { + pid: 4242, + unref: () => { + unrefCalls += 1 + } + } + + const result = spawnUpdaterProcess( + 'hermes-setup.exe', + ['--update', '--branch', 'main'], + { cwd: 'C:\\Hermes', detached: true, stdio: 'ignore' }, + { + isWindows: true, + spawnProcess: (command, args, options) => { + calls.push({ args, command, options }) + + return child + } + } + ) + + assert.equal(result, child) + assert.equal(unrefCalls, 1) + assert.deepEqual(calls, [ + { + args: ['--update', '--branch', 'main'], + command: 'hermes-setup.exe', + options: { cwd: 'C:\\Hermes', detached: true, stdio: 'ignore', windowsHide: true } + } + ]) +}) + +test('spawnUpdaterProcess preserves updater options off Windows', () => { + let capturedOptions: SpawnOptions | undefined + + spawnUpdaterProcess( + 'hermes-setup', + ['--update'], + { detached: true, stdio: 'ignore' }, + { + isWindows: false, + spawnProcess: (_command, _args, options) => { + capturedOptions = options + + return { unref: () => {} } + } + } + ) + + assert.deepEqual(capturedOptions, { detached: true, stdio: 'ignore' }) +}) diff --git a/apps/desktop/electron/updater-process.ts b/apps/desktop/electron/updater-process.ts new file mode 100644 index 000000000000..d0706a6e5140 --- /dev/null +++ b/apps/desktop/electron/updater-process.ts @@ -0,0 +1,36 @@ +import { spawn, type SpawnOptions } from 'node:child_process' + +import { hiddenWindowsChildOptions } from './windows-child-options' + +export interface UpdaterChild { + pid?: number + unref: () => void +} + +export interface SpawnUpdaterProcessDeps { + isWindows?: boolean + spawnProcess?: (command: string, args: string[], options: SpawnOptions) => UpdaterChild +} + +/** + * Spawn the detached installer used for update and bootstrap-recovery handoffs. + * The helper owns both hidden-console selection and unref semantics so every + * updater handoff follows the same behavior and can be tested without Electron. + */ +export function spawnUpdaterProcess( + updater: string, + updaterArgs: string[], + options: SpawnOptions, + deps: SpawnUpdaterProcessDeps = {} +): UpdaterChild { + const isWindows = deps.isWindows ?? process.platform === 'win32' + const spawnOptions = hiddenWindowsChildOptions(options, isWindows) as SpawnOptions + + const child = deps.spawnProcess + ? deps.spawnProcess(updater, updaterArgs, spawnOptions) + : spawn(updater, updaterArgs, spawnOptions) + + child.unref() + + return child +}