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>
This commit is contained in:
Gille 2026-07-16 20:50:40 -06:00 committed by GitHub
parent 1f7d2be22f
commit 531e5763e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 103 additions and 10 deletions

View file

@ -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.

View file

@ -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' })
})

View file

@ -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
}