diff --git a/crates/codex-plus-core/src/watcher.rs b/crates/codex-plus-core/src/watcher.rs index 8a9965ba..ef6f68ad 100644 --- a/crates/codex-plus-core/src/watcher.rs +++ b/crates/codex-plus-core/src/watcher.rs @@ -5,6 +5,10 @@ use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::time::Duration; +#[cfg(windows)] +pub use crate::windows_integration::WindowsProcessInfo; + + pub const WATCHER_INTERVAL_SECONDS: f64 = 3.0; pub const CDP_PROBE_TIMEOUT_SECONDS: f64 = 0.5; pub const TAKEOVER_FAILURE_BACKOFF_SECONDS: f64 = 30.0; @@ -170,10 +174,22 @@ pub fn uninstall_watcher() -> anyhow::Result<()> { #[cfg(windows)] pub fn find_codex_processes() -> Vec { - codex_process_ids( - crate::windows_integration::enumerate_processes() - .into_iter() - .filter(|process| process.exe_file.eq_ignore_ascii_case("codex.exe")) + let processes: Vec<_> = crate::windows_integration::enumerate_processes() + .into_iter() + .filter(|process| process.exe_file.eq_ignore_ascii_case("codex.exe")) + .collect(); + find_codex_processes_from_snapshot(&processes) +} + +/// Filter the list of already enumerated Windows processes for Codex processes. +/// Exposed so the Windows-specific logic can be unit-tested without scanning the live system. +#[cfg(windows)] +pub fn find_codex_processes_from_snapshot( + processes: &[crate::windows_integration::WindowsProcessInfo], +) -> Vec { + let mut ids = codex_process_ids( + processes + .iter() .filter_map(|process| { process .executable_path @@ -183,7 +199,19 @@ pub fn find_codex_processes() -> Vec { .collect::>() .iter() .map(|(pid, path)| (*pid, path.as_str())), - ) + ); + + // Local/portable installs use "Codex.exe" (capital C) as the Electron main process. + // Keep the launcher alive while that process is still running. + for process in processes { + if process.exe_file.eq_ignore_ascii_case("Codex.exe") { + ids.push(process.process_id); + } + } + + ids.sort_unstable(); + ids.dedup(); + ids } #[cfg(not(windows))] diff --git a/crates/codex-plus-core/tests/watcher.rs b/crates/codex-plus-core/tests/watcher.rs index 81f80e3b..e89e0427 100644 --- a/crates/codex-plus-core/tests/watcher.rs +++ b/crates/codex-plus-core/tests/watcher.rs @@ -4,6 +4,10 @@ use codex_plus_core::watcher::{ process_ids_still_running, should_recover_stale_launcher, watcher_disabled_flag, }; +#[cfg(windows)] +use codex_plus_core::watcher::{find_codex_processes_from_snapshot, WindowsProcessInfo}; + + #[test] fn cdp_listening_returns_true_for_bound_loopback_port() { let listener = std::net::TcpListener::bind(("127.0.0.1", 0)).unwrap(); @@ -111,3 +115,82 @@ fn stop_wait_tracks_only_expected_process_ids() { vec![20, 30] ); } + +#[cfg(windows)] +#[test] +fn find_codex_processes_finds_local_install_with_capitial_c() { + let processes = [WindowsProcessInfo { + process_id: 42, + parent_process_id: 0, + exe_file: "Codex.exe".to_string(), + executable_path: Some(std::path::PathBuf::from( + r"D:\360Downloads\codexapp\app\Codex.exe", + )), + }]; + + assert_eq!(find_codex_processes_from_snapshot(&processes), vec![42]); +} + +#[cfg(windows)] +#[test] +fn find_codex_processes_finds_local_install_case_insensitively() { + let processes = [WindowsProcessInfo { + process_id: 43, + parent_process_id: 0, + exe_file: "codex.exe".to_string(), + executable_path: Some(std::path::PathBuf::from( + r"D:\360Downloads\codexapp\app\codex.exe", + )), + }]; + + assert_eq!(find_codex_processes_from_snapshot(&processes), vec![43]); +} + +#[cfg(windows)] +#[test] +fn find_codex_processes_combines_store_and_local_installs() { + let processes = [ + WindowsProcessInfo { + process_id: 11, + parent_process_id: 0, + exe_file: "Codex.exe".to_string(), + executable_path: Some(std::path::PathBuf::from( + r"C:\Program Files\WindowsApps\OpenAI.Codex_1.0.0.0_x64__abc\app\Codex.exe", + )), + }, + WindowsProcessInfo { + process_id: 42, + parent_process_id: 0, + exe_file: "Codex.exe".to_string(), + executable_path: Some(std::path::PathBuf::from( + r"D:\360Downloads\codexapp\app\Codex.exe", + )), + }, + ]; + + assert_eq!(find_codex_processes_from_snapshot(&processes), vec![11, 42]); +} + +#[cfg(windows)] +#[test] +fn find_codex_processes_ignores_unrelated_processes() { + let processes = [ + WindowsProcessInfo { + process_id: 10, + parent_process_id: 0, + exe_file: "notepad.exe".to_string(), + executable_path: Some(std::path::PathBuf::from(r"C:\Windows\notepad.exe")), + }, + WindowsProcessInfo { + process_id: 20, + parent_process_id: 0, + exe_file: "codex-plus-plus.exe".to_string(), + executable_path: Some(std::path::PathBuf::from( + r"D:\Programs\Codex++\codex-plus-plus.exe", + )), + }, + ]; + + assert!(find_codex_processes_from_snapshot(&processes).is_empty()); +} +