Skip to content
Merged
2 changes: 1 addition & 1 deletion app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ cef = { version = "=146.4.1", default-features = false }
openhuman_core = { path = "../..", package = "openhuman", default-features = false }

[target.'cfg(unix)'.dependencies]
nix = { version = "0.29", default-features = false, features = ["signal"] }
nix = { version = "0.29", default-features = false, features = ["signal", "user"] }

[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6"
Expand Down
122 changes: 116 additions & 6 deletions app/src-tauri/src/cef_preflight.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
//! CEF cache-lock preflight check (macOS).
//! CEF cache-lock preflight check (macOS and Linux).
//!
//! When another OpenHuman instance is already running, it holds an exclusive
//! lock on the CEF user-data-dir at `~/Library/Caches/com.openhuman.app/cef`.
//! lock on the CEF user-data-dir. On macOS this is
//! `~/Library/Caches/com.openhuman.app/cef`; on Linux it is the path in
//! `OPENHUMAN_CEF_CACHE_PATH` (set by `cef_profile::prepare_process_cache_path`
//! before this module runs), falling back to `$XDG_CACHE_HOME/<id>/cef` or
//! `$HOME/.cache/<id>/cef` when the env var is absent.
//!
//! The vendored `tauri-runtime-cef` crate calls `cef::initialize()` and
//! asserts the result equals `1`; on lock collision it returns `0` and the
//! assertion panics with a Rust backtrace and no actionable message
//! (see issue #864).
//! (Sentry OPENHUMAN-TAURI-K1 on Linux, issue #864 on macOS).
//!
//! This module runs *before* the Tauri builder constructs the runtime.
//! It detects the lock-holder PID via Chromium's `SingletonLock` symlink and
Expand Down Expand Up @@ -72,7 +77,12 @@ impl fmt::Display for CefLockError {

impl std::error::Error for CefLockError {}

/// Resolves the macOS default CEF cache directory and runs the preflight.
/// Resolves the platform default CEF cache directory and runs the preflight.
///
/// Checks `OPENHUMAN_CEF_CACHE_PATH` first (always set by
/// `cef_profile::prepare_process_cache_path` before this runs). Falls back
/// to the platform-specific default: `~/Library/Caches/<id>/cef` on macOS,
/// `$XDG_CACHE_HOME/<id>/cef` or `$HOME/.cache/<id>/cef` on Linux.
pub fn check_default_cache() -> Result<(), CefLockError> {
if let Some(configured) = std::env::var_os("OPENHUMAN_CEF_CACHE_PATH") {
let configured = PathBuf::from(configured);
Expand All @@ -84,10 +94,22 @@ pub fn check_default_cache() -> Result<(), CefLockError> {
}

let home = std::env::var_os("HOME").ok_or(CefLockError::NoHomeDir)?;
let cache_path = PathBuf::from(home)
.join("Library/Caches")
let home = PathBuf::from(home);

#[cfg(target_os = "macos")]
let cache_path = home.join("Library/Caches").join(APP_IDENTIFIER).join("cef");

// On Linux: $XDG_CACHE_HOME/<id>/cef or $HOME/.cache/<id>/cef.
// This matches the fallback path in tauri-runtime-cef's CefRuntime::init
// (via `dirs::cache_dir()`).
#[cfg(target_os = "linux")]
let cache_path = std::env::var_os("XDG_CACHE_HOME")
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.unwrap_or_else(|| home.join(".cache"))
.join(APP_IDENTIFIER)
.join("cef");

log::debug!("[cef-preflight] cache_path={}", cache_path.display());
check_cef_cache_lock(&cache_path)
}
Expand Down Expand Up @@ -201,6 +223,12 @@ mod tests {
use super::*;
use std::os::unix::fs::symlink;

// Shared lock for all tests that mutate process-global env vars.
// Each test previously had its own local `static ENV_LOCK`, allowing
// concurrent test threads to race on OPENHUMAN_CEF_CACHE_PATH /
// XDG_CACHE_HOME. A single module-level lock serialises them.
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

#[test]
fn parse_target_simple() {
assert_eq!(
Expand Down Expand Up @@ -310,4 +338,86 @@ mod tests {
);
let _ = fs::remove_dir_all(&tmp);
}

/// `check_default_cache` must use `OPENHUMAN_CEF_CACHE_PATH` when set —
/// on both macOS and Linux the profile module always sets this before the
/// preflight runs, so the platform-specific fallback paths are irrelevant
/// in production, but the configured-path branch must work on all platforms.
#[test]
fn check_default_cache_uses_configured_env_path() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());

let prior = std::env::var_os("OPENHUMAN_CEF_CACHE_PATH");
let tmp = fresh_tmp("default-cache-env");

std::env::set_var("OPENHUMAN_CEF_CACHE_PATH", &tmp);
let result = check_default_cache();

match prior {
Some(v) => std::env::set_var("OPENHUMAN_CEF_CACHE_PATH", v),
None => std::env::remove_var("OPENHUMAN_CEF_CACHE_PATH"),
}

assert!(result.is_ok(), "expected Ok with no lock, got {result:?}");
let _ = fs::remove_dir_all(&tmp);
}

/// `check_default_cache` with env-path pointing to a dir holding a live lock
/// must return `CefLockError::Held`.
#[test]
fn check_default_cache_env_path_held_returns_err() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());

let prior = std::env::var_os("OPENHUMAN_CEF_CACHE_PATH");
let tmp = fresh_tmp("default-cache-held");
let me = std::process::id() as i32;
symlink(format!("testhost-{me}"), tmp.join("SingletonLock")).unwrap();

std::env::set_var("OPENHUMAN_CEF_CACHE_PATH", &tmp);
let result = check_default_cache();

match prior {
Some(v) => std::env::set_var("OPENHUMAN_CEF_CACHE_PATH", v),
None => std::env::remove_var("OPENHUMAN_CEF_CACHE_PATH"),
}

match result {
Err(CefLockError::Held { pid, .. }) => assert_eq!(pid, me),
other => panic!("expected Held, got {other:?}"),
}
let _ = fs::remove_dir_all(&tmp);
}

/// On Linux, `check_default_cache` without `OPENHUMAN_CEF_CACHE_PATH` set
/// must fall back to `$XDG_CACHE_HOME/<id>/cef` and return Ok when no lock
/// is present.
#[cfg(target_os = "linux")]
#[test]
fn check_default_cache_linux_xdg_fallback_no_lock() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());

let prior_cache = std::env::var_os("OPENHUMAN_CEF_CACHE_PATH");
let prior_xdg = std::env::var_os("XDG_CACHE_HOME");
std::env::remove_var("OPENHUMAN_CEF_CACHE_PATH");

// Redirect XDG_CACHE_HOME to a temp dir we control.
let tmp = fresh_tmp("linux-xdg-fallback");
std::env::set_var("XDG_CACHE_HOME", &tmp);

let result = check_default_cache();

std::env::remove_var("XDG_CACHE_HOME");
match prior_cache {
Some(v) => std::env::set_var("OPENHUMAN_CEF_CACHE_PATH", v),
None => {}
}
match prior_xdg {
Some(v) => std::env::set_var("XDG_CACHE_HOME", v),
None => {}
}

// No SingletonLock under tmp/<id>/cef — should be Ok.
assert!(result.is_ok(), "expected Ok with no lock, got {result:?}");
let _ = fs::remove_dir_all(&tmp);
}
}
118 changes: 116 additions & 2 deletions app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
compile_error!("src-tauri host is desktop-only. Non-desktop targets are not supported.");

mod cdp;
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
mod cef_preflight;
mod cef_profile;
mod core_process;
Expand Down Expand Up @@ -1593,8 +1593,57 @@ fn warn_if_wsl_x11_desktop_launch() {
#[cfg(not(target_os = "linux"))]
fn warn_if_wsl_x11_desktop_launch() {}

/// Returns `true` if a display server is available on Linux.
/// Testable pure function: takes the env-presence booleans directly.
#[cfg(any(target_os = "linux", test))]
fn linux_display_server_present(display: bool, wayland_display: bool) -> bool {
display || wayland_display
}

/// Pre-CEF display-server check for Linux (Sentry OPENHUMAN-TAURI-K1).
///
/// CEF/Chromium requires X11 (`DISPLAY`) or Wayland (`WAYLAND_DISPLAY`) to
/// initialise. Without either, `cef_initialize` returns 0 and the vendored
/// `tauri-runtime-cef` asserts `result == 1` → panic `left: 0, right: 1`.
/// This is fatal and silent on WSL2 without WSLg and on any headless Linux box.
/// Detect it here and exit with a clear message before `CefRuntime::init` runs.
#[cfg(target_os = "linux")]
fn check_linux_display_server() {
if linux_display_server_present(
has_non_empty_env("DISPLAY"),
has_non_empty_env("WAYLAND_DISPLAY"),
) {
log::debug!(
"[cef-preflight] Linux display server present: DISPLAY={:?} WAYLAND_DISPLAY={:?}",
std::env::var("DISPLAY").ok(),
std::env::var("WAYLAND_DISPLAY").ok()
);
return;
}
let msg = "[openhuman] no display server found (DISPLAY and WAYLAND_DISPLAY are both unset).\n\
OpenHuman requires an X11 or Wayland display to run.\n\
On WSL2: install WSLg or configure X11 forwarding from Windows.\n\
Set DISPLAY (e.g. export DISPLAY=:0) or WAYLAND_DISPLAY before launching.";
log::error!(
"[cef-preflight] Linux display server missing — CEF cannot initialize \
(OPENHUMAN-TAURI-K1): DISPLAY and WAYLAND_DISPLAY both unset"
);
eprintln!("\n{msg}\n");
std::process::exit(1);
}

#[cfg(not(target_os = "linux"))]
fn check_linux_display_server() {}

type CefCommandLineArg = (&'static str, Option<&'static str>);

/// Returns `true` when the process is running as root (UID 0) on Linux.
/// Testable pure function; takes the uid directly.
#[cfg(any(target_os = "linux", test))]
fn linux_is_root_uid(uid: u32) -> bool {
uid == 0
}

fn append_platform_cef_gpu_workarounds(args: &mut Vec<CefCommandLineArg>, os: &str, arch: &str) {
// Issue #1697: on Arch/Manjaro-family Linux systems, the AppImage can
// abort during CEF GPU process startup when EGL context creation fails
Expand All @@ -1620,6 +1669,28 @@ fn append_platform_cef_gpu_workarounds(args: &mut Vec<CefCommandLineArg>, os: &s
"[cef-startup] Intel macOS detected: adding --disable-gpu-compositing (issue #1012)"
);
}

// Sentry OPENHUMAN-TAURI-K1: `cef::initialize` returns 0 when running as
// root (uid 0) on Linux unless `--no-sandbox` is passed as a command-line
// argument. The `no_sandbox: 1` field in `cef::Settings` disables the
// sub-process sandbox but does NOT satisfy Chromium's separate root-user
// check in the browser process — that check requires the CLI flag.
//
// This hits CI / coder-bot / Docker environments (e.g.
// `/root/.hermes/profiles/coder-bot/home`) that run as root inside a
// container. Without the flag, `cef_initialize` returns 0 and the vendored
// runtime assertion fires (`left: 0, right: 1`).
#[cfg(target_os = "linux")]
{
let uid = nix::unistd::getuid().as_raw();
if os == "linux" && linux_is_root_uid(uid) {
args.push(("--no-sandbox", None));
log::info!(
"[cef-startup] running as root (uid=0) on Linux: adding --no-sandbox \
(OPENHUMAN-TAURI-K1)"
);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

pub fn run() {
Expand Down Expand Up @@ -1812,6 +1883,9 @@ pub fn run() {
}

warn_if_wsl_x11_desktop_launch();
// Exit before CEF if no display server is available — prevents the
// `assert_eq!(cef_initialize(…), 1)` panic (OPENHUMAN-TAURI-K1).
check_linux_display_server();

// The vendored tauri-cef dev-server proxy builds a reqwest 0.13 client
// (see vendor/tauri-cef/crates/tauri/src/protocol/tauri.rs) which calls
Expand Down Expand Up @@ -1899,7 +1973,12 @@ pub fn run() {
#[cfg(target_os = "macos")]
process_recovery::reap_stale_openhuman_processes();

#[cfg(target_os = "macos")]
// CEF cache-lock preflight: if another OpenHuman instance holds the CEF
// user-data-dir SingletonLock, `cef_initialize` returns 0 and the vendored
// runtime panics (`left: 0, right: 1`). Catch the collision here and exit
// cleanly. Stale locks (PID dead) are removed so crashed processes don't
// block subsequent launches. macOS: issue #864. Linux: OPENHUMAN-TAURI-K1.
#[cfg(any(target_os = "macos", target_os = "linux"))]
if let Err(e) = cef_preflight::check_default_cache() {
eprintln!("\n[openhuman] {e}\n");
std::process::exit(1);
Expand Down Expand Up @@ -3204,6 +3283,41 @@ mod tests {
assert!(!should_warn_for_wsl_x11_desktop(true, true, false, true));
}

// -------------------------------------------------------------------------
// Linux display-server pre-flight (Sentry OPENHUMAN-TAURI-K1)
// -------------------------------------------------------------------------

#[test]
fn linux_display_present_with_x11() {
assert!(linux_display_server_present(true, false));
}

#[test]
fn linux_display_present_with_wayland() {
assert!(linux_display_server_present(false, true));
}

#[test]
fn linux_display_present_with_both() {
assert!(linux_display_server_present(true, true));
}

#[test]
fn linux_display_absent_without_either() {
assert!(!linux_display_server_present(false, false));
}

#[test]
fn linux_root_uid_detected() {
assert!(linux_is_root_uid(0));
}

#[test]
fn linux_non_root_uid_not_detected() {
assert!(!linux_is_root_uid(1000));
assert!(!linux_is_root_uid(1));
}

// -------------------------------------------------------------------------
// Platform constants (issue #1012 Sentry tagging)
// -------------------------------------------------------------------------
Expand Down
Loading