refactor: better daemon management

This commit is contained in:
zhom
2026-02-22 03:03:41 +04:00
parent 98798b83df
commit ddc2657165
5 changed files with 120 additions and 10 deletions
+1
View File
@@ -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",
+51 -8
View File
@@ -173,6 +173,34 @@ fn run_daemon() {
// Store tray icon in Option - created after event loop starts
let mut tray_icon: Option<TrayIcon> = 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::<serde_json::Value>(&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");
}
+40
View File
@@ -127,9 +127,49 @@ pub fn activate_gui() {
}
}
fn read_gui_pid() -> Option<u32> {
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")
+23
View File
@@ -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);
}
}
+5 -2
View File
@@ -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 {