diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index 12df3efc1..9be427646 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -775,6 +775,26 @@ fn is_daemon_mode() -> bool { std::env::args().any(|arg| arg == "daemon" || arg == "--daemon") } +/// Returns true when an executable named `name` is discoverable on `$PATH`. +/// +/// Inline `which`-style lookup so the deep-link pre-flight on Linux can +/// skip `tauri-plugin-deep-link::register_all` cleanly when `xdg-mime` is +/// missing (OPENHUMAN-TAURI-AS). Walks `$PATH` entries, joins `name`, and +/// returns true on the first hit that is a regular file. The metadata check +/// is `is_file()` rather than an executable-bit check: on Linux any file in +/// `$PATH` that is named like the binary is enough to gate the plugin call +/// (the plugin itself will surface the real exec error to its own warn), +/// and an executable-bit check would require unix-specific +/// `MetadataExt::mode` plumbing that isn't worth the platform branch for a +/// single discoverability gate. +#[cfg(target_os = "linux")] +fn path_has_executable(name: &str) -> bool { + let Some(path_var) = std::env::var_os("PATH") else { + return false; + }; + std::env::split_paths(&path_var).any(|dir| dir.join(name).is_file()) +} + /// Tauri command: bring the main window to front from any webview (e.g. overlay orb click). #[tauri::command] fn activate_main_window(app: AppHandle) -> Result<(), String> { @@ -1723,12 +1743,40 @@ pub fn run() { let builder = builder.manage(meet_video::frame_bus::MeetVideoFrameBusState::new()); builder .setup(move |app| { - #[cfg(any(windows, target_os = "linux"))] + #[cfg(windows)] { if let Err(err) = app.deep_link().register_all() { log::warn!("[deep-link] register_all failed (non-fatal): {err}"); } } + #[cfg(target_os = "linux")] + { + // `tauri-plugin-deep-link::register_all` on Linux shells out + // to `xdg-mime` (and `update-desktop-database` / `xdg-icon-resource`) + // to install MIME-type associations for our custom URL + // schemes. On Linux installs that ship without xdg-utils — + // WSL2 without a desktop env, headless servers, minimal + // containers (OPENHUMAN-TAURI-AS: WSL2 user in BR) — the + // tool isn't on PATH and the plugin fires + // `log::error!("Failed to run OS command \`xdg-mime\`…")` + // *internally* before returning the Err. That internal + // error log is scooped up by `sentry-tracing` into a Sentry + // event even though our `if let Err` arm below already + // demotes the user-visible failure to a warn. Skip the + // plugin call entirely when xdg-mime isn't available so + // the internal log never fires — registration only matters + // on systems with a desktop environment, where xdg-utils + // is part of the desktop install anyway. + if path_has_executable("xdg-mime") { + if let Err(err) = app.deep_link().register_all() { + log::warn!("[deep-link] register_all failed (non-fatal): {err}"); + } + } else { + log::warn!( + "[deep-link] skipping register_all — xdg-mime not on PATH (xdg-utils not installed; deep-link MIME registration unavailable on this host)" + ); + } + } // Start the webview_apis WebSocket bridge BEFORE spawning core — // core reads OPENHUMAN_WEBVIEW_APIS_PORT on first connect, and @@ -3048,4 +3096,76 @@ mod tests { "messages that merely contain the dev-proxy prefix must NOT be filtered" ); } + + // ------------------------------------------------------------------------- + // path_has_executable / deep-link xdg-mime pre-flight (OPENHUMAN-TAURI-AS) + // ------------------------------------------------------------------------- + + /// With a controlled `$PATH` containing one dir that holds a file named + /// `xdg-mime`, the lookup must succeed (mirrors a Linux desktop install + /// where xdg-utils ships the binary). + #[cfg(target_os = "linux")] + #[test] + fn path_has_executable_finds_file_on_path() { + let _g = ENV_LOCK.lock().unwrap(); + let original = std::env::var_os("PATH"); + + let dir = tempfile::tempdir().expect("tempdir"); + std::fs::write(dir.path().join("xdg-mime"), b"#!/bin/sh\n").expect("write stub"); + std::env::set_var("PATH", dir.path()); + + assert!( + path_has_executable("xdg-mime"), + "must discover xdg-mime when present in a $PATH entry" + ); + + match original { + Some(v) => std::env::set_var("PATH", v), + None => std::env::remove_var("PATH"), + } + } + + /// With a controlled `$PATH` that does NOT contain `xdg-mime`, the lookup + /// must fail (mirrors WSL2 / minimal containers without xdg-utils — the + /// case OPENHUMAN-TAURI-AS protects against). + #[cfg(target_os = "linux")] + #[test] + fn path_has_executable_returns_false_when_missing() { + let _g = ENV_LOCK.lock().unwrap(); + let original = std::env::var_os("PATH"); + + let dir = tempfile::tempdir().expect("tempdir"); + // Intentionally do not create xdg-mime in `dir`. + std::env::set_var("PATH", dir.path()); + + assert!( + !path_has_executable("xdg-mime"), + "must return false when xdg-mime is not in any $PATH entry" + ); + + match original { + Some(v) => std::env::set_var("PATH", v), + None => std::env::remove_var("PATH"), + } + } + + /// When `$PATH` is unset entirely, the lookup must short-circuit to false + /// rather than panic or fall back to the cwd. + #[cfg(target_os = "linux")] + #[test] + fn path_has_executable_returns_false_when_path_unset() { + let _g = ENV_LOCK.lock().unwrap(); + let original = std::env::var_os("PATH"); + + std::env::remove_var("PATH"); + assert!( + !path_has_executable("xdg-mime"), + "unset $PATH must yield false (skip register_all on the missing-xdg-utils branch)" + ); + + match original { + Some(v) => std::env::set_var("PATH", v), + None => std::env::remove_var("PATH"), + } + } }