hermes-agent/apps/desktop/electron/updater-process.ts

91 lines
3.1 KiB
TypeScript

import { spawn, type SpawnOptions } from 'node:child_process'
import { statSync } from 'node:fs'
import path from 'node:path'
import { hiddenWindowsChildOptions } from './windows-child-options'
export interface UpdaterChild {
pid?: number
unref: () => void
}
export interface ResolveStagedUpdaterBinaryDeps {
isWindows?: boolean
fileExists?: (candidate: string) => boolean
}
function stagedFileExists(candidate: string): boolean {
try {
return statSync(candidate).isFile()
} catch {
return false
}
}
/**
* Decide which staged installer binary — if any — may be handed an update.
*
* The Tauri installer self-copies into HERMES_HOME on *every* platform
* (`hermes-setup.exe` on Windows, `hermes-setup` elsewhere — see
* apps/bootstrap-installer `paths::installer_dest` and
* `bootstrap::copy_self_to_hermes_home`), so finding that binary on macOS or
* Linux is expected, not leftover junk.
*
* Handing an update to it is nonetheless a Windows-only policy. Windows needs
* the quit -> hand-off -> rebuild dance because a venv shim file lock keeps the
* running desktop from rewriting its own bits; macOS and Linux have no such
* lock and update in place through applyUpdatesPosixInApp(). Off Windows the
* hand-off therefore buys nothing and costs a great deal: a staged binary older
* than the hand-off protocol holds the update marker, spawns `hermes update`,
* and that child refuses its own parent — wedging the in-app Update button for
* good, with no route (update, re-download, reinstall) to a newer binary
* (#74836). Returning null off Windows is what routes those platforms to the
* in-app updater.
*
* Null on Windows too when nothing is staged (a dev/source run, or a CLI
* install that never went through the installer); callers degrade gracefully.
*/
export function resolveStagedUpdaterBinary(
hermesHome: string,
deps: ResolveStagedUpdaterBinaryDeps = {}
): string | null {
const isWindows = deps.isWindows ?? process.platform === 'win32'
if (!isWindows) {
return null
}
const fileExists = deps.fileExists ?? stagedFileExists
const candidate = path.join(hermesHome, 'hermes-setup.exe')
return fileExists(candidate) ? candidate : null
}
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
}