#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] compile_error!("src-tauri host is desktop-only. Non-desktop targets are not supported."); mod cdp; #[cfg(target_os = "macos")] mod cef_preflight; mod cef_profile; mod core_process; mod core_rpc; mod dictation_hotkeys; mod discord_scanner; mod fake_camera; mod file_logging; mod gmessages_scanner; mod imessage_scanner; #[cfg(target_os = "macos")] mod mascot_native_window; mod meet_audio; mod meet_call; mod meet_scanner; mod meet_video; mod native_notifications; mod notification_settings; mod process_kill; mod process_recovery; mod screen_capture; mod slack_scanner; mod telegram_scanner; mod webview_accounts; mod webview_apis; mod whatsapp_scanner; mod window_state; #[cfg(target_os = "macos")] use tauri::menu::{PredefinedMenuItem, Submenu}; #[cfg(any(target_os = "macos", target_os = "windows"))] use tauri::WindowEvent; #[cfg(not(target_os = "linux"))] use tauri::{ menu::{Menu, MenuItem}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, }; use tauri::{AppHandle, Emitter, Manager, PhysicalPosition, RunEvent, WebviewWindow}; use tauri_plugin_global_shortcut::{GlobalShortcutExt, ShortcutState}; #[cfg(any(windows, target_os = "linux"))] use tauri_plugin_deep_link::DeepLinkExt; #[cfg(target_os = "macos")] use objc2::runtime::{AnyClass, AnyObject}; #[cfg(target_os = "macos")] use objc2::ClassType; #[cfg(target_os = "macos")] use objc2_app_kit::{NSPanel, NSWindowCollectionBehavior, NSWindowStyleMask}; // CEF is the only runtime; alias kept so command handlers thread the runtime generic uniformly. pub(crate) type AppRuntime = tauri::Cef; static EARLY_TEARDOWN_RAN: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); #[cfg(target_os = "macos")] const APP_QUIT_MENU_ID: &str = "app_quit"; #[tauri::command] fn core_rpc_url() -> String { crate::core_rpc::core_rpc_url_value() } /// Tauri command: return the per-process bearer token that must be sent with /// every core RPC request as `Authorization: Bearer `. /// /// The token is generated by the Tauri shell at startup (inside /// [`CoreProcessHandle::new`]), injected into the core child process via /// `OPENHUMAN_CORE_TOKEN`, and stored in the handle — available immediately /// with no file I/O or timing issues. #[tauri::command] fn core_rpc_token(state: tauri::State<'_, core_process::CoreProcessHandle>) -> String { log::debug!("[auth] core_rpc_token: returning token to frontend"); state.inner().rpc_token().to_string() } #[tauri::command] fn overlay_parent_rpc_url() -> Option { let url = std::env::var("OPENHUMAN_CORE_RPC_URL").ok()?; let trimmed = url.trim(); if trimmed.is_empty() { return None; } Some(trimmed.to_string()) } #[tauri::command] fn process_diagnostics_list_owned() -> Result, String> { match process_recovery::enumerate_openhuman_processes() { Ok(processes) => { log::info!( "[startup-recovery] diagnostics listed {} owned OpenHuman processes", processes.len() ); Ok(processes) } Err(err) => { log::warn!("[startup-recovery] diagnostics process enumeration failed: {err}"); Err(err) } } } #[allow(dead_code)] // Overlay disabled in tauri.conf.json; helper kept for future re-enable. fn pin_overlay_bottom_right(window: &WebviewWindow) { let Ok(Some(monitor)) = window.current_monitor() else { log::warn!("[overlay] could not resolve current monitor for positioning"); return; }; let Ok(size) = window.outer_size() else { log::warn!("[overlay] could not resolve overlay size for positioning"); return; }; let margin = 20i32; let x = monitor.position().x + monitor.size().width as i32 - size.width as i32 - margin; let y = monitor.position().y + monitor.size().height as i32 - size.height as i32 - margin; if let Err(err) = window.set_position(PhysicalPosition::new(x, y)) { log::warn!("[overlay] failed to pin overlay bottom-right: {err}"); } else { log::info!("[overlay] pinned overlay bottom-right at {},{}", x, y); } } #[cfg(target_os = "macos")] #[allow(dead_code)] // Overlay disabled in tauri.conf.json; helper kept for future re-enable. fn configure_overlay_window_macos(window: &WebviewWindow) { // Standard NSWindow cannot float above fullscreen apps on macOS because // fullscreen apps run in a separate Space. Only NSPanel can do this. // // Tauri/tao hardcodes NSWindow as the window class, so we use // object_setClass() to reclass the existing NSWindow into an NSPanel // at runtime. This avoids creating a new window (which crashes because // Tao's window delegate is tightly coupled to the original NSWindow). // // After reclassing, we set the NonactivatingPanel style mask and // Transient collection behavior — matching the working Swift overlay // helper (accessibility/helper.rs OverlayController) which is confirmed // to float above fullscreen apps on macOS Sonoma. // // Previous attempts that FAILED: // 1. CGShieldingWindowLevel + CanJoinAllSpaces + FullScreenAuxiliary → hidden // 2. Window level i32::MAX-17 + Stationary → hidden // 3. CGS private API CGSSetWindowTags sticky bit → hidden // 4. object_setClass WITHOUT NonactivatingPanel style mask → hidden // 5. Create new NSPanel + reparent webview → CRASH (Tao delegate panic) // // See: https://github.com/tauri-apps/tauri/issues/11488 match window.ns_window() { Ok(ns_window_raw) => unsafe { let ns_window = ns_window_raw as *mut AnyObject; // ── Reclass NSWindow → NSPanel ────────────────────────── let panel_class: *const AnyClass = NSPanel::class(); objc2::ffi::object_setClass(ns_window, panel_class); log::info!("[overlay] reclassed NSWindow → NSPanel via object_setClass"); // Cast to NSPanel for method calls let panel: &NSPanel = &*(ns_window as *const NSPanel); // ── Style mask: add NonactivatingPanel ────────────────── // This is the KEY piece the Swift helper uses. Without it, // the panel doesn't behave as a proper non-activating panel // and won't float above fullscreen Spaces. let current_style = panel.styleMask(); panel.setStyleMask(current_style | NSWindowStyleMask::NonactivatingPanel); // ── Collection behavior ───────────────────────────────── // The Swift helper uses .canJoinAllSpaces + .transient // (NOT .stationary or .fullScreenAuxiliary alone). // Transient means the panel follows the active Space and // appears above fullscreen apps. panel.setCollectionBehavior( NSWindowCollectionBehavior::CanJoinAllSpaces | NSWindowCollectionBehavior::Transient | NSWindowCollectionBehavior::FullScreenAuxiliary | NSWindowCollectionBehavior::IgnoresCycle, ); // ── Window level: status bar tier ─────────────────────── // NSStatusWindowLevel = 25. The Swift helper uses .statusBar // which is the same value. panel.setLevel(25); // ── Panel-specific properties ─────────────────────────── panel.setFloatingPanel(true); panel.setHidesOnDeactivate(false); panel.setBecomesKeyOnlyIfNeeded(true); panel.setWorksWhenModal(true); // Make sure it's ordered front panel.orderFrontRegardless(); log::info!( "[overlay] NSPanel configured — level=25, \ NonactivatingPanel+canJoinAllSpaces+transient, \ floatingPanel={}, hidesOnDeactivate={}", panel.isFloatingPanel(), panel.hidesOnDeactivate(), ); }, Err(err) => { log::warn!("[overlay] failed to access native NSWindow handle: {err}"); } } } /// Core update is handled by the Tauri shell auto-updater (`tauri-plugin-updater`) /// since the core ships in-process with the app. This command is kept as a /// no-op stub so the frontend's `checkCoreUpdate` keeps working without errors; /// it always reports the running version as up-to-date. #[tauri::command] async fn check_core_update( _state: tauri::State<'_, core_process::CoreProcessHandle>, ) -> Result { let version = env!("CARGO_PKG_VERSION"); Ok(serde_json::json!({ "running_version": version, "minimum_version": version, "outdated": false, "latest_version": version, "update_available": false, })) } /// Stub kept for frontend compatibility — use `apply_app_update` instead. #[tauri::command] async fn apply_core_update( _state: tauri::State<'_, core_process::CoreProcessHandle>, _app: tauri::AppHandle, ) -> Result<(), String> { Err("core ships in-process; use the Tauri shell updater (apply_app_update) instead".into()) } #[tauri::command] async fn restart_core_process( state: tauri::State<'_, core_process::CoreProcessHandle>, ) -> Result<(), String> { log::info!("[core] restart_core_process: command invoked from frontend"); let _guard = state.inner().restart_lock().await; log::debug!("[core] restart_core_process: acquired restart lock"); state.inner().restart().await } /// Start the embedded core process on demand. /// /// Called by the BootCheckGate (Local mode) before the version check. The /// core no longer auto-spawns at Tauri setup — the UI is responsible for /// driving the lifecycle so it can surface startup failures and version /// mismatches to the user. /// /// Idempotent: `ensure_running` is a no-op if the core is already up. #[tauri::command] async fn start_core_process( state: tauri::State<'_, core_process::CoreProcessHandle>, ) -> Result<(), String> { log::info!("[core] start_core_process: command invoked from frontend"); state.inner().ensure_running().await } /// Cleanly exit the application. /// /// Called by the BootCheckGate "Quit" button when the core is unreachable and /// the user chooses to close the app rather than switch modes. #[tauri::command] async fn app_quit(app: tauri::AppHandle) -> Result<(), String> { log::info!("[app] app_quit: quit requested from frontend"); app.exit(0); Ok(()) } #[tauri::command] async fn restart_app(app: tauri::AppHandle) -> Result<(), String> { log::info!("[app] restart_app invoked from frontend"); // Persist main-window geometry and hide the window before exit so // the macOS WindowServer doesn't briefly black-out the desktop layer // on the (now defunct) display when the focused app dies, and so // the new process can land its window on the same display+position // the user had it on. (#900 secondary fixes) if let Some(window) = app.get_webview_window("main") { window_state::save_main(&window); if let Err(err) = window.hide() { log::warn!("[app] hide main window before restart failed: {err}"); } } log::info!("[app] restart_app — starting early teardown before restart"); perform_early_teardown_async(&app).await; log::info!("[app] restart_app — early teardown complete, restarting"); app.restart(); // restart() does not return, but we must satisfy the signature Ok(()) } /// Read the authoritative active user id from `active_user.toml` so the /// frontend can seed `userScopedStorage` BEFORE redux-persist hydrates. /// /// The previous frontend-only seed (a `localStorage` key) was bound to the /// per-user CEF profile dir, so on every restart-driven user flip the new /// process read whatever value the new profile's `localStorage` happened to /// hold from a prior session — usually stale, triggering a false re-flip and /// a restart loop. The Rust core writes `active_user.toml` atomically as part /// of `auth_store_session`, so it's the only profile-independent source of /// truth available to the UI at boot. Reuses /// `cef_profile::default_root_openhuman_dir()` so the lookup honors /// `OPENHUMAN_WORKSPACE` overrides used in test harnesses. (#900) #[tauri::command] fn get_active_user_id() -> Result, String> { let dir = cef_profile::default_root_openhuman_dir()?; Ok(cef_profile::read_active_user_id(&dir)) } #[tauri::command] async fn schedule_cef_profile_purge(user_id: Option) -> Result { let queued = cef_profile::queue_profile_purge_for_user(user_id.as_deref())?; Ok(queued.display().to_string()) } /// Information about an available shell-app update returned to the frontend. #[derive(Debug, Clone, serde::Serialize)] struct AppUpdateInfo { /// The currently-running app version (matches `tauri.conf.json::version`). current_version: String, /// True when the configured updater endpoint advertises a newer version. available: bool, /// Newer version reported by the updater endpoint, if any. available_version: Option, /// Release notes / body for the new version, if the manifest provided one. body: Option, } fn no_app_update_available(current_version: String) -> AppUpdateInfo { AppUpdateInfo { current_version, available: false, available_version: None, body: None, } } /// Probe the updater endpoint and report whether a newer shell build is available. /// Does NOT download or install. Pair with `apply_app_update` to actually upgrade. #[tauri::command] async fn check_app_update(app: tauri::AppHandle) -> Result { use tauri_plugin_updater::UpdaterExt; let current_version = app.package_info().version.to_string(); log::info!("[app-update] check requested (current: {current_version})"); let updater = app .updater() .map_err(|e| format!("updater plugin not initialized: {e}"))?; match updater.check().await { Ok(Some(update)) => { log::info!( "[app-update] update available: {} -> {}", current_version, update.version ); Ok(AppUpdateInfo { current_version, available: true, available_version: Some(update.version.clone()), body: update.body.clone(), }) } Ok(None) => { log::info!("[app-update] no update available"); Ok(no_app_update_available(current_version)) } Err(e) => { log::warn!( "[app-update] check failed; treating as no update available for this probe: {e}" ); Ok(no_app_update_available(current_version)) } } } /// Download and install the latest shell update, then relaunch. /// /// Shuts the core sidecar down before download begins so the install step /// (which on macOS replaces the entire `.app` bundle) does not race against /// a live sidecar holding file handles inside `Contents/Resources/`. The /// new bundled sidecar is launched fresh after `app.restart()`. /// /// Emits Tauri events `app-update:status` and `app-update:progress` so the /// frontend can show a snackbar / progress bar. #[tauri::command] async fn apply_app_update( state: tauri::State<'_, core_process::CoreProcessHandle>, app: tauri::AppHandle, ) -> Result<(), String> { use tauri::Emitter; use tauri_plugin_updater::UpdaterExt; log::info!("[app-update] manual apply_app_update invoked from frontend"); let updater = app .updater() .map_err(|e| format!("updater plugin not initialized: {e}"))?; let _ = app.emit("app-update:status", "checking"); let update = match updater.check().await { Ok(Some(u)) => u, Ok(None) => { log::info!("[app-update] no update available"); let _ = app.emit("app-update:status", "up_to_date"); return Ok(()); } Err(e) => { log::warn!("[app-update] check failed: {e}"); let _ = app.emit("app-update:status", "error"); return Err(format!("update check failed: {e}")); } }; let new_version = update.version.clone(); log::info!( "[app-update] downloading {} (size hint: {:?})", new_version, update.signature ); let _ = app.emit("app-update:status", "downloading"); // Shut the core sidecar down before the install step replaces the .app. // We hold the restart lock until app.restart() so nothing tries to // respawn the sidecar from the in-flight (or freshly-replaced) bundle. let _guard = state.inner().restart_lock().await; log::debug!("[app-update] acquired core restart lock"); state.inner().shutdown().await; let progress_app = app.clone(); let install_app = app.clone(); let download_result = update .download_and_install( move |chunk_length, content_length| { let payload = serde_json::json!({ "chunk": chunk_length, "total": content_length, }); let _ = progress_app.emit("app-update:progress", payload); }, move || { log::info!("[app-update] download complete — installing"); let _ = install_app.emit("app-update:status", "installing"); }, ) .await; if let Err(e) = download_result { log::error!("[app-update] download/install failed: {e}"); // Same recovery as `install_app_update`: the .app wasn't swapped, // so revive the in-process core we shut down above. if let Err(start_err) = state.inner().ensure_running().await { log::error!("[app-update] failed to restart core after apply error: {start_err}"); } let _ = app.emit("app-update:status", "error"); return Err(format!("download_and_install failed: {e}")); } log::info!("[app-update] install complete — relaunching"); let _ = app.emit("app-update:status", "restarting"); log::info!("[app-update] starting early teardown before restart"); perform_early_teardown_async(&app).await; // Note: app.restart() never returns. Anything after this is unreachable. app.restart(); } /// Holds an `Update` handle plus its downloaded bytes between the /// `download_app_update` (background) and `install_app_update` (user /// confirmed restart) commands. Sized at ~100MB on macOS for the .app /// bundle, which is fine to keep in RAM until the user is ready. struct PendingAppUpdate { update: tauri_plugin_updater::Update, bytes: Vec, version: String, } /// Tauri-managed state slot for the in-flight pending update. `None` means /// "no update has been downloaded since launch"; `Some(_)` means the bytes /// are ready and `install_app_update` can finalize without re-downloading. #[derive(Default)] struct PendingAppUpdateState(tokio::sync::Mutex>); /// Result returned to the frontend after a download attempt. #[derive(Debug, Clone, serde::Serialize)] struct AppUpdateDownloadResult { /// True when an update was found and the bytes are now staged. ready: bool, /// Version of the staged update (if any). version: Option, /// Release notes for the staged update. body: Option, } /// Probe the updater endpoint and, if a newer build is advertised, download /// the bundle bytes into memory but do NOT install. The frontend can then /// surface a "Restart to apply" prompt at a moment that's safe for the user /// (no in-flight conversation, etc.) before calling `install_app_update`. /// /// Emits the same `app-update:status` and `app-update:progress` events as /// `apply_app_update`, so the React state machine can drive a single UI off /// either path. Status sequence: `checking` → `downloading` → `ready_to_install`, /// or `up_to_date` / `error`. #[tauri::command] async fn download_app_update( app: tauri::AppHandle, state: tauri::State<'_, PendingAppUpdateState>, ) -> Result { use tauri::Emitter; use tauri_plugin_updater::UpdaterExt; log::info!("[app-update] download_app_update invoked from frontend"); let updater = app .updater() .map_err(|e| format!("updater plugin not initialized: {e}"))?; let _ = app.emit("app-update:status", "checking"); let update = match updater.check().await { Ok(Some(u)) => u, Ok(None) => { log::info!("[app-update] no update available"); let _ = app.emit("app-update:status", "up_to_date"); return Ok(AppUpdateDownloadResult { ready: false, version: None, body: None, }); } Err(e) => { log::warn!("[app-update] check failed: {e}"); let _ = app.emit("app-update:status", "error"); return Err(format!("update check failed: {e}")); } }; let new_version = update.version.clone(); let body = update.body.clone(); log::info!("[app-update] downloading {} (background)", new_version); let _ = app.emit("app-update:status", "downloading"); let progress_app = app.clone(); let download_result = update .download( move |chunk_length, content_length| { let payload = serde_json::json!({ "chunk": chunk_length, "total": content_length, }); let _ = progress_app.emit("app-update:progress", payload); }, || { log::info!("[app-update] download complete — staging for install"); }, ) .await; let bytes = match download_result { Ok(b) => b, Err(e) => { log::error!("[app-update] download failed: {e}"); let _ = app.emit("app-update:status", "error"); return Err(format!("download failed: {e}")); } }; let mut slot = state.0.lock().await; *slot = Some(PendingAppUpdate { update, bytes, version: new_version.clone(), }); drop(slot); log::info!( "[app-update] staged {} — awaiting user-initiated install", new_version ); let _ = app.emit("app-update:status", "ready_to_install"); Ok(AppUpdateDownloadResult { ready: true, version: Some(new_version), body, }) } /// Install the previously-downloaded update bytes (staged by /// `download_app_update`), then relaunch. Errors with `no pending update` /// if `download_app_update` hasn't run yet — the frontend should fall back /// to a fresh `apply_app_update` in that case. /// /// Acquires the core restart lock + shuts the in-process core server down /// before install, same as `apply_app_update`, so the macOS .app bundle /// replacement does not race against a live core holding file handles. #[tauri::command] async fn install_app_update( core_state: tauri::State<'_, core_process::CoreProcessHandle>, pending: tauri::State<'_, PendingAppUpdateState>, app: tauri::AppHandle, ) -> Result<(), String> { use tauri::Emitter; log::info!("[app-update] install_app_update invoked from frontend"); let staged = { let mut slot = pending.0.lock().await; slot.take() }; let staged = match staged { Some(s) => s, None => { log::warn!("[app-update] install requested but no staged update"); let _ = app.emit("app-update:status", "error"); return Err("no pending update — call download_app_update first".into()); } }; log::info!("[app-update] installing staged version {}", staged.version); let _guard = core_state.inner().restart_lock().await; log::debug!("[app-update] acquired core restart lock"); core_state.inner().shutdown().await; let _ = app.emit("app-update:status", "installing"); if let Err(e) = staged.update.install(staged.bytes) { log::error!("[app-update] install failed: {e}"); // The .app on disk wasn't replaced, so we keep running the // pre-install build — bring the core back up before returning // so the user can keep working instead of being silently offline. if let Err(start_err) = core_state.inner().ensure_running().await { log::error!("[app-update] failed to restart core after install error: {start_err}"); } let _ = app.emit("app-update:status", "error"); return Err(format!("install failed: {e}")); } log::info!("[app-update] install complete — relaunching"); let _ = app.emit("app-update:status", "restarting"); log::info!("[app-update] starting early teardown before restart"); perform_early_teardown_async(&app).await; // Note: app.restart() never returns. Anything after this is unreachable. app.restart(); } /// Register (or re-register) the global dictation toggle hotkey. /// Emits `dictation://toggle` to all webviews when the shortcut is pressed. #[tauri::command] async fn register_dictation_hotkey( app: AppHandle, shortcut: String, ) -> Result<(), String> { log::info!("[dictation] register_dictation_hotkey: shortcut={shortcut}"); let old_shortcuts = { let state = app.state::(); let guard = state.0.lock().unwrap(); guard.clone() }; let expanded_shortcuts = dictation_hotkeys::expand_dictation_shortcuts(&shortcut); if expanded_shortcuts.is_empty() { return Err("Shortcut cannot be empty".to_string()); } log::info!( "[dictation] expanded shortcuts: {}", expanded_shortcuts.join(", ") ); let register_shortcut = |shortcut_variant: &str| -> Result<(), String> { let app_clone = app.clone(); app.global_shortcut() .on_shortcut(shortcut_variant, move |_app, _sc, event| { if event.state == ShortcutState::Pressed { log::debug!("[dictation] hotkey pressed — emitting dictation://toggle"); if let Err(e) = app_clone.emit("dictation://toggle", ()) { log::warn!("[dictation] emit failed: {e}"); } } }) .map_err(|e| format!("Failed to register shortcut '{shortcut_variant}': {e}")) }; let mut unregistered_old: Vec = Vec::new(); for old in &old_shortcuts { log::debug!("[dictation] unregistering previous shortcut: {old}"); if let Err(e) = app.global_shortcut().unregister(old.as_str()) { for restored in &unregistered_old { if let Err(restore_err) = register_shortcut(restored.as_str()) { log::warn!( "[dictation] rollback failed while restoring old shortcut '{restored}': {restore_err}" ); } } return Err(format!( "Failed to unregister previous shortcut '{old}': {e}" )); } unregistered_old.push(old.clone()); } let mut newly_registered: Vec = Vec::new(); for shortcut_variant in &expanded_shortcuts { if let Err(err) = register_shortcut(shortcut_variant.as_str()) { log::error!("[dictation] failed to register shortcut '{shortcut_variant}': {err}"); for registered in &newly_registered { if let Err(unregister_err) = app.global_shortcut().unregister(registered.as_str()) { log::warn!( "[dictation] rollback failed while unregistering '{registered}': {unregister_err}" ); } } for old in &old_shortcuts { if let Err(restore_err) = register_shortcut(old.as_str()) { log::warn!( "[dictation] rollback failed while restoring old shortcut '{old}': {restore_err}" ); } } return Err(err); } newly_registered.push(shortcut_variant.clone()); } // Persist all newly registered shortcuts. { let state = app.state::(); let mut guard = state.0.lock().unwrap(); *guard = expanded_shortcuts.clone(); } log::info!( "[dictation] shortcuts registered: {}", expanded_shortcuts.join(", ") ); Ok(()) } /// Unregister the global dictation hotkey (if any). #[tauri::command] async fn unregister_dictation_hotkey(app: AppHandle) -> Result<(), String> { log::info!("[dictation] unregister_dictation_hotkey: called"); let state = app.state::(); let mut guard = state.0.lock().unwrap(); if guard.is_empty() { log::debug!("[dictation] no shortcut registered — nothing to unregister"); } else { let old_shortcuts = guard.clone(); guard.clear(); for old in old_shortcuts { log::debug!("[dictation] unregistering shortcut: {old}"); app.global_shortcut() .unregister(old.as_str()) .map_err(|e| { log::warn!("[dictation] failed to unregister '{old}': {e}"); format!("Failed to unregister shortcut '{old}': {e}") })?; log::info!("[dictation] shortcut unregistered: {old}"); } } Ok(()) } 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> { log::debug!("[window] activate_main_window called from overlay"); show_main_window(&app) } /// Show the floating mascot. macOS: native NSPanel + WKWebView (so the /// window is actually transparent — vendored tauri-cef can't render /// transparent windowed-mode browsers). Loads the Vite dev URL in /// development and the bundled `index.html` in production. Other OSes: /// not yet wired up. #[tauri::command] fn mascot_window_show(app: AppHandle) -> Result<(), String> { log::info!("[mascot-window] show requested"); #[cfg(target_os = "macos")] { return mascot_native_window::show(&app); } #[cfg(not(target_os = "macos"))] { let _ = app; Err("floating mascot window is macOS-only for now".into()) } } /// Hide the floating mascot. #[tauri::command] fn mascot_window_hide(app: AppHandle) -> Result<(), String> { log::info!("[mascot-window] hide requested"); #[cfg(target_os = "macos")] { let _ = app; mascot_native_window::hide(); Ok(()) } #[cfg(not(target_os = "macos"))] { let _ = app; Ok(()) } } #[cfg(target_os = "macos")] fn mascot_native_window_is_open() -> bool { mascot_native_window::is_open() } #[cfg(not(target_os = "macos"))] fn mascot_native_window_is_open() -> bool { false } /// Hide or show the OS top-level main-window frame on Windows by enumerating /// this process's top-level windows and matching the visible /// `Chrome_WidgetWin_1` host. `WebviewWindow::hwnd()` from the vendored CEF /// runtime returns a `cef::Window` internal handle that `ShowWindow` rejects, /// so we walk the OS window list instead (#1607). Empirically there is one /// matching top-level frame; the single-instance lock window uses class /// `com.openhuman.app-sic` and is excluded. /// /// `SW_HIDE` removes the frame from screen AND taskbar — full hide-to-tray as /// PR #1548 intended. On restore, the IsWindowVisible filter excludes hidden /// windows, so we also accept currently-hidden Chrome_WidgetWin_1 frames when /// `hide=false`. #[cfg(target_os = "windows")] fn set_main_window_hidden(hide: bool) { use std::ffi::OsString; use std::os::windows::ffi::OsStringExt; use windows_sys::Win32::Foundation::{BOOL, HWND, LPARAM}; use windows_sys::Win32::UI::WindowsAndMessaging::{ EnumWindows, GetClassNameW, GetWindowThreadProcessId, IsWindowVisible, ShowWindow, SW_HIDE, SW_SHOW, }; struct EnumCtx { target_pid: u32, action: i32, require_visible: bool, matched: u32, } unsafe extern "system" fn enum_proc(hwnd: HWND, lparam: LPARAM) -> BOOL { let ctx = unsafe { &mut *(lparam as *mut EnumCtx) }; let mut pid: u32 = 0; unsafe { GetWindowThreadProcessId(hwnd, &mut pid) }; if pid != ctx.target_pid { return 1; } if ctx.require_visible && unsafe { IsWindowVisible(hwnd) } == 0 { return 1; } let mut class_buf = [0u16; 64]; let n = unsafe { GetClassNameW(hwnd, class_buf.as_mut_ptr(), class_buf.len() as i32) }; if n <= 0 { return 1; } let class = OsString::from_wide(&class_buf[..n as usize]) .to_string_lossy() .into_owned(); if class != "Chrome_WidgetWin_1" { return 1; } unsafe { ShowWindow(hwnd, ctx.action) }; ctx.matched += 1; 1 } let action = if hide { SW_HIDE } else { SW_SHOW }; let mut ctx = EnumCtx { target_pid: std::process::id(), action, // Hide path: only touch currently-visible frames. Show path: also // pick up frames already in the SW_HIDE state. require_visible: hide, matched: 0, }; unsafe { EnumWindows(Some(enum_proc), &mut ctx as *mut _ as LPARAM) }; log::info!( "[window-hide] EnumWindows: action={} matched={} pid={}", if hide { "SW_HIDE" } else { "SW_SHOW" }, ctx.matched, ctx.target_pid, ); } fn show_main_window(app: &AppHandle) -> Result<(), String> { // On Windows: surface the OS top-level Chrome_WidgetWin_1 frame BEFORE // any Tauri lookups. After our close handler's SW_HIDE the runtime // briefly drops the `WebviewWindow` record for "main" (CEF treats the // hidden host as gone), so `get_webview_window("main")` returns None // and the early `?` below would abort before SW_SHOW fires (#1607). // EnumWindows + SW_SHOW operates directly on the OS HWND that // survived independently, and the runtime re-tracks the window once // it's visible again. #[cfg(target_os = "windows")] { set_main_window_hidden(false); if let Some(webview) = app.get_webview("main") { let _ = webview.show(); let _ = webview.set_focus(); } } let window = app .get_webview_window("main") .ok_or_else(|| "main window not found".to_string())?; window .show() .map_err(|err| format!("failed to show main window: {err}"))?; window .unminimize() .map_err(|err| format!("failed to unminimize main window: {err}"))?; window .set_focus() .map_err(|err| format!("failed to focus main window: {err}"))?; // `WebviewWindow::set_focus` only dispatches `WindowMessage::SetFocus` // (vendor/tauri-cef cef_impl.rs `WindowMessage::SetFocus` → `window.request_focus()`), // which lifts the OS window but does NOT call `CefBrowserHost::SetFocus(true)`. // Without that CEF-level focus call the renderer never gets wired as the // keyboard input target on cold launch: the chat textarea accepts focus // (cursor blinks) but `WM_KEYDOWN` messages aren't forwarded to it, so // typing is silently dead until the user click-outside / click-back // triggers `WM_KILLFOCUS`+`WM_SETFOCUS` and CEF's window handler routes // through `host.set_focus(1)` internally. // // Explicitly dispatch `WebviewMessage::SetFocus` (cef_impl.rs handler // for that variant), which is what actually invokes // `CefBrowserHost::SetFocus(true)`. let webview: &tauri::Webview = window.as_ref(); if let Err(err) = webview.set_focus() { log::warn!( "[show_main_window] CEF webview set_focus failed (non-fatal — \ keyboard routing may not initialize until user click-outside-and-back): {err}" ); } Ok(()) } #[cfg(target_os = "macos")] fn macos_app_menu(app: &AppHandle) -> tauri::Result> { let about = PredefinedMenuItem::about(app, None, None)?; let hide = PredefinedMenuItem::hide(app, None)?; let hide_others = PredefinedMenuItem::hide_others(app, None)?; let show_all = PredefinedMenuItem::show_all(app, None)?; let quit = MenuItem::with_id( app, APP_QUIT_MENU_ID, "Quit OpenHuman", true, Some("CmdOrCtrl+Q"), )?; let app_sep_1 = PredefinedMenuItem::separator(app)?; let app_sep_2 = PredefinedMenuItem::separator(app)?; let app_menu = Submenu::with_items( app, "OpenHuman", true, &[ &about, &app_sep_1, &hide, &hide_others, &show_all, &app_sep_2, &quit, ], )?; let undo = PredefinedMenuItem::undo(app, None)?; let redo = PredefinedMenuItem::redo(app, None)?; let cut = PredefinedMenuItem::cut(app, None)?; let copy = PredefinedMenuItem::copy(app, None)?; let paste = PredefinedMenuItem::paste(app, None)?; let select_all = PredefinedMenuItem::select_all(app, None)?; let edit_sep_1 = PredefinedMenuItem::separator(app)?; let edit_sep_2 = PredefinedMenuItem::separator(app)?; let edit_menu = Submenu::with_items( app, "Edit", true, &[ &undo, &redo, &edit_sep_1, &cut, ©, &paste, &edit_sep_2, &select_all, ], )?; let close = PredefinedMenuItem::close_window(app, None)?; let minimize = PredefinedMenuItem::minimize(app, None)?; let fullscreen = PredefinedMenuItem::fullscreen(app, None)?; let window_menu = Submenu::with_items(app, "Window", true, &[&close, &minimize, &fullscreen])?; Menu::with_items(app, &[&app_menu, &edit_menu, &window_menu]) } #[cfg(target_os = "linux")] fn setup_tray(app: &AppHandle) -> tauri::Result<()> { let _ = app; log::warn!( "[tray] skipping tray setup on linux: tray menu creation still panics inside GTK during packaged runs" ); Ok(()) } #[cfg(not(target_os = "linux"))] fn setup_tray(app: &AppHandle) -> tauri::Result<()> { log::info!("[tray] setting up tray icon"); let show_item = MenuItem::with_id( app, "tray_show_window", "Open OpenHuman", true, None::<&str>, )?; let quit_item = MenuItem::with_id(app, "tray_quit", "Quit", true, None::<&str>)?; // The floating mascot has a native NSPanel + WKWebView host, so the // tray entry only does anything on macOS. Don't surface a menu item // on Windows that's guaranteed to error — gate it to the platform // where `mascot_window_show` actually works. #[cfg(target_os = "macos")] let menu = { let mascot_item = MenuItem::with_id( app, "tray_toggle_mascot", "Toggle floating mascot", true, None::<&str>, )?; Menu::with_items(app, &[&show_item, &mascot_item, &quit_item])? }; #[cfg(not(target_os = "macos"))] let menu = Menu::with_items(app, &[&show_item, &quit_item])?; let icon = app .default_window_icon() .cloned() .ok_or_else(|| tauri::Error::AssetNotFound("default window icon".to_string()))?; TrayIconBuilder::with_id("openhuman-tray") .icon(icon) .menu(&menu) .on_menu_event(|app, event| match event.id().as_ref() { "tray_show_window" => { log::info!("[tray] action=show_window source=menu"); if let Err(err) = show_main_window(app) { log::warn!("[tray] failed to show main window from menu: {err}"); } } "tray_toggle_mascot" => { log::info!("[tray] action=toggle_mascot source=menu"); if mascot_native_window_is_open() { if let Err(err) = mascot_window_hide(app.clone()) { log::error!("[tray] failed to hide mascot window: {err}"); } } else if let Err(err) = mascot_window_show(app.clone()) { log::error!("[tray] failed to show mascot window: {err}"); } } "tray_quit" => { log::info!("[tray] action=quit source=menu"); shutdown_app_sync(app, 0); } _ => {} }) .on_tray_icon_event(|tray, event| { if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = event { log::info!("[tray] action=show_window source=left_click"); if let Err(err) = show_main_window(tray.app_handle()) { log::warn!("[tray] failed to show main window from tray click: {err}"); } } }) .build(app)?; log::info!("[tray] tray icon ready"); Ok(()) } const CEF_PREWARM_LABEL: &str = "cef-prewarm"; /// Spawn a hidden 1×1 child webview at `about:blank` on the main window so /// CEF's child-webview render path is hot before the user clicks an /// account. The first `webview_account_open` then skips the cold /// renderer-process spinup. Idempotent — bails if the prewarm webview /// already exists. fn spawn_cef_prewarm(app: &AppHandle) -> Result<(), String> { use tauri::webview::WebviewBuilder; use tauri::WebviewUrl; if app.get_webview(CEF_PREWARM_LABEL).is_some() { return Ok(()); } let parent = app .get_window("main") .ok_or_else(|| "main window not found".to_string())?; let url: tauri::Url = "about:blank" .parse() .map_err(|e| format!("about:blank parse: {e}"))?; let builder = WebviewBuilder::new(CEF_PREWARM_LABEL, WebviewUrl::External(url)); parent .add_child( builder, tauri::LogicalPosition::new(-20000.0, -20000.0), tauri::LogicalSize::new(1.0, 1.0), ) .map_err(|e| format!("add_child failed: {e}"))?; log::info!("[cef-prewarm] hidden warmup webview spawned"); Ok(()) } /// Drop the prewarm webview if still alive. Called from `RunEvent::Exit` /// so its CEF browser is torn down before `cef::shutdown()` runs. fn teardown_cef_prewarm(app: &AppHandle) -> Result<(), String> { let Some(wv) = app.get_webview(CEF_PREWARM_LABEL) else { return Err("no prewarm webview".into()); }; wv.close().map_err(|e| e.to_string())?; log::info!("[cef-prewarm] teardown ok"); Ok(()) } const CEF_CLOSE_FIXED_YIELD: std::time::Duration = std::time::Duration::from_millis(20); const CEF_CLOSE_POLL_BUDGET: std::time::Duration = std::time::Duration::from_millis(300); const CEF_CLOSE_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(20); fn close_early_cef_webviews(app: &AppHandle) -> Vec { let mut closed_labels = Vec::new(); if teardown_cef_prewarm(app).is_ok() { closed_labels.push(CEF_PREWARM_LABEL.to_string()); } if let Some(state) = app.try_state::() { closed_labels.extend(state.shutdown_all(app)); } closed_labels } fn shutdown_imessage_scanner(app: &AppHandle) { if let Some(registry) = app.try_state::>() { registry.inner().shutdown(); } } fn pending_cef_webview_labels( app: &AppHandle, labels: &[String], ) -> Vec { let mut seen = std::collections::HashSet::new(); labels .iter() .filter(|label| seen.insert((*label).clone())) .filter(|label| app.get_webview(label.as_str()).is_some()) .cloned() .collect() } async fn wait_for_cef_webviews_to_close_async( app: &AppHandle, labels: &[String], ) { if labels.is_empty() { return; } log::info!( "[app] waiting for CEF webview close requests labels={:?}", labels ); tokio::time::sleep(CEF_CLOSE_FIXED_YIELD).await; let start = std::time::Instant::now(); let mut pending = pending_cef_webview_labels(app, labels); while !pending.is_empty() && start.elapsed() < CEF_CLOSE_POLL_BUDGET { tokio::time::sleep(CEF_CLOSE_POLL_INTERVAL).await; pending = pending_cef_webview_labels(app, labels); } if pending.is_empty() { log::info!( "[app] CEF webview close poll drained labels={:?} elapsed_ms={}", labels, start.elapsed().as_millis() ); } else { log::info!( "[app] CEF webview close poll still pending labels={:?} elapsed_ms={} (will continue in runtime shutdown)", pending, start.elapsed().as_millis() ); } } /// Shared early teardown logic before CEF's shutdown to prevent races and zombie processes. /// /// Synchronous entry used from `RunEvent::ExitRequested` and tray quit. We intentionally /// **do not** poll here with `std::thread::sleep` — that would block the Tauri / CEF main /// event loop and prevent close messages from being processed. Close requests are issued /// in [`close_early_cef_webviews`]; the exit pump drains them. Use /// [`perform_early_teardown_async`] when an async caller can await /// [`wait_for_cef_webviews_to_close_async`] without starving the UI loop. fn perform_early_teardown_sync(app_handle: &AppHandle) { log::info!("[app] perform_early_teardown_sync — early teardown"); let closed_labels = close_early_cef_webviews(app_handle); shutdown_imessage_scanner(app_handle); webview_apis::server::stop(); if let Some(core) = app_handle.try_state::() { let core = core.inner().clone(); // Aborts the embedded server task. Synchronous and safe on // the UI thread — `JoinHandle::abort` returns immediately. tauri::async_runtime::block_on(async move { core.send_terminate_signal().await; }); } if !closed_labels.is_empty() { log::info!( "[app] sync early teardown: close requested for labels={:?} — skipping main-thread poll so the event loop can drain CEF", closed_labels ); } log::info!("[app] perform_early_teardown_sync — early teardown complete"); } fn perform_early_teardown_sync_once(app_handle: &AppHandle, reason: &str) { if EARLY_TEARDOWN_RAN.swap(true, std::sync::atomic::Ordering::SeqCst) { log::info!( "[app] perform_early_teardown_sync_once — already ran, skipping reason={reason}" ); return; } log::info!("[app] perform_early_teardown_sync_once — reason={reason}"); perform_early_teardown_sync(app_handle); } /// Shared early teardown logic before CEF's shutdown to prevent races and zombie processes. /// Asynchronous version to be called from async Tauri commands (e.g. `restart_app`, updates). async fn perform_early_teardown_async(app_handle: &AppHandle) { log::info!("[app] perform_early_teardown_async — early teardown"); let closed_labels = close_early_cef_webviews(app_handle); shutdown_imessage_scanner(app_handle); webview_apis::server::stop(); if let Some(core) = app_handle.try_state::() { let core = core.inner().clone(); core.send_terminate_signal().await; } wait_for_cef_webviews_to_close_async(app_handle, &closed_labels).await; log::info!("[app] perform_early_teardown_async — early teardown complete"); } /// Explicitly winds down CEF and Tauri before an app.exit(0) fn shutdown_app_sync(app_handle: &AppHandle, exit_code: i32) { log::info!("[app] shutdown_app_sync — starting early teardown"); perform_early_teardown_sync_once(app_handle, "shutdown_app_sync"); log::info!("[app] shutdown_app_sync — early teardown complete, exiting"); app_handle.exit(exit_code); } #[cfg(target_os = "linux")] const WSL_X11_DESKTOP_WARNING: &str = "[startup] likely unsupported desktop environment: WSL with classic X11 forwarding detected (DISPLAY is set, but WAYLAND_DISPLAY/WSLg markers are absent). OpenHuman's Tauri/CEF desktop flow is fragile in this setup; use native Windows development or Windows 11 WSLg for desktop GUI work."; #[cfg(any(target_os = "linux", test))] fn should_warn_for_wsl_x11_desktop( is_wsl: bool, display_set: bool, wayland_display_set: bool, wslg_marker_set: bool, ) -> bool { is_wsl && display_set && !wayland_display_set && !wslg_marker_set } #[cfg(target_os = "linux")] fn is_wsl_environment() -> bool { if std::env::var("WSL_DISTRO_NAME") .ok() .filter(|v| !v.trim().is_empty()) .is_some() { return true; } std::fs::read_to_string("/proc/sys/kernel/osrelease") .ok() .map(|release| release.to_ascii_lowercase().contains("microsoft")) .unwrap_or(false) } #[cfg(target_os = "linux")] fn has_non_empty_env(key: &str) -> bool { std::env::var(key) .ok() .filter(|v| !v.trim().is_empty()) .is_some() } #[cfg(target_os = "linux")] fn has_wslg_marker() -> bool { has_non_empty_env("WSLG_RUNTIME_DIR") || std::path::Path::new("/mnt/wslg").exists() } #[cfg(target_os = "linux")] fn warn_if_wsl_x11_desktop_launch() { if should_warn_for_wsl_x11_desktop( is_wsl_environment(), has_non_empty_env("DISPLAY"), has_non_empty_env("WAYLAND_DISPLAY"), has_wslg_marker(), ) { log::warn!("{WSL_X11_DESKTOP_WARNING}"); eprintln!("{WSL_X11_DESKTOP_WARNING}"); } } #[cfg(not(target_os = "linux"))] fn warn_if_wsl_x11_desktop_launch() {} type CefCommandLineArg = (&'static str, Option<&'static str>); fn append_platform_cef_gpu_workarounds(args: &mut Vec, 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 // before Chromium's own fallback path gets a usable renderer. Disable the // hardware GPU path on Linux so packaged builds can still launch via // software compositing. if os == "linux" { args.push(("--disable-gpu", None)); args.push(("--disable-gpu-compositing", None)); log::info!( "[cef-startup] Linux detected: adding --disable-gpu and --disable-gpu-compositing (issue #1697)" ); } // Issue #1012: Intel macOS (x86_64) crashes with EXC_CRASH (SIGABRT) // inside CrBrowserMain when CEF 146 tries to use GPU compositing via // Metal on Intel GPU hardware/drivers. Disable GPU compositing on // x86_64 macOS so the browser process falls back to software compositing // instead of aborting. if os == "macos" && arch == "x86_64" { args.push(("--disable-gpu-compositing", None)); log::info!( "[cef-startup] Intel macOS detected: adding --disable-gpu-compositing (issue #1012)" ); } } pub fn run() { // Initialize Sentry for the Tauri shell (desktop host) process before any // other startup work. Reads `OPENHUMAN_TAURI_SENTRY_DSN` at runtime first, // then falls back to the value baked in at compile time via the release // workflow. Missing/empty DSN ⇒ `sentry::init` returns a no-op guard. // // The guard is held for the entire lifetime of `run()` so events queued // during shutdown still flush. Only invoked here (and not in `main.rs`) // so renderer/GPU CEF helper subprocesses (re-exec'd via // `tauri::cef_entry_point`) and the `OpenHuman core …` in-process core // path do NOT spin up a second client — those have their own reporting // surfaces. let _sentry_guard = sentry::init(sentry::ClientOptions { dsn: std::env::var("OPENHUMAN_TAURI_SENTRY_DSN") .ok() .filter(|s| !s.is_empty()) .or_else(|| option_env!("OPENHUMAN_TAURI_SENTRY_DSN").map(|s| s.to_string())) .filter(|s| !s.is_empty()) .and_then(|s| s.parse().ok()), release: Some(std::borrow::Cow::Owned(build_sentry_release_tag())), environment: Some(std::borrow::Cow::Owned(resolve_sentry_environment())), send_default_pii: false, before_send: Some(std::sync::Arc::new(|mut event| { // Drop "dev-server fetch failed" noise: the vendored // `tauri-runtime-cef` dev proxy // (vendor/tauri-cef/crates/tauri/src/protocol/tauri.rs) calls // `log::error!("Failed to request {url}: {err}")` whenever the // CEF webview asks for an asset on `http://localhost:1420` (the // Vite dev URL baked into `tauri.conf.json`). That `log::error!` // is bridged into `tracing` and picked up by the sentry-tracing // layer as an Event — see `src/core/logging.rs::sentry_tracing_layer`. // In packaged staging/production builds Vite isn't running, so // the request correctly fails — but the failure is noise we // don't want in Sentry (issue OPENHUMAN-TAURI-V, 66+ events). // See [sentry-localhost-filter] log line below for diagnostics. if event_is_localhost_dev_fetch_noise(&event) { log::debug!( "[sentry-localhost-filter] dropping dev-server fetch noise event: {:?}", event.message.as_deref().unwrap_or("") ); return None; } if openhuman_core::core::observability::is_budget_event(&event) { // Log only structured tag metadata — `event.message` can carry // upstream provider error text including tokens / pasted-through // secrets, and per `CLAUDE.md` "never log secrets or full PII". // The (domain, status) pair is sufficient diagnostic since // those are the tags `is_budget_event` gates on. log::debug!( "[sentry-budget-filter] dropping budget-exhausted event (domain={:?}, status={:?})", event.tags.get("domain"), event.tags.get("status") ); return None; } // Defense-in-depth: drop max-tool-iterations cap events that // slipped past the call-site filters in the core (see // `openhuman_core::core::observability::is_max_iterations_event` // for the rationale). The shell links the core in-process so // any captured event for this deterministic agent-state // outcome is filtered here too (OPENHUMAN-TAURI-99 / -98). if openhuman_core::core::observability::is_max_iterations_event(&event) { log::debug!( "[sentry-max-iter-filter] dropping max-iteration cap noise event: {:?}", event.message.as_deref().unwrap_or("") ); return None; } if openhuman_core::core::observability::is_transient_backend_api_failure(&event) || openhuman_core::core::observability::is_transient_integrations_failure(&event) || openhuman_core::core::observability::is_updater_transient_event(&event) { return None; } // Drop 401 "Session expired. Please log in again." bodies and // pre-flight "no session token stored" guards — mirrors the // core binary's before_send chain. Since #1061 the Tauri shell // links the core in-process, so any session-expired event // captured by either surface lands in the same Sentry client // here and must be filtered identically. Keeps // OPENHUMAN-TAURI-25 / -1Q / -27 / -1G off Sentry. if openhuman_core::core::observability::is_session_expired_event(&event) { // Metadata-only log shape — `event.message` carries the raw // backend response body which CLAUDE.md forbids from local // logs. Mirror the core binary's main.rs filter. log::debug!( "[sentry-session-expired-filter] dropping session-expired event_id={:?}", event.event_id ); return None; } // Strip server_name (hostname) to avoid leaking machine identity. event.server_name = None; event.user = None; Some(event) })), sample_rate: 1.0, ..sentry::ClientOptions::default() }); // Tag every Sentry event with CPU architecture and OS so Intel-specific // crashes (issue #1012 — SIGABRT in CrBrowserMain on x86_64 macOS) are // clearly identified without needing a separate build identifier. sentry::configure_scope(|scope| { scope.set_tag("cpu_arch", std::env::consts::ARCH); scope.set_tag("os_name", std::env::consts::OS); #[cfg(target_os = "macos")] if let Some(ver) = macos_os_version() { scope.set_tag("os_version", ver); } }); // Optional smoke trigger for verifying the Sentry pipeline end-to-end. // Run with `OPENHUMAN_TAURI_SENTRY_TEST=panic` to fire a panic, or // `=message` to send a captured-message event. No-op when unset. if let Ok(mode) = std::env::var("OPENHUMAN_TAURI_SENTRY_TEST") { match mode.as_str() { "panic" => panic!("OPENHUMAN_TAURI_SENTRY_TEST=panic — local Sentry smoke test"), "message" => { sentry::capture_message( "OPENHUMAN_TAURI_SENTRY_TEST=message — local Sentry smoke test", sentry::Level::Error, ); let _ = sentry::Hub::current().client().map(|c| c.flush(None)); } other => log::warn!( "OPENHUMAN_TAURI_SENTRY_TEST={other:?} — unknown mode (use 'panic' or 'message')" ), } } let daemon_mode = is_daemon_mode(); // Install the unified tracing subscriber + daily-rotated file appender // before any other startup work so CEF preflight failures, sentry // smoke-test events, and the rest of `run()` are captured in // `/logs/openhuman-YYYY-MM-DD.log`. The shell's `log::*` calls // are bridged into the same subscriber via `tracing_log::LogTracer`, // replacing the previous stderr-only `env_logger`. file_logging::init(); // Log platform identity early so every log session is tagged with arch // and OS version — essential for reproducing and triaging Intel-only // crashes like issue #1012 (SIGABRT in CrBrowserMain on x86_64 macOS). { let arch = std::env::consts::ARCH; let os = std::env::consts::OS; #[cfg(target_os = "macos")] let os_ver = macos_os_version().unwrap_or_else(|| "unknown".to_string()); #[cfg(not(target_os = "macos"))] let os_ver = "n/a".to_string(); log::info!("[startup] platform: arch={arch} os={os} os_version={os_ver}"); } warn_if_wsl_x11_desktop_launch(); // 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 // rustls 0.23's `CryptoProvider::get_default()`. rustls 0.23 no longer // picks a provider implicitly — without one installed, the proxy panics // with "No provider set" the first time `tauri dev` forwards a request. // Install the ring provider once before any HTTPS client is built. let _ = rustls::crypto::ring::default_provider().install_default(); // ── Windows pre-CEF single-instance guard (Sentry OPENHUMAN-TAURI-A) ── // // `tauri_plugin_single_instance` detects a second launch inside its // `.setup()` hook — but `.setup()` runs AFTER `Builder::build()` which // calls `CefRuntime::init` → `cef::initialize()`. On a second launch, // `cef::initialize()` returns 0 because the primary holds the CEF // cache lock; the vendored runtime asserts `result == 1` and panics // (left: 0, right: 1, fatal, Windows-only, 598 events). // // Fix: acquire a named Win32 mutex at the very top of `run()` — before // any CEF or builder work — so any secondary instance sees // `ERROR_ALREADY_EXISTS` and exits immediately. The mutex name uses // a `-cef-init` suffix distinct from the plugin's own `-sim` mutex so // the two guards don't interfere; the plugin still handles WM_COPYDATA // forwarding for graceful "focus primary" behaviour once the app is // fully initialised. // // The RAII guard holds the mutex handle for the lifetime of `run()`. // Windows releases all process handles automatically on exit, so // explicit cleanup is only needed if `run()` returns normally. #[cfg(windows)] let _cef_init_mutex_guard = { use windows_sys::Win32::Foundation::{CloseHandle, GetLastError, ERROR_ALREADY_EXISTS}; use windows_sys::Win32::System::Threading::CreateMutexW; // Must match the bundle identifier in tauri.conf.json. // Changing the app identifier requires updating this string too. let mutex_name: Vec = "com.openhuman.app-cef-init\0".encode_utf16().collect(); // SAFETY: mutex_name is null-terminated UTF-16; handle is checked below. let handle = unsafe { CreateMutexW(std::ptr::null(), 0, mutex_name.as_ptr()) }; if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS { // Another instance is already past this point — exit before we // touch CEF at all. The plugin's WM_COPYDATA path won't run // here (it needs an AppHandle from setup()), but the primary // is already showing its window so the user experience is fine. if !handle.is_null() { unsafe { CloseHandle(handle) }; } log::info!( "[single-instance] pre-CEF mutex held by primary; secondary exiting (OPENHUMAN-TAURI-A fix)" ); std::process::exit(0); } // Primary: hold the handle until run() returns. struct OwnedMutex(isize); impl Drop for OwnedMutex { fn drop(&mut self) { if self.0 != 0 { unsafe { CloseHandle(self.0 as _) }; } } } OwnedMutex(handle as isize) }; // CEF cache-lock preflight (macOS only): if another OpenHuman instance // is already holding the CEF user-data-dir, the vendored // `tauri-runtime-cef` panics inside `cef::initialize` with a Rust // backtrace and no actionable message (issue #864). Catch the collision // here and exit cleanly with a message that names the lock-holder PID // and the workaround. Stale locks (PID dead) are removed and we // continue, matching Chromium's own startup recovery. match cef_profile::prepare_process_cache_path() { Ok(path) => log::debug!("[cef-profile] startup cache path={}", path.display()), Err(error) => { log::error!( "[cef-profile] failed to configure per-user CEF cache; refusing to start with shared/global cache: {error}" ); std::process::exit(1); } } #[cfg(target_os = "macos")] process_recovery::reap_stale_openhuman_processes(); #[cfg(target_os = "macos")] if let Err(e) = cef_preflight::check_default_cache() { eprintln!("\n[openhuman] {e}\n"); std::process::exit(1); } let builder = { // Bypass macOS Keychain. Without this, every embedded service that // touches password / cookie / encryption-key storage triggers a // "Allow access to your keychain?" prompt — WhatsApp Web hits it on // every cold start, Chromium's own component-update store also does. // `use-mock-keychain` swaps the Keychain backend for an in-process // mock; `password-store=basic` is the equivalent for the password // manager. Both are no-ops on Windows/Linux, so safe to always set. // // In debug builds we additionally expose the Chrome DevTools // Protocol on localhost:19222 so every CEF webview can be // inspected from a regular browser (right-click "Inspect" does // not propagate to CEF child webviews on macOS). Release builds // intentionally do NOT open the CDP port — it would let any // process on the machine drive the embedded WhatsApp/Slack/etc. // webviews. // // The port was 9222 (Chromium's default) but ollama's // OpenAI-compatible server squats on 127.0.0.1:9222 in some // installs, which silently broke CDP attach (our client hit // ollama, the WS handshake failed, child webviews stayed at // about:blank → black screen). Picked 19222 to dodge that // collision; if you change it here also update // `cdp::CDP_PORT` and `whatsapp_scanner::CDP_PORT`. // // NOTE: flags must be prefixed with `--`. The runtime's // `on_before_command_line_processing` dispatch (in // `tauri-runtime-cef/src/cef_impl.rs`) routes value-less args that // don't start with `-` to `append_argument` (positional) instead of // `append_switch`, which means Chromium silently ignores them. let mut args: Vec = vec![ ("--use-mock-keychain", None), ("--password-store", Some("basic")), // Enable SharedArrayBuffer so embedded apps that need WebRTC // audio worklets / Opus encoders (Slack Huddles, Meet // real-time features, Discord voice) can actually initialise. // Chromium gates SharedArrayBuffer behind cross-origin // isolation by default; web apps embedded inside CEF rarely // send COOP/COEP headers, so without this flag the feature // silently disappears and huddle/call buttons no-op. ("--enable-features", Some("SharedArrayBuffer")), // Defeat Chromium's modern throttlers that ignore the // legacy `--disable-background-timer-throttling` flag. // Empirically with that flag *alone*, the producer in the // (visible but non-key) main window still got pinned to // 1Hz worker timers as soon as the off-screen Meet window // opened. These three feature flags are the canonical // additional knobs (Electron / Puppeteer use them). ( "--disable-features", Some( "IntensiveWakeUpThrottling,CalculateNativeWinOcclusion,\ AutofillActorMode,GlicActorUi,LensOverlay", ), ), // Allow autoplay (audio + video) without a prior user gesture. // CEF inherits Chromium's default policy, which leaves an // AudioContext suspended until the user interacts with the // page; @remotion/player gates its rAF frame loop on // AudioContext.state === 'running', so on a cold tab the // mascot SVG paints frame 0 and freezes there until the user // alt-tabs / clicks somewhere (which counts as a gesture and // resumes the context — the "fast on revisit" symptom). With // this flag the AudioContext starts in 'running' immediately // and the mascot animates from first paint. We control every // surface in this webview, so dropping the gesture gate is // safe. ("--autoplay-policy", Some("no-user-gesture-required")), // Background-throttling defeaters. The MeetCallProducer // pumps mascot frames at 24 fps from the *main* OpenHuman // window, but as soon as the off-screen Meet webview opens // (or the user clicks anywhere outside main), macOS demotes // the renderer's priority and Chromium throttles its // setInterval / worker timers / rAF down to ~1 Hz — the // mascot stream collapses to 1 fps. Page-level workarounds // (silent AudioContext, muted