From 0229246ab879b9968c9fcc384d8d5d0add939771 Mon Sep 17 00:00:00 2001 From: Cornna <96944678+ymylive@users.noreply.github.com> Date: Sun, 28 Jun 2026 02:21:56 -0700 Subject: [PATCH] fix(desktop): probe venv runtime health before trusting bootstrap marker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A broken/empty Windows launcher venv can see the source tree via PYTHONPATH but lack PyYAML, so 'import hermes_cli' succeeds while the first real CLI import dies — the desktop then trusts the bootstrap marker, spawns a dead backend, and loops on 'gateway offline' (#52378). - backend-probes.cjs: canImportHermesCli now runs 'import yaml; import hermes_cli.config' (extracted as hermesRuntimeImportProbe) and accepts an env override, so a dependency regression is caught without a real broken venv fixture. - main.cjs: isBootstrapComplete() routes through new isActiveRuntimeUsable(), which requires the venv python to pass the runtime import probe (with ACTIVE_HERMES_ROOT on PYTHONPATH) — not just exist on disk. Salvaged from PR #38179. The PR's install.ps1 reset/clean + autocrlf changes and their tests are dropped: current main already preserves dirty checkouts via stash (the data-loss-safe #38542 path) rather than the PR's older reset-based Repair-ManagedCheckoutBeforeUpdate approach. --- apps/desktop/electron/backend-probes.cjs | 25 ++++++++++++++++--- apps/desktop/electron/backend-probes.test.cjs | 8 +++++- apps/desktop/electron/main.cjs | 20 ++++++++++++++- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/apps/desktop/electron/backend-probes.cjs b/apps/desktop/electron/backend-probes.cjs index 0e0e5848481..b69bd3f0858 100644 --- a/apps/desktop/electron/backend-probes.cjs +++ b/apps/desktop/electron/backend-probes.cjs @@ -37,7 +37,18 @@ const { execFileSync } = require('node:child_process') const PROBE_TIMEOUT_MS = 5000 /** - * Return true iff `python -c "import hermes_cli"` exits 0. + * Return the Python snippet used to verify Hermes can import far enough to + * launch the CLI. Kept exported for tests so dependency regressions are + * caught without needing a real broken venv fixture. + * + * @returns {string} + */ +function hermesRuntimeImportProbe() { + return 'import yaml; import hermes_cli.config' +} + +/** + * Return true iff the Hermes runtime import probe exits 0. * * Used to gate the "fallback to system Python with hermes_cli installed" * rung of resolveHermesBackend. Without this, a system Python 3.11-3.13 @@ -46,13 +57,20 @@ const PROBE_TIMEOUT_MS = 5000 * site-packages -- and the resolver returns a backend that immediately * dies on spawn. * + * The probe intentionally imports hermes_cli.config, not just the top-level + * package: a broken/empty Windows launcher venv can still see the source tree + * through PYTHONPATH but lack PyYAML, then die on the first real CLI import. + * * @param {string} pythonPath - Absolute path to a python.exe / python. + * @param {object} [opts] + * @param {object} [opts.env] - Additional environment for the probe. * @returns {boolean} */ -function canImportHermesCli(pythonPath) { +function canImportHermesCli(pythonPath, opts = {}) { if (!pythonPath) return false try { - execFileSync(pythonPath, ['-c', 'import hermes_cli'], { + execFileSync(pythonPath, ['-c', hermesRuntimeImportProbe()], { + env: { ...process.env, ...(opts.env || {}) }, stdio: 'ignore', timeout: PROBE_TIMEOUT_MS, windowsHide: true @@ -101,6 +119,7 @@ function verifyHermesCli(hermesCommand, opts = {}) { module.exports = { canImportHermesCli, + hermesRuntimeImportProbe, verifyHermesCli, PROBE_TIMEOUT_MS } diff --git a/apps/desktop/electron/backend-probes.test.cjs b/apps/desktop/electron/backend-probes.test.cjs index 13d30a286c1..93158a32ce8 100644 --- a/apps/desktop/electron/backend-probes.test.cjs +++ b/apps/desktop/electron/backend-probes.test.cjs @@ -11,7 +11,7 @@ const fs = require('node:fs') const os = require('node:os') const path = require('node:path') -const { canImportHermesCli, verifyHermesCli } = require('./backend-probes.cjs') +const { canImportHermesCli, hermesRuntimeImportProbe, verifyHermesCli } = require('./backend-probes.cjs') // Resolve the host's own Node binary -- guaranteed to be on disk and // runnable. We use it as both a stand-in for "a python that doesn't @@ -40,6 +40,12 @@ test('canImportHermesCli returns false when binary does not exist', () => { assert.equal(canImportHermesCli(ghost), false) }) +test('hermes runtime import probe checks config dependencies', () => { + const probe = hermesRuntimeImportProbe() + assert.match(probe, /\bimport yaml\b/) + assert.match(probe, /\bimport hermes_cli\.config\b/) +}) + test('verifyHermesCli returns false when command is falsy', () => { assert.equal(verifyHermesCli(''), false) assert.equal(verifyHermesCli(null), false) diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs index 2a4b2fcb29c..64187c92c13 100644 --- a/apps/desktop/electron/main.cjs +++ b/apps/desktop/electron/main.cjs @@ -2608,6 +2608,24 @@ function readBootstrapMarker() { return readJson(BOOTSTRAP_COMPLETE_MARKER) } +// Marker-independent: is the canonical install at ACTIVE_HERMES_ROOT actually +// runnable right now? A complete CLI install (`install.sh --include-desktop`) +// or a DMG launch over a prior CLI install satisfies this WITHOUT the desktop +// ever having written the bootstrap marker -- so we must be able to recognise +// "already installed" off the filesystem alone, not just the marker. +function isActiveRuntimeUsable() { + const venvPython = getVenvPython(VENV_ROOT) + return ( + isHermesSourceRoot(ACTIVE_HERMES_ROOT) && + fileExists(venvPython) && + canImportHermesCli(venvPython, { + env: { + PYTHONPATH: [ACTIVE_HERMES_ROOT, process.env.PYTHONPATH].filter(Boolean).join(path.delimiter) + } + }) + ) +} + function isBootstrapComplete() { const marker = readBootstrapMarker() if (!marker || typeof marker !== 'object') return false @@ -2620,7 +2638,7 @@ function isBootstrapComplete() { // a runnable venv: an interrupted or split-home install can leave the marker // + checkout without a venv, and trusting that spawns a dead backend // ("gateway offline") instead of re-running bootstrap to repair it. - return isHermesSourceRoot(ACTIVE_HERMES_ROOT) && fileExists(getVenvPython(VENV_ROOT)) + return isActiveRuntimeUsable() } function writeBootstrapMarker(payload) {