Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions app/src-tauri/src/file_logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,14 @@ mod tests {
/// can race; the lock keeps the env stable for each test's duration.
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

/// Acquire the test environment lock, recovering from poison if needed.
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}

#[test]
fn resolve_data_dir_honors_workspace_override() {
let _guard = ENV_LOCK.lock().unwrap();
let _guard = env_lock();
let prior = std::env::var("OPENHUMAN_WORKSPACE").ok();
std::env::set_var("OPENHUMAN_WORKSPACE", "/tmp/openhuman-test-override");
let dir = resolve_data_dir();
Expand All @@ -73,7 +78,7 @@ mod tests {

#[test]
fn resolve_data_dir_ignores_empty_workspace() {
let _guard = ENV_LOCK.lock().unwrap();
let _guard = env_lock();
let prior = std::env::var("OPENHUMAN_WORKSPACE").ok();
std::env::set_var("OPENHUMAN_WORKSPACE", "");
// Empty string must NOT short-circuit — fall through to the
Expand Down
187 changes: 90 additions & 97 deletions app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ mod webview_apis;
mod whatsapp_scanner;
mod window_state;

#[cfg(target_os = "windows")]
use std::sync::atomic::{AtomicBool, Ordering};

#[cfg(any(target_os = "macos", target_os = "windows"))]
use tauri::WindowEvent;
#[cfg(not(target_os = "linux"))]
Expand All @@ -54,6 +57,11 @@ 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;

// Guard to prevent multiple concurrent focus fix cycles on Windows.
// Ensures the fix runs only once per app launch.
#[cfg(target_os = "windows")]
static FOCUS_FIX_RUNNING: AtomicBool = AtomicBool::new(false);

#[tauri::command]
fn core_rpc_url() -> String {
crate::core_rpc::core_rpc_url_value()
Expand Down Expand Up @@ -1634,88 +1642,82 @@ pub fn run() {
if let Err(err) = window.show() {
log::warn!("[window-state] show main window failed: {err}");
}
// CEF keyboard routing fix — cold launch:

// CEF keyboard routing fix for Windows cold launch.
//
// Background: After window.show(), keyboard input doesn't reach
// the CEF renderer until the user manually clicks outside and
// back into the window. This happens because CEF's internal
// focus state needs initialization that doesn't occur during
// the initial show() call.
//
// `window.show()` does not wire the renderer as the
// keyboard input target. `Window::set_focus` only
// dispatches `WindowMessage::SetFocus` → `request_focus`,
// which lifts the OS window but does not call
// `CefBrowserHost::SetFocus(true)`. Without that
// CEF-level focus, the textarea accepts focus on cold
// launch (cursor blinks) but `WM_KEYDOWN` messages
// never reach the renderer — typing is silently dead
// until the user click-outside / click-back triggers
// `WM_KILLFOCUS`+`WM_SETFOCUS`, which CEF's window
// handler routes through `host.set_focus(1)` internally.
// Previous approach (issue #1584): Used minimize/unminimize
// cycle to trigger WM_KILLFOCUS + WM_SETFOCUS events. This
// worked but caused visible flickering that users reported
// as "rapid flashing" making the app unusable.
//
// We need to call `webview.set_focus()` (which dispatches
// `WebviewMessage::SetFocus` → `host.set_focus(1)`)
// *after* CEF has finished creating the browser — too
// early and `browser()`/`host()` return None and the
// call silently no-ops. Defer the call to a spawned
// task with a small delay so CEF's browser-create
// settles. Then call it again after another delay as
// belt-and-suspenders for slower init paths.
// Previous attempts at calling `webview.set_focus()` alone
// confirmed the dispatch reaches CEF (both returned Ok),
// but keyboard routing stayed broken. `host.set_focus(1)`
// alone is insufficient — CEF's internal focus state
// needs a blur-then-focus *cycle* to wire keyboard
// routing on cold launch (matches the user-discovered
// workaround: click outside the window, then click back).
// Current approach: Direct focus calls without window state
// changes. We call set_focus() on both the window and webview
// after CEF finishes initializing. This is less disruptive
// but may not work on all systems. Users can disable via
// OPENHUMAN_DISABLE_FOCUS_FIX=1 if it causes problems.
//
// The vendored tauri-cef doesn't expose `set_focus(false)`,
// so we mimic the cycle at the OS-window level:
// minimize triggers `WM_KILLFOCUS` (CEF's window handler
// propagates this to `host.set_focus(0)`), unminimize
// restores the window and triggers `WM_SETFOCUS` →
// `host.set_focus(1)`. Pair with explicit `set_focus`
// calls on both Window and Webview to cover the case
// where minimize/unminimize raced ahead of CEF's
// browser-create.
// Windows-only: the bug class (CEF host-renderer focus
// desync after a `visible: false` → `show()` transition
// without a real `WM_KILLFOCUS`+`WM_SETFOCUS` edge)
// manifests on the Windows CEF integration. macOS and
// Linux CEF use different focus propagation paths and
// don't exhibit the symptom, so running the
// minimize/unminimize cycle there would just be a
// visible flicker for no benefit. (Per CodeRabbit
// review on PR #1528.)
// The fix runs only once per launch (guarded by atomic flag)
// to prevent multiple concurrent cycles if the window is
// shown/hidden/shown during startup.
#[cfg(target_os = "windows")]
{
log::info!("[focus-fix] scheduling deferred CEF focus-cycle");
let webview_window_clone = window.clone();
tauri::async_runtime::spawn(async move {
// Wait for CEF to finish creating the browser host
// (synchronous setup() returns before this completes).
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
// Blur-then-focus cycle via minimize/unminimize.
// This is what the manual click-outside / click-back
// workaround does at the Win32 level.
log::info!("[focus-fix] starting minimize→unminimize focus cycle");
if let Err(err) = webview_window_clone.minimize() {
log::warn!("[focus-fix] minimize failed: {err}");
}
// Tiny pause so Windows actually processes the
// minimize before we ask to restore.
tokio::time::sleep(std::time::Duration::from_millis(80)).await;
if let Err(err) = webview_window_clone.unminimize() {
log::warn!("[focus-fix] unminimize failed: {err}");
}
// Belt-and-suspenders: explicit Window + Webview focus
// after the cycle in case the minimize→restore path
// didn't propagate.
tokio::time::sleep(std::time::Duration::from_millis(40)).await;
if let Err(err) = webview_window_clone.set_focus() {
log::warn!("[focus-fix] post-cycle window.set_focus failed: {err}");
}
let webview: &tauri::Webview<AppRuntime> = webview_window_clone.as_ref();
if let Err(err) = webview.set_focus() {
log::warn!("[focus-fix] post-cycle webview.set_focus failed: {err}");
// Check if user disabled the fix via environment variable
let fix_disabled = std::env::var("OPENHUMAN_DISABLE_FOCUS_FIX")
.map(|v| {
let v = v.trim().to_ascii_lowercase();
v == "1" || v == "true" || v == "yes"
})
.unwrap_or(false);

if fix_disabled {
log::info!("[focus-fix] disabled via OPENHUMAN_DISABLE_FOCUS_FIX");
} else if FOCUS_FIX_RUNNING.compare_exchange(
false,
true,
Ordering::SeqCst,
Ordering::SeqCst,
).is_ok() {
log::info!("[focus-fix] scheduling deferred CEF focus initialization");
let webview_window_clone = window.clone();
tauri::async_runtime::spawn(async move {
// Wait longer for CEF to fully initialize. Previous
// 300ms was too short on some systems, causing the
// fix to run before CEF was ready.
tokio::time::sleep(std::time::Duration::from_millis(500)).await;

// First attempt: direct focus calls
log::info!("[focus-fix] applying focus (attempt 1/3)");
if let Err(err) = webview_window_clone.set_focus() {
log::warn!("[focus-fix] window.set_focus failed: {err}");
}
let webview: &tauri::Webview<AppRuntime> = webview_window_clone.as_ref();
if let Err(err) = webview.set_focus() {
log::warn!("[focus-fix] webview.set_focus failed: {err}");
}

// Second attempt after a short delay (belt-and-suspenders)
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
log::debug!("[focus-fix] applying focus (attempt 2/3)");
let _ = webview_window_clone.set_focus();
let _ = webview.set_focus();

// Third attempt for slower systems
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
log::debug!("[focus-fix] applying focus (attempt 3/3)");
let _ = webview_window_clone.set_focus();
let _ = webview.set_focus();

log::info!("[focus-fix] focus initialization complete");
});
} else {
log::debug!("[focus-fix] already running, skipping duplicate");
}
log::info!("[focus-fix] focus cycle complete");
});
}
}
}
Expand Down Expand Up @@ -2120,20 +2122,6 @@ pub fn run() {
let _ = window.hide();
}
}
#[cfg(target_os = "windows")]
RunEvent::WindowEvent {
label,
event: WindowEvent::CloseRequested { api, .. },
..
} if label == "main" => {
log::info!(
"[window] close requested on main window — hiding to tray instead of destroying"
);
api.prevent_close();
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.hide();
}
}
#[cfg(target_os = "macos")]
RunEvent::Reopen { .. } => {
log::info!("[window] reopen event — showing main window");
Expand Down Expand Up @@ -2309,6 +2297,11 @@ mod tests {
// spurious failures.
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

/// Acquire the test environment lock, recovering from poison if needed.
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}

/// Test that is_daemon_mode correctly detects daemon flag variations
#[test]
fn is_daemon_mode_detects_daemon_flag() {
Expand All @@ -2320,7 +2313,7 @@ mod tests {
/// Test core_rpc_url returns expected format
#[test]
fn core_rpc_url_returns_expected_format() {
let _g = ENV_LOCK.lock().unwrap();
let _g = env_lock();
let original = std::env::var("OPENHUMAN_CORE_RPC_URL").ok();

std::env::set_var("OPENHUMAN_CORE_RPC_URL", "http://localhost:9999/rpc");
Expand All @@ -2340,7 +2333,7 @@ mod tests {
/// Test overlay_parent_rpc_url handles empty env var
#[test]
fn overlay_parent_rpc_url_handles_empty() {
let _g = ENV_LOCK.lock().unwrap();
let _g = env_lock();
let original = std::env::var("OPENHUMAN_CORE_RPC_URL").ok();

std::env::set_var("OPENHUMAN_CORE_RPC_URL", "");
Expand Down Expand Up @@ -2543,7 +2536,7 @@ mod tests {

#[test]
fn sentry_environment_reads_openhuman_app_env() {
let _g = ENV_LOCK.lock().unwrap();
let _g = env_lock();
let key = "OPENHUMAN_APP_ENV";
let original = std::env::var(key).ok();
std::env::set_var(key, "staging");
Expand All @@ -2557,7 +2550,7 @@ mod tests {

#[test]
fn sentry_environment_trims_whitespace_from_openhuman_app_env() {
let _g = ENV_LOCK.lock().unwrap();
let _g = env_lock();
let key = "OPENHUMAN_APP_ENV";
let original = std::env::var(key).ok();
std::env::set_var(key, " dev ");
Expand All @@ -2571,7 +2564,7 @@ mod tests {

#[test]
fn sentry_environment_skips_empty_openhuman_app_env() {
let _g = ENV_LOCK.lock().unwrap();
let _g = env_lock();
let key = "OPENHUMAN_APP_ENV";
let original = std::env::var(key).ok();
std::env::set_var(key, "");
Expand All @@ -2586,7 +2579,7 @@ mod tests {

#[test]
fn sentry_environment_skips_whitespace_only_openhuman_app_env() {
let _g = ENV_LOCK.lock().unwrap();
let _g = env_lock();
let key = "OPENHUMAN_APP_ENV";
let original = std::env::var(key).ok();
std::env::set_var(key, " ");
Expand All @@ -2603,7 +2596,7 @@ mod tests {
/// asserts the hard default when no compile-time override is present.
#[test]
fn sentry_environment_defaults_to_production_when_unset() {
let _g = ENV_LOCK.lock().unwrap();
let _g = env_lock();
if option_env!("VITE_OPENHUMAN_APP_ENV").is_some() {
// A compile-time override is baked in; skip — the fallback path is
// exercised by sentry_environment_skips_empty_openhuman_app_env.
Expand Down
11 changes: 8 additions & 3 deletions src/core/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,8 +371,13 @@ mod tests {
/// concurrent env-var writes would race.
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

/// Acquire the test environment lock, recovering from poison if needed.
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}

fn with_clean_rust_log<R>(f: impl FnOnce() -> R) -> R {
let _guard = ENV_LOCK.lock().unwrap();
let _guard = env_lock();
let prior = std::env::var("RUST_LOG").ok();
std::env::remove_var("RUST_LOG");
let result = f();
Expand Down Expand Up @@ -435,7 +440,7 @@ mod tests {

#[test]
fn seed_rust_log_respects_existing_value() {
let _guard = ENV_LOCK.lock().unwrap();
let _guard = env_lock();
let prior = std::env::var("RUST_LOG").ok();
std::env::set_var("RUST_LOG", "warn");
seed_rust_log(true, CliLogDefault::Global);
Expand All @@ -456,7 +461,7 @@ mod tests {

#[test]
fn parse_log_file_constraints_handles_csv_and_whitespace() {
let _guard = ENV_LOCK.lock().unwrap();
let _guard = env_lock();
let prior = std::env::var("OPENHUMAN_LOG_FILE_CONSTRAINTS").ok();
std::env::set_var("OPENHUMAN_LOG_FILE_CONSTRAINTS", "rpc, , agent ,memory");
let parsed = parse_log_file_constraints();
Expand Down
4 changes: 2 additions & 2 deletions src/openhuman/composio/periodic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ pub(crate) async fn run_one_tick() -> Result<(), String> {
#[cfg(test)]
mod tests {
use super::*;
use crate::openhuman::config::TEST_ENV_LOCK as ENV_LOCK;
use crate::openhuman::config::test_env_lock;
use tempfile::tempdir;

#[test]
Expand Down Expand Up @@ -285,7 +285,7 @@ mod tests {
async fn run_one_tick_returns_ok_when_no_client() {
// Isolate the workspace/env so config loading doesn't contend with
// sibling tests mutating OPENHUMAN_WORKSPACE in parallel.
let _guard = ENV_LOCK.lock().expect("env lock");
let _guard = test_env_lock();
let tmp = tempdir().expect("tempdir");
unsafe {
std::env::set_var("OPENHUMAN_WORKSPACE", tmp.path());
Expand Down
9 changes: 9 additions & 0 deletions src/openhuman/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ pub use schemas::{
#[cfg(test)]
pub(crate) static TEST_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

/// Acquire the test environment lock, recovering from poison if needed.
/// When a test panics while holding the lock, the mutex becomes poisoned.
/// This helper clears the poison and returns the guard, allowing subsequent
/// tests to continue rather than cascading failures.
#[cfg(test)]
pub(crate) fn test_env_lock() -> std::sync::MutexGuard<'static, ()> {
TEST_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading