From e4f4e0e301748a294110895f09a3bdc65794c9ff Mon Sep 17 00:00:00 2001 From: Stanley5249 Date: Sat, 25 Jul 2026 03:00:49 +0800 Subject: [PATCH 1/2] fix(tray): distinguish the Windows GUI from the CLI by exact name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI binary `openlogi.exe` case-insensitively equals the GUI's `OpenLogi.exe`, so in a cargo target dir — the only layout holding both — the tray's Show/Quit mistook a transient CLI run for the GUI, and `spawn_gui` launched the CLI (`dir.join("OpenLogi.exe").exists()` resolves to it on the case-insensitive filesystem) instead of the window. Match `OpenLogi.exe` case-sensitively so the lowercase CLI no longer counts as the GUI, and probe the unambiguous `openlogi-gui.exe` first when spawning. --- crates/openlogi-agent/src/tray_windows.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/openlogi-agent/src/tray_windows.rs b/crates/openlogi-agent/src/tray_windows.rs index cbe1fb40..f211d34e 100644 --- a/crates/openlogi-agent/src/tray_windows.rs +++ b/crates/openlogi-agent/src/tray_windows.rs @@ -290,6 +290,12 @@ fn open_or_focus_gui() { /// same-user filter keeps other sessions (fast user switching) out of /// Show/Quit — their windows are invisible to `EnumWindows` and their /// processes unkillable anyway, but don't even consider them. +/// +/// The `OpenLogi.exe` match is case-*sensitive*: the CLI binary is +/// `openlogi.exe`, which case-insensitively equals the GUI's `OpenLogi.exe`. +/// The two share a name on the case-insensitive filesystem (only the dev +/// target dir ever holds both), so an `eq_ignore_ascii_case` match would take +/// a transient CLI invocation for the GUI and then fail to find its window. fn gui_pids() -> Vec { use sysinfo::{Pid, Process, ProcessesToUpdate, System}; let mut system = System::new(); @@ -302,8 +308,7 @@ fn gui_pids() -> Vec { .values() .filter(|p| { let name = p.name().to_string_lossy(); - (name.eq_ignore_ascii_case("OpenLogi.exe") - || name.eq_ignore_ascii_case("openlogi-gui.exe")) + (name == "OpenLogi.exe" || name.eq_ignore_ascii_case("openlogi-gui.exe")) && (own_user.is_none() || p.user_id() == own_user) }) .map(|p| p.pid().as_u32()) @@ -354,9 +359,13 @@ fn spawn_gui() { return; }; let Some(dir) = exe.parent() else { return }; - // Installed/portable layout first (the product name), then the cargo - // target-dir name (dev) — the reverse of the GUI's agent sibling lookup. - let gui = ["OpenLogi.exe", "openlogi-gui.exe"] + // Dev target dir first: it holds both `openlogi.exe` (CLI) and + // `openlogi-gui.exe`, and the CLI shares `OpenLogi.exe`'s name on the + // case-insensitive filesystem — so `dir.join("OpenLogi.exe").exists()` + // there resolves to the CLI and would launch it. Probing the unambiguous + // `openlogi-gui.exe` first avoids that; the installed layout has only + // `OpenLogi.exe` and falls through to it. + let gui = ["openlogi-gui.exe", "OpenLogi.exe"] .iter() .map(|name| dir.join(name)) .find(|p| p.exists()); From 0a202a1052186b30a0ff67248f0a47f42d13bfdc Mon Sep 17 00:00:00 2001 From: Stanley5249 Date: Sun, 26 Jul 2026 02:47:32 +0800 Subject: [PATCH 2/2] test(tray): pin that the Windows CLI binary is not the GUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GUI/CLI discrimination is one string comparison, and its failure mode is silent: Show would spawn a duplicate GUI that immediately exits on the singleton lock, so nothing visible happens. Extract the predicate out of `gui_pids`'s filter and assert all three names, so a regression fails a local `cargo test` on Windows instead of only showing up as a dead tray menu. CI does not run it: the module is cfg'd to Windows and the only Windows job is clippy, which compiles the test but never executes it. Also record why the two names are matched differently — `OpenLogi.exe` exactly (the CLI `openlogi.exe` collides with it case-insensitively) and `openlogi-gui.exe` case-insensitively (nothing else shares that name). --- crates/openlogi-agent/src/tray_windows.rs | 30 +++++++++++++++++------ 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/crates/openlogi-agent/src/tray_windows.rs b/crates/openlogi-agent/src/tray_windows.rs index f211d34e..1a8ae7db 100644 --- a/crates/openlogi-agent/src/tray_windows.rs +++ b/crates/openlogi-agent/src/tray_windows.rs @@ -290,12 +290,6 @@ fn open_or_focus_gui() { /// same-user filter keeps other sessions (fast user switching) out of /// Show/Quit — their windows are invisible to `EnumWindows` and their /// processes unkillable anyway, but don't even consider them. -/// -/// The `OpenLogi.exe` match is case-*sensitive*: the CLI binary is -/// `openlogi.exe`, which case-insensitively equals the GUI's `OpenLogi.exe`. -/// The two share a name on the case-insensitive filesystem (only the dev -/// target dir ever holds both), so an `eq_ignore_ascii_case` match would take -/// a transient CLI invocation for the GUI and then fail to find its window. fn gui_pids() -> Vec { use sysinfo::{Pid, Process, ProcessesToUpdate, System}; let mut system = System::new(); @@ -307,14 +301,22 @@ fn gui_pids() -> Vec { .processes() .values() .filter(|p| { - let name = p.name().to_string_lossy(); - (name == "OpenLogi.exe" || name.eq_ignore_ascii_case("openlogi-gui.exe")) + is_gui_process_name(&p.name().to_string_lossy()) && (own_user.is_none() || p.user_id() == own_user) }) .map(|p| p.pid().as_u32()) .collect() } +/// Whether a process image name is one of the GUI binaries. +/// +/// `OpenLogi.exe` is matched case-*sensitively*: the CLI is `openlogi.exe`, +/// which `eq_ignore_ascii_case` would accept, and the dev target dir holds +/// both. Windows reports image names with their on-disk case, so this holds. +fn is_gui_process_name(name: &str) -> bool { + name == "OpenLogi.exe" || name.eq_ignore_ascii_case("openlogi-gui.exe") +} + /// Bring the first visible top-level window owned by one of `pids` to the /// foreground, restoring it if minimized. Returns whether one was found. fn focus_window_of(pids: &[u32]) -> bool { @@ -412,3 +414,15 @@ fn quit(hwnd: HWND) { fn wide(s: &str) -> Vec { s.encode_utf16().chain(std::iter::once(0)).collect() } + +#[cfg(test)] +mod tests { + use super::is_gui_process_name; + + #[test] + fn the_cli_binary_is_not_the_gui() { + assert!(is_gui_process_name("OpenLogi.exe")); + assert!(is_gui_process_name("openlogi-gui.exe")); + assert!(!is_gui_process_name("openlogi.exe")); // the CLI + } +}