Files
aiturk-hermes-ide/apps/desktop/electron/gateway-stop-before-update.ts
T

98 lines
3.6 KiB
TypeScript

/**
* gateway-stop-before-update.ts
*
* Windows-only helper for the update hand-off (#70337): stop every
* separately-running messaging gateway BEFORE the venv-shim lock poll.
*
* Why not just tree-kill gateway.pid's PID:
* - gateway.pid records the uv WORKER process, but the venv shim lock is
* held by its parent LAUNCHER (venv\Scripts\python.exe). taskkill /T from
* the worker PID does not reach parents, so the lock could survive.
* - a single gateway.pid read misses multi-profile setups entirely.
*
* So we delegate to `hermes gateway stop --all`: the CLI discovers every
* profile's gateway processes (launcher + worker) via find_gateway_pids,
* drains in-flight agents (planned-stop marker -> resume_pending), and
* force-kills survivors — the same logic `hermes update`'s
* _pause_windows_gateways_for_update relies on.
*
* Pure + dependency-injected so the launcher/worker and multi-profile
* behavior is assertable without booting Electron.
*/
import { execFileSync, type ExecFileSyncOptionsWithStringEncoding } from 'node:child_process'
import fs from 'node:fs'
export interface StopGatewayBeforeUpdateDeps {
/** Defaults to process.platform === 'win32'; injectable for tests. */
isWindows?: boolean
/** Defaults to fs.existsSync; injectable for tests. */
existsSync?: (p: string) => boolean
/** Defaults to execFileSync from node:child_process; injectable for tests. */
execFileSync?: (command: string, args: string[], options: ExecFileSyncOptionsWithStringEncoding) => Buffer | string
/** Observability hook for tests. */
spy?: (command: string, args: string[]) => void
}
export const GATEWAY_STOP_TIMEOUT_MS = 20_000
/**
* Best-effort stop of all-profile messaging gateways via the CLI.
* Never throws: a wedged/absent CLI must not abort the update hand-off
* (the shim-lock poll + the updater's venv-blocker scan still fail loudly
* if the venv stays held). Returns true when the CLI ran (or was invoked
* with the injected spy), false when skipped (non-Windows / missing CLI).
*/
export function stopGatewayBeforeUpdate(
hermesCliPath: string,
hermesHome: string,
deps: StopGatewayBeforeUpdateDeps = {}
): boolean {
return runGatewayLifecycleCommand(hermesCliPath, ['gateway', 'stop', '--all'], deps)
}
/**
* Drain-semantics counterpart (#76057 review): `gateway stop --all` before
* the lock gate takes gateways down even when the update later ABORTS
* (venv-blocked by a user terminal, probe failure, updater spawn failure).
* The updater's own pause machinery resumes what it pauses — the Desktop
* must mirror that on its abort paths, or a failed update strands every
* profile's gateway stopped. Best-effort, never throws.
*/
export function startGatewaysAfterUpdateAbort(hermesCliPath: string, deps: StopGatewayBeforeUpdateDeps = {}): boolean {
return runGatewayLifecycleCommand(hermesCliPath, ['gateway', 'start', '--all'], deps)
}
function runGatewayLifecycleCommand(hermesCliPath: string, args: string[], deps: StopGatewayBeforeUpdateDeps): boolean {
const isWindows = deps.isWindows ?? process.platform === 'win32'
if (!isWindows) {
return false
}
const existsSync = deps.existsSync ?? fs.existsSync
const exec = deps.execFileSync ?? execFileSync
if (deps.spy) {
deps.spy(hermesCliPath, args)
}
if (!existsSync(hermesCliPath)) {
return false
}
try {
exec(hermesCliPath, args, {
timeout: GATEWAY_STOP_TIMEOUT_MS,
windowsHide: true,
stdio: 'ignore',
encoding: 'utf8'
})
return true
} catch {
// Best-effort (see header comment).
return false
}
}