Skip to content
Open
Show file tree
Hide file tree
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
50 changes: 39 additions & 11 deletions apps/bootstrap-installer/src-tauri/src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,17 +224,7 @@ async fn run_update(app: AppHandle) -> Result<()> {
&format!("[update] updating against branch {update_branch}"),
);
let child_env = update_child_env(&install_root);
let mut update_args: Vec<String> =
vec!["update".into(), "--yes".into(), "--gateway".into()];
// --force skips `hermes update`'s Windows running-exe guard (which would
// `sys.exit(2)` and dead-end the handoff). By contract the desktop has
// already exited and waited for the install locks to clear before launching
// us, and wait_for_install_locks_free below force-kills any straggler — so by the
// time `hermes update` runs there is no legitimate hermes.exe to protect,
// and the guard would only produce a false "Hermes is still running" stop.
update_args.push("--force".into());
update_args.push("--branch".into());
update_args.push(update_branch);
let update_args = build_update_args(&update_branch);

emit_stage(&app, "update", StageState::Running, None, None);
let started = Instant::now();
Expand Down Expand Up @@ -762,6 +752,25 @@ where
.filter(|s| !s.is_empty())
}

fn build_update_args(update_branch: &str) -> Vec<String> {
vec![
"update".into(),
"--yes".into(),
"--gateway".into(),
// Desktop updates replace the app underneath the user. Force the same
// restore point users can request from the CLI before any mutation.
"--backup".into(),
// --force skips `hermes update`'s Windows running-exe guard (which
// would `sys.exit(2)` and dead-end the handoff). By contract the
// desktop has already exited and waited for the venv shim to unlock
// before launching us, and wait_for_venv_free force-kills any
// straggler, so the guard would only produce a false stop.
"--force".into(),
"--branch".into(),
update_branch.into(),
]
}

fn target_app_from_args<I, S>(args: I) -> Option<PathBuf>
where
I: IntoIterator<Item = S>,
Expand Down Expand Up @@ -1101,6 +1110,25 @@ mod tests {
assert_eq!(update_branch_from_args(["--update"]), None);
}

#[test]
fn desktop_update_args_force_pre_update_backup() {
let args = build_update_args("main");

assert!(args.contains(&"--backup".to_string()));
assert_eq!(
args,
vec![
"update".to_string(),
"--yes".to_string(),
"--gateway".to_string(),
"--backup".to_string(),
"--force".to_string(),
"--branch".to_string(),
"main".to_string(),
]
);
}

#[test]
fn rebuild_retries_only_on_failure() {
assert!(!rebuild_needs_retry(Some(0)), "a clean rebuild must not retry");
Expand Down
60 changes: 44 additions & 16 deletions apps/desktop/electron/main.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ const { worktreesForIpc } = require('./git-worktrees.cjs')
const { OFFICIAL_REPO_HTTPS_URL, isOfficialSshRemote } = require('./update-remote.cjs')
const { resolveBehindCount, shouldCountCommits } = require('./update-count.cjs')
const { runRebuildWithRetry } = require('./update-rebuild.cjs')
const {
buildHermesUpdateArgs,
buildManualHermesUpdateCommand,
createUpdaterLaunchPlan
} = require('./update-handoff.cjs')
const { killHermesRuntimeProcessesForUpdate } = require('./update-processes.cjs')
const {
buildPosixCleanupScript,
buildWindowsCleanupScript,
Expand Down Expand Up @@ -1913,6 +1919,17 @@ async function releaseBackendLock(updateRoot, tag) {
stopAllPoolBackends()
for (const pid of pids) forceKillProcessTree(pid)

const runtimePids = killHermesRuntimeProcessesForUpdate(updateRoot, {
currentPid: process.pid,
killTree: forceKillProcessTree,
onListError: err =>
rememberLog(`[updates] could not enumerate Hermes runtime processes before update: ${err?.message || err}`),
onError: (pid, err) => rememberLog(`[updates] failed to stop Hermes runtime process ${pid}: ${err?.message || err}`)
})
if (runtimePids.length) {
rememberLog(`[updates] stopped ${runtimePids.length} Hermes runtime process(es) before update`)
}

const shim = venvHermesShimPath(updateRoot)
const deadlineMs = Date.now() + 15000
while (Date.now() < deadlineMs) {
Expand Down Expand Up @@ -1959,21 +1976,22 @@ async function applyUpdates(opts = {}) {
// hermes-setup.exe into HERMES_HOME). They DO have a working `hermes`
// on PATH / in the venv, so the correct path is the one-liner in their
// native medium. We show the EXACT command, branch-pinned to the
// checkout they're on — bare `hermes update` defaults to main and would
// silently switch a bb/gui (or any non-main) install off-branch. Mirror
// the GUI button's contract: append --branch <current> for non-main
// checkouts, keep it bare for main so the card stays clean.
// checkout they're on — unpinned `hermes update --backup` defaults to
// main and would silently switch a bb/gui (or any non-main) install
// off-branch. Mirror the GUI button's contract: append --branch
// <current> for non-main checkouts, omit it for main so the card stays
// clean.
const updateRoot = resolveUpdateRoot()
let command = 'hermes update'
let command = buildManualHermesUpdateCommand()
try {
const head = await runGit(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: updateRoot })
const current = (head.stdout || '').trim()
if (head.code === 0 && current && current !== 'HEAD') {
const branch = await resolveHealedBranch(updateRoot, current)
if (branch !== 'main') command = `hermes update --branch ${branch}`
if (branch !== 'main') command = buildManualHermesUpdateCommand(branch)
}
} catch {
// Best-effort: fall back to bare `hermes update` if branch detection fails.
// Best-effort: fall back to branch-agnostic `hermes update --backup` if detection fails.
}
rememberLog(`[updates] no staged updater; surfacing manual \`${command}\` for CLI install at ${updateRoot}`)
emitUpdateProgress({ stage: 'manual', message: command, percent: null })
Expand Down Expand Up @@ -2005,20 +2023,29 @@ 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 launch = createUpdaterLaunchPlan({
handoffDir: path.join(HERMES_HOME, 'logs'),
isWindows: IS_WINDOWS,
updater,
updaterArgs
})
const child = spawn(launch.command, launch.args, {
cwd: HERMES_HOME,
env: {
...process.env,
HERMES_HOME,
PATH: pathWithHermesManagedNode(venvBin)
},
detached: true,
detached: launch.detached,
stdio: 'ignore',
windowsHide: false
windowsHide: launch.windowsHide
})
child.unref()

rememberLog(`[updates] launched updater: ${updater} ${updaterArgs.join(' ')}; exiting desktop to release venv shim`)
rememberLog(
`[updates] launched updater: ${launch.command} ${launch.args.join(' ')}; ` +
`script=${launch.scriptPath || 'direct'}; exiting desktop to release venv shim`
)

// Linger on the "updating — don't reopen" overlay long enough for the user
// to actually read it (and to bridge the gap until the updater's own window
Expand Down Expand Up @@ -2136,8 +2163,9 @@ async function applyUpdatesPosixInApp() {
const updateRoot = resolveUpdateRoot()
const hermes = resolveHermesCliBinary(updateRoot)
if (!hermes) {
emitUpdateProgress({ stage: 'manual', message: 'hermes update', percent: null })
return { ok: true, manual: true, command: 'hermes update', hermesRoot: updateRoot }
const command = buildManualHermesUpdateCommand()
emitUpdateProgress({ stage: 'manual', message: command, percent: null })
return { ok: true, manual: true, command, hermesRoot: updateRoot }
}

// Put the Hermes-managed Node and the venv on PATH so `hermes desktop`'s
Expand Down Expand Up @@ -2174,19 +2202,19 @@ async function applyUpdatesPosixInApp() {

// Branch-pin so a non-main checkout doesn't get switched to main (and self-heal
// to main when the pinned branch no longer exists on origin).
let branchArgs = []
let updateBranch = null
try {
const head = await runGit(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: updateRoot })
const current = (head.stdout || '').trim()
if (head.code === 0 && current && current !== 'HEAD') {
branchArgs = ['--branch', await resolveHealedBranch(updateRoot, current)]
updateBranch = await resolveHealedBranch(updateRoot, current)
}
} catch {
// best effort
}

emitUpdateProgress({ stage: 'update', message: 'Updating Hermes (git + dependencies)…', percent: 10 })
const updated = await runStreamedUpdate(hermes, ['update', '--yes', ...branchArgs], {
const updated = await runStreamedUpdate(hermes, buildHermesUpdateArgs({ assumeYes: true, branch: updateBranch }), {
cwd: updateRoot,
env,
stage: 'update'
Expand Down
Loading