Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 81 additions & 35 deletions apps/desktop/electron/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SpawnOptions } from 'node:child_process'
import { execFile, execFileSync, spawn } from 'node:child_process'
import crypto from 'node:crypto'
import fs from 'node:fs'
Expand Down Expand Up @@ -564,11 +565,20 @@

const HERMES_HOME = resolveHermesHome()

function hermesManagedNodePathEntries() {
function hermesHomeCandidates(hermesHome = HERMES_HOME) {
const candidates = [hermesHome]
const parent = path.dirname(hermesHome)
if (path.basename(parent).toLowerCase() === 'profiles') {

Check warning on line 571 in apps/desktop/electron/main.ts

View workflow job for this annotation

GitHub Actions / JS & TS checks / apps/desktop / check:lint

Expected blank line before this statement
candidates.push(path.dirname(parent))
}
return [...new Set(candidates)]

Check warning on line 574 in apps/desktop/electron/main.ts

View workflow job for this annotation

GitHub Actions / JS & TS checks / apps/desktop / check:lint

Expected blank line before this statement
}

function hermesManagedNodePathEntries(hermesHome = HERMES_HOME) {
// NOTE: keep this ordering in sync with iter_hermes_node_dirs() in
// hermes_constants.py — this Node main process cannot import the Python
// module, so the platform-ordering rule is mirrored here.
const root = path.join(HERMES_HOME, 'node')
const root = path.join(hermesHome, 'node')
const bin = path.join(root, 'bin')
const entries = IS_WINDOWS ? [root, bin] : [bin, root]

Expand All @@ -579,6 +589,10 @@
return [...hermesManagedNodePathEntries(), ...entries, process.env.PATH].filter(Boolean).join(path.delimiter)
}

function pathWithHermesManagedNodeForHome(hermesHome, ...entries) {
return [...hermesManagedNodePathEntries(hermesHome), ...entries, process.env.PATH].filter(Boolean).join(path.delimiter)
}

// ACTIVE_HERMES_ROOT — the canonical mutable Hermes install. Same path
// install.ps1 / install.sh use, so a desktop-only user and a CLI-only user end
// up with identical layouts and can share one install.
Expand Down Expand Up @@ -1746,7 +1760,7 @@
// reports as a blocker, aborting every update attempt.
function updateGateDeps() {
return {
hasLiveMarker: () => Boolean(readLiveUpdateMarker(HERMES_HOME)),
hasLiveMarker: () => hermesHomeCandidates().some(hermesHome => Boolean(readLiveUpdateMarker(hermesHome))),
isUpdateInFlight: () => updateInFlight
}
}
Expand Down Expand Up @@ -2601,11 +2615,63 @@
// the desktop never touches its own bits while running. Returns null when the
// updater isn't staged (e.g. a dev/source run that never went through the
// installer); callers degrade gracefully.
function resolveUpdaterBinary() {
const name = IS_WINDOWS ? 'hermes-setup.exe' : 'hermes-setup'
const candidate = path.join(HERMES_HOME, name)
function resolveUpdaterBinary(
candidates = hermesHomeCandidates(),
{ isWindows = IS_WINDOWS, exists = fileExists } = {}
) {
const name = isWindows ? 'hermes-setup.exe' : 'hermes-setup'
for (const hermesHome of candidates) {

Check warning on line 2623 in apps/desktop/electron/main.ts

View workflow job for this annotation

GitHub Actions / JS & TS checks / apps/desktop / check:lint

Expected blank line before this statement
const candidate = path.join(hermesHome, name)
if (exists(candidate)) {

Check warning on line 2625 in apps/desktop/electron/main.ts

View workflow job for this annotation

GitHub Actions / JS & TS checks / apps/desktop / check:lint

Expected blank line before this statement
if (hermesHome !== HERMES_HOME) {
rememberLog(`[updates] resolved staged updater from base Hermes home: ${candidate}`)
}
return candidate

Check warning on line 2629 in apps/desktop/electron/main.ts

View workflow job for this annotation

GitHub Actions / JS & TS checks / apps/desktop / check:lint

Expected blank line before this statement
}
}
return null

Check warning on line 2632 in apps/desktop/electron/main.ts

View workflow job for this annotation

GitHub Actions / JS & TS checks / apps/desktop / check:lint

Expected blank line before this statement
}

function updaterHermesHome(updater) {
return updater ? path.dirname(updater) : HERMES_HOME
}

return fileExists(candidate) ? candidate : null
function resolveUpdateRootForHermesHome(hermesHome) {
return hermesHome === HERMES_HOME ? resolveUpdateRoot() : path.join(hermesHome, 'hermes-agent')
}

function updateMarkerHomes(primaryHermesHome, candidates = hermesHomeCandidates()) {
return [...new Set([primaryHermesHome, ...candidates].filter(Boolean))]
}

function writeUpdateMarkers(primaryHermesHome, pid) {
const homes = updateMarkerHomes(primaryHermesHome)
for (const hermesHome of homes) {

Check warning on line 2649 in apps/desktop/electron/main.ts

View workflow job for this annotation

GitHub Actions / JS & TS checks / apps/desktop / check:lint

Expected blank line before this statement
writeUpdateMarker(hermesHome, pid)
}
}

function updaterHandoffContext(updater) {
const handoffHome = updaterHermesHome(updater)
const updateRoot = resolveUpdateRootForHermesHome(handoffHome)
const venvBin = path.join(updateRoot, 'venv', IS_WINDOWS ? 'Scripts' : 'bin')

return {
handoffHome,
updateRoot,
venvBin,
spawnOptions: {
cwd: handoffHome,
env: {
...process.env,
HERMES_HOME: handoffHome,
PATH: pathWithHermesManagedNodeForHome(handoffHome, venvBin)
},
detached: true,
stdio: 'ignore',
windowsHide: false
}
}
}

function repairMacUpdaterHelper(updater) {
Expand Down Expand Up @@ -2884,7 +2950,8 @@
})
repairMacUpdaterHelper(updater)

const updateRoot = resolveUpdateRoot()
const handoff = updaterHandoffContext(updater)
const { handoffHome, updateRoot, venvBin } = handoff
const { branch: configuredBranch } = readDesktopUpdateConfig()
const branch = await resolveHealedBranch(updateRoot, configuredBranch || DEFAULT_UPDATE_BRANCH)
const updaterArgs = ['--update', '--branch', branch]
Expand All @@ -2894,13 +2961,10 @@
updaterArgs.push('--target-app', targetApp)
}

const venvBin = path.join(updateRoot, 'venv', IS_WINDOWS ? 'Scripts' : 'bin')

// ── Pre-flight state.db integrity guard (#68474) ─────────────────
// Emergency backup and header verification before the update touches
// anything. Runs while the backend is still alive.
preflightStateDb(HERMES_HOME, rememberLog)

// Stop our own backend(s) and wait for the venv shim to unlock BEFORE we
// spawn the updater. Without this the updater races a still-locked
// hermes.exe (held by the backend child / its grandchildren) and the update
Expand Down Expand Up @@ -2958,16 +3022,7 @@

// 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 = spawnUpdaterProcess(updater, updaterArgs, {
cwd: HERMES_HOME,
env: {
...process.env,
HERMES_HOME,
PATH: pathWithHermesManagedNode(venvBin)
},
detached: true,
stdio: 'ignore'
})
const child = spawnUpdaterProcess(updater, updaterArgs, handoff.spawnOptions as SpawnOptions)

// Write the update-in-progress marker IMMEDIATELY — before the 2.5s
// quit dwell. The Tauri updater won't write its own marker for several
Expand All @@ -2977,7 +3032,7 @@
// waitForUpdateToFinish() gate sees a live update and parks instead.
// The updater overwrites this with its own PID later; same format.
if (Number.isInteger(child.pid)) {
writeUpdateMarker(HERMES_HOME, child.pid)
writeUpdateMarkers(handoffHome, child.pid)
}

rememberLog(`[updates] launched updater: ${updater} ${updaterArgs.join(' ')}; exiting desktop to release venv shim`)
Expand Down Expand Up @@ -3009,14 +3064,14 @@
return false
}

const updateRoot = resolveUpdateRoot()
const handoff = updaterHandoffContext(updater)
const { handoffHome, updateRoot, venvBin } = handoff
const { branch: configuredBranch } = readDesktopUpdateConfig()

const branch = directoryExists(path.join(updateRoot, '.git'))
? await resolveHealedBranch(updateRoot, configuredBranch || DEFAULT_UPDATE_BRANCH)
: configuredBranch || DEFAULT_UPDATE_BRANCH

const venvBin = path.join(updateRoot, 'venv', IS_WINDOWS ? 'Scripts' : 'bin')
const venvHermes = path.join(venvBin, IS_WINDOWS ? 'hermes.exe' : 'hermes')
const venvPython = path.join(venvBin, IS_WINDOWS ? 'python.exe' : 'python')

Expand All @@ -3033,22 +3088,13 @@

await releaseBackendLockForUpdate(updateRoot)

const child = spawnUpdaterProcess(updater, updaterArgs, {
cwd: HERMES_HOME,
env: {
...process.env,
HERMES_HOME,
PATH: pathWithHermesManagedNode(venvBin)
},
detached: true,
stdio: 'ignore'
})
const child = spawnUpdaterProcess(updater, updaterArgs, handoff.spawnOptions as SpawnOptions)

// 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.
if (Number.isInteger(child.pid)) {
writeUpdateMarker(HERMES_HOME, child.pid)
writeUpdateMarkers(handoffHome, child.pid)
}

rememberLog(
Expand Down
Loading