diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index 26a4fb358..e9391ddde 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -408,6 +408,33 @@ struct ResolvedDataPaths { active_workspace_marker_path: std::path::PathBuf, } +fn is_windows_file_lock_raw_os_error(raw_os_error: Option) -> bool { + matches!(raw_os_error, Some(32 | 33)) +} + +fn is_windows_file_lock_error(error: &std::io::Error) -> bool { + cfg!(windows) && is_windows_file_lock_raw_os_error(error.raw_os_error()) +} + +fn reset_local_data_delete_error( + label: &str, + path: &std::path::Path, + error: &std::io::Error, +) -> String { + if is_windows_file_lock_error(error) { + log::warn!( + "[core] reset_local_data: Windows file lock blocked removal of {label} at {}: {error}", + path.display() + ); + return format!( + "Failed to remove {label} at {} because it is locked by another OpenHuman window or process. Close all OpenHuman windows and try again. ({error})", + path.display() + ); + } + + format!("Failed to remove {label} at {}: {error}", path.display()) +} + /// Call the core's `config_get_data_paths` RPC and parse the response. async fn fetch_data_paths() -> Result { let url = crate::core_rpc::core_rpc_url_value(); @@ -475,10 +502,7 @@ async fn remove_path_if_exists(path: &std::path::Path, label: &str) -> Result<() ); Ok(()) } - Err(e) => Err(format!( - "Failed to remove {label} at {}: {e}", - path.display() - )), + Err(e) => Err(reset_local_data_delete_error(label, path, &e)), } } @@ -499,10 +523,7 @@ async fn remove_dir_if_exists(path: &std::path::Path, label: &str) -> Result<(), ); Ok(()) } - Err(e) => Err(format!( - "Failed to remove {label} at {}: {e}", - path.display() - )), + Err(e) => Err(reset_local_data_delete_error(label, path, &e)), } } @@ -3443,6 +3464,41 @@ mod tests { } } + #[test] + fn reset_local_data_windows_file_lock_error_codes_are_recognized() { + assert!(is_windows_file_lock_raw_os_error(Some(32))); + assert!(is_windows_file_lock_raw_os_error(Some(33))); + assert!(!is_windows_file_lock_raw_os_error(Some(5))); + assert!(!is_windows_file_lock_raw_os_error(None)); + } + + #[test] + fn reset_local_data_delete_error_keeps_generic_message_for_other_errors() { + let err = std::io::Error::from(std::io::ErrorKind::PermissionDenied); + let msg = reset_local_data_delete_error( + "current openhuman dir", + std::path::Path::new("/tmp/openhuman"), + &err, + ); + + assert!(msg.starts_with("Failed to remove current openhuman dir at /tmp/openhuman:")); + assert!(!msg.contains("Close all OpenHuman windows and try again")); + } + + #[cfg(windows)] + #[test] + fn reset_local_data_delete_error_explains_windows_file_locks() { + let err = std::io::Error::from_raw_os_error(32); + let msg = reset_local_data_delete_error( + "current openhuman dir", + std::path::Path::new("C:\\Users\\me\\.openhuman"), + &err, + ); + + assert!(msg.contains("locked by another OpenHuman window or process")); + assert!(msg.contains("Close all OpenHuman windows and try again")); + } + /// Tests for setup_tray conditional compilation /// The PR adds two versions of setup_tray(): /// 1. No-op for linux + cef: logs warning and returns Ok(())