mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(tauri): resolve Linux CEF init panic — root/container + SingletonLock + display-server guards (OPENHUMAN-TAURI-K1) (#2103)
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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!(
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+116
-2
@@ -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;
|
||||
@@ -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
|
||||
@@ -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)"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -3214,6 +3293,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)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user