diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0f4fb24..2d5c7c8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -127,6 +127,7 @@ objc2-app-kit = { version = "0.3.2", features = ["NSWindow", "NSApplication", "N winreg = "0.55" windows = { version = "0.62", features = [ "Win32_Foundation", + "Win32_System_Console", "Win32_System_ProcessStatus", "Win32_System_Threading", "Win32_System_Diagnostics_Debug", diff --git a/src-tauri/src/bin/donut_daemon.rs b/src-tauri/src/bin/donut_daemon.rs index fdca470..cc9256e 100644 --- a/src-tauri/src/bin/donut_daemon.rs +++ b/src-tauri/src/bin/donut_daemon.rs @@ -173,6 +173,34 @@ fn run_daemon() { // Store tray icon in Option - created after event loop starts let mut tray_icon: Option = None; + // Install signal handlers so SIGTERM/SIGINT trigger graceful shutdown + #[cfg(unix)] + unsafe { + extern "C" fn signal_handler(_sig: libc::c_int) { + SHOULD_QUIT.store(true, std::sync::atomic::Ordering::SeqCst); + } + libc::signal(libc::SIGTERM, signal_handler as libc::sighandler_t); + libc::signal(libc::SIGINT, signal_handler as libc::sighandler_t); + } + + #[cfg(windows)] + unsafe { + use windows::Win32::System::Console::{SetConsoleCtrlHandler, PHANDLER_ROUTINE}; + + unsafe extern "system" fn ctrl_handler(_ctrl_type: u32) -> windows::Win32::Foundation::BOOL { + SHOULD_QUIT.store(true, std::sync::atomic::Ordering::SeqCst); + windows::Win32::Foundation::TRUE + } + + let _ = SetConsoleCtrlHandler( + Some(std::mem::transmute::< + unsafe extern "system" fn(u32) -> windows::Win32::Foundation::BOOL, + PHANDLER_ROUTINE, + >(ctrl_handler)), + true, + ); + } + // Run the event loop event_loop.run(move |event, _, control_flow| { // Use WaitUntil to check for menu events periodically while staying low on CPU @@ -264,25 +292,40 @@ fn run_daemon() { } fn stop_daemon() { + let state_path = get_state_path(); let state = read_state(); if let Some(pid) = state.daemon_pid { - #[cfg(unix)] - { - unsafe { - libc::kill(pid as i32, libc::SIGTERM); - } - eprintln!("Sent stop signal to daemon (PID {})", pid); - } - + // On Windows, taskkill /F kills instantly with no handler, so kill GUI first #[cfg(windows)] { use std::process::Command; + + // Read gui_pid from state file and kill it first + if let Ok(content) = fs::read_to_string(&state_path) { + if let Ok(val) = serde_json::from_str::(&content) { + if let Some(gui_pid) = val.get("gui_pid").and_then(|v| v.as_u64()) { + let _ = Command::new("taskkill") + .args(["/PID", &gui_pid.to_string(), "/F"]) + .output(); + } + } + } + let _ = Command::new("taskkill") .args(["/PID", &pid.to_string(), "/F"]) .output(); eprintln!("Sent stop signal to daemon (PID {})", pid); } + + #[cfg(unix)] + { + let _ = &state_path; // suppress unused warning on unix + unsafe { + libc::kill(pid as i32, libc::SIGTERM); + } + eprintln!("Sent stop signal to daemon (PID {})", pid); + } } else { eprintln!("Daemon is not running"); } diff --git a/src-tauri/src/daemon/tray.rs b/src-tauri/src/daemon/tray.rs index 701dcc4..43781b1 100644 --- a/src-tauri/src/daemon/tray.rs +++ b/src-tauri/src/daemon/tray.rs @@ -127,9 +127,49 @@ pub fn activate_gui() { } } +fn read_gui_pid() -> Option { + let path = super::autostart::get_data_dir()?.join("daemon-state.json"); + let content = std::fs::read_to_string(path).ok()?; + let val: serde_json::Value = serde_json::from_str(&content).ok()?; + val.get("gui_pid")?.as_u64().map(|p| p as u32) +} + +fn kill_gui_by_pid() -> bool { + let Some(pid) = read_gui_pid() else { + return false; + }; + + #[cfg(unix)] + { + let ret = unsafe { libc::kill(pid as i32, libc::SIGTERM) }; + ret == 0 + } + + #[cfg(windows)] + { + Command::new("taskkill") + .args(["/PID", &pid.to_string(), "/F"]) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + } + + #[cfg(not(any(unix, windows)))] + { + false + } +} + pub fn quit_gui() { log::info!("[daemon] Quitting GUI..."); + if kill_gui_by_pid() { + log::info!("[daemon] GUI killed by PID"); + return; + } + + log::info!("[daemon] PID-based kill failed, falling back to name-based kill"); + #[cfg(target_os = "macos")] { let _ = Command::new("osascript") diff --git a/src-tauri/src/daemon_spawn.rs b/src-tauri/src/daemon_spawn.rs index f795aa8..8cbde60 100644 --- a/src-tauri/src/daemon_spawn.rs +++ b/src-tauri/src/daemon_spawn.rs @@ -313,3 +313,26 @@ pub fn ensure_daemon_running() -> Result<(), String> { } Ok(()) } + +pub fn register_gui_pid() { + let path = get_state_path(); + let mut val: serde_json::Value = if path.exists() { + fs::read_to_string(&path) + .ok() + .and_then(|c| serde_json::from_str(&c).ok()) + .unwrap_or_else(|| serde_json::json!({})) + } else { + serde_json::json!({}) + }; + + if let Some(obj) = val.as_object_mut() { + obj.insert( + "gui_pid".to_string(), + serde_json::Value::Number(std::process::id().into()), + ); + } + + if let Ok(content) = serde_json::to_string_pretty(&val) { + let _ = fs::write(&path, content); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6c8c74f..b17446c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -764,13 +764,16 @@ pub fn run() { log::warn!("Failed to start daemon: {e}"); } + // Register this GUI's PID in daemon state so the daemon can kill us directly + daemon_spawn::register_gui_pid(); + // Monitor daemon health - quit GUI if daemon dies let app_handle_daemon = app.handle().clone(); tauri::async_runtime::spawn(async move { // Give the daemon time to fully start - tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; - let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5)); + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1)); interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); loop {