File size: 8,238 Bytes
9c71ee7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | //! Filesystem paths + logging setup.
//!
//! Mirrors `hermes_constants.get_hermes_home()` from the Python CLI:
//! Windows: %LOCALAPPDATA%\hermes
//! macOS: ~/.hermes
//! Linux: ~/.hermes (override via $HERMES_HOME)
//!
//! NOTE (macOS): Python's get_hermes_home(), scripts/install.sh, and the
//! Electron desktop's resolveHermesHome() ALL use ~/.hermes on macOS — there
//! is no ~/Library/Application Support branch anywhere else. An earlier
//! version of this file used Application Support, which drifted from every
//! other component: the installer wrote the install to one dir and the
//! desktop looked for it in another, so first launch never found the backend.
//!
//! IMPORTANT: this must match exactly. Drift here means install.ps1
//! writes to one place and the installer reads from another, breaking
//! the bootstrap-complete check.
use std::path::{Path, PathBuf};
#[cfg(target_os = "macos")]
use std::process::Command;
use tracing_appender::non_blocking::WorkerGuard;
/// Returns the canonical Hermes home directory, respecting $HERMES_HOME if set.
pub fn hermes_home() -> PathBuf {
if let Ok(override_path) = std::env::var("HERMES_HOME") {
if !override_path.trim().is_empty() {
return PathBuf::from(override_path);
}
}
#[cfg(target_os = "windows")]
{
// %LOCALAPPDATA%\hermes — matches scripts/install.ps1's $HermesHome.
if let Some(local_app_data) = dirs::data_local_dir() {
return local_app_data.join("hermes");
}
}
// macOS + Linux + fallback: ~/.hermes (matches Python get_hermes_home(),
// install.sh, and the Electron desktop's resolveHermesHome()).
if let Some(home) = dirs::home_dir() {
return home.join(".hermes");
}
// Last resort — current dir, almost certainly wrong but at least
// doesn't panic.
PathBuf::from(".hermes")
}
pub fn log_dir() -> PathBuf {
hermes_home().join("logs")
}
pub fn log_path() -> PathBuf {
log_dir().join("bootstrap-installer.log")
}
pub fn bootstrap_cache_dir() -> PathBuf {
hermes_home().join("bootstrap-cache")
}
/// Stable location the installer copies itself to after a successful install.
/// The desktop app re-invokes this with `--update`, and the start-menu /
/// desktop shortcuts can point users back to it. Lives directly under
/// HERMES_HOME so it survives repo checkout deletion (unlike anything under
/// hermes-agent/).
///
/// On Windows this is `%LOCALAPPDATA%\hermes\hermes-setup.exe`; on other
/// platforms the extension differs but the directory is the same.
pub fn installer_dest() -> PathBuf {
let name = if cfg!(target_os = "windows") {
"hermes-setup.exe"
} else {
"hermes-setup"
};
hermes_home().join(name)
}
/// Marker the updater writes for the duration of an in-app update and removes
/// when it finishes (see update.rs `UpdateMarkerGuard`). A freshly-launched
/// desktop checks this before spawning its own local backend: spawning one
/// mid-update re-locks the venv shim and triggers `force_kill_other_hermes`,
/// which then kills that legitimate backend in a respawn loop (#50238).
///
/// Lives directly under HERMES_HOME (same rationale as `installer_dest`) so the
/// Electron desktop — which resolves HERMES_HOME identically and pins it into
/// the updater's env — agrees on the exact path.
pub fn update_in_progress_marker() -> PathBuf {
hermes_home().join(".hermes-update-in-progress")
}
/// Copy the currently-running installer binary to `installer_dest()` so it's
/// available for future `--update` runs and shortcut launches.
///
/// No-ops (returns Ok) when the running exe is ALREADY the destination — which
/// is exactly the case during an `--update` run (the desktop launched us FROM
/// that path), where copying onto ourselves would be a Windows sharing
/// violation. Best-effort: a failure here must not fail the install, so the
/// caller logs and continues.
///
/// NOTE: because of that no-op, a user's staged installer is only ever written
/// by a full install/repair. Every later `--update` runs the ORIGINAL binary,
/// so an installer-protocol change can strand the whole installed base on a
/// binary that predates it (see `restage_from_checkout`, which repairs this
/// from the freshly-updated checkout).
pub fn copy_self_to_hermes_home() -> std::io::Result<()> {
let src = std::env::current_exe()?;
let dest = installer_dest();
// Skip if we're already running from the destination (update re-invocation
// or a prior copy). canonicalize both so symlinks / 8.3 short paths / case
// differences don't trick us into a self-copy.
let same = match (src.canonicalize(), dest.canonicalize()) {
(Ok(a), Ok(b)) => a == b,
_ => src == dest,
};
if same {
tracing::info!(?dest, "installer already at destination; skipping self-copy");
return Ok(());
}
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::copy(&src, &dest)?;
repair_macos_installer_helper(&dest);
tracing::info!(?src, ?dest, "copied installer to HERMES_HOME");
Ok(())
}
#[cfg(target_os = "macos")]
fn repair_macos_installer_helper(path: &Path) {
// The staged helper may inherit quarantine from the downloaded installer.
// Desktop later launches this exact file for in-app updates, so make it
// executable before the update handoff reaches LaunchServices/Gatekeeper.
let _ = Command::new("/usr/bin/xattr")
.args(["-cr"])
.arg(path)
.status();
let verify = Command::new("/usr/bin/codesign")
.arg("--verify")
.arg(path)
.status();
if !matches!(verify, Ok(status) if status.success()) {
let _ = Command::new("/usr/bin/codesign")
.args(["--force", "--sign", "-"])
.arg(path)
.status();
}
}
#[cfg(not(target_os = "macos"))]
fn repair_macos_installer_helper(_path: &Path) {}
/// Where the bootstrap-complete marker lives (existence-only for the Rust
/// installer fast path; JSON schema-checked by the Electron app). Per main.ts:
/// const BOOTSTRAP_COMPLETE_MARKER = path.join(ACTIVE_HERMES_ROOT, '.hermes-bootstrap-complete')
/// We don't always know ACTIVE_HERMES_ROOT until install.ps1 reports it, so
/// this is a probe helper, not a definitive path.
pub fn likely_bootstrap_marker(install_root: &Path) -> PathBuf {
install_root.join(".hermes-bootstrap-complete")
}
/// Initializes tracing to bootstrap-installer.log under HERMES_HOME/logs/.
/// Returns a guard that flushes the appender on drop — keep it alive for
/// the lifetime of the process.
pub fn init_logging() -> Option<WorkerGuard> {
let dir = log_dir();
if let Err(err) = std::fs::create_dir_all(&dir) {
// No log dir → log to stderr only. Don't panic; the installer
// should still be usable on an exotic filesystem.
eprintln!("[hermes-setup] could not create log dir {dir:?}: {err}");
return None;
}
let file_appender = tracing_appender::rolling::never(&dir, "bootstrap-installer.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let env_filter = tracing_subscriber::EnvFilter::try_from_env("HERMES_BOOTSTRAP_LOG")
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_writer(non_blocking)
.with_ansi(false)
.with_target(true)
.init();
Some(guard)
}
// ---------------------------------------------------------------------------
// Tauri commands
// ---------------------------------------------------------------------------
#[tauri::command]
pub fn get_log_path() -> String {
log_path().to_string_lossy().into_owned()
}
#[tauri::command]
pub fn get_hermes_home() -> String {
hermes_home().to_string_lossy().into_owned()
}
#[tauri::command]
pub fn open_log_dir(app: tauri::AppHandle) -> Result<(), String> {
use tauri_plugin_opener::OpenerExt;
let path = log_dir();
app.opener()
.open_path(path.to_string_lossy(), None::<&str>)
.map_err(|e| e.to_string())
}
|