From 88d41f342b606a771d5578792b6c5fb3aa5064a2 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Mon, 26 Jan 2026 17:02:00 -0800 Subject: [PATCH] Implement server running preference and cleanup on exit - Added `setKeepServerRunning` function to manage server persistence on app close. - Integrated server running preference in `ConnectionForm` and synced settings on app startup. - Enhanced server management in `main.rs` to handle orphaned processes based on user preference. - Improved cleanup logic to ensure proper termination of server processes when not set to keep running. --- app/src/App.tsx | 13 +- .../ServerSettings/ConnectionForm.tsx | 4 + app/src/lib/tauri.ts | 15 ++ tauri/src-tauri/gen/Assets.car | Bin 3847048 -> 3847048 bytes tauri/src-tauri/src/main.rs | 198 +++++++++++++++++- 5 files changed, 222 insertions(+), 8 deletions(-) diff --git a/app/src/App.tsx b/app/src/App.tsx index 623f458a..108d5f4d 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -15,7 +15,8 @@ import { Toaster } from '@/components/ui/toaster'; import { ProfileList } from '@/components/VoiceProfiles/ProfileList'; import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast'; import { MODEL_DISPLAY_NAMES, useRestoreActiveTasks } from '@/lib/hooks/useRestoreActiveTasks'; -import { isMacOS, isTauri, setupWindowCloseHandler, startServer } from '@/lib/tauri'; +import { isMacOS, isTauri, setupWindowCloseHandler, startServer, setKeepServerRunning } from '@/lib/tauri'; +import { useServerStore } from '@/stores/serverStore'; // Track if server is starting to prevent duplicate starts let serverStarting = false; @@ -51,6 +52,16 @@ function App() { // Monitor active downloads/generations and show toasts for them const activeDownloads = useRestoreActiveTasks(); + // Sync stored setting to Rust on startup + useEffect(() => { + if (isTauri()) { + const keepRunning = useServerStore.getState().keepServerRunningOnClose; + setKeepServerRunning(keepRunning).catch((error) => { + console.error('Failed to sync initial setting to Rust:', error); + }); + } + }, []); + // Setup window close handler and auto-start server when running in Tauri (production only) useEffect(() => { if (!isTauri()) { diff --git a/app/src/components/ServerSettings/ConnectionForm.tsx b/app/src/components/ServerSettings/ConnectionForm.tsx index 83526a74..c647bb81 100644 --- a/app/src/components/ServerSettings/ConnectionForm.tsx +++ b/app/src/components/ServerSettings/ConnectionForm.tsx @@ -17,6 +17,7 @@ import { Input } from '@/components/ui/input'; import { Checkbox } from '@/components/ui/checkbox'; import { useToast } from '@/components/ui/use-toast'; import { useServerStore } from '@/stores/serverStore'; +import { setKeepServerRunning } from '@/lib/tauri'; const connectionSchema = z.object({ serverUrl: z.string().url('Please enter a valid URL'), @@ -88,6 +89,9 @@ export function ConnectionForm() { checked={keepServerRunningOnClose} onCheckedChange={(checked: boolean) => { setKeepServerRunningOnClose(checked); + setKeepServerRunning(checked).catch((error) => { + console.error('Failed to sync setting to Rust:', error); + }); toast({ title: 'Setting updated', description: checked diff --git a/app/src/lib/tauri.ts b/app/src/lib/tauri.ts index 5ad17720..3e6e9795 100644 --- a/app/src/lib/tauri.ts +++ b/app/src/lib/tauri.ts @@ -54,6 +54,21 @@ export async function stopServer(): Promise { } } +/** + * Set whether the server should keep running when the app closes (Tauri only) + */ +export async function setKeepServerRunning(keepRunning: boolean): Promise { + if (!isTauri()) { + return; + } + + try { + await invoke('set_keep_server_running', { keepRunning }); + } catch (error) { + console.error('Failed to set keep server running setting:', error); + } +} + /** * Setup window close handler to check setting and stop server if needed */ diff --git a/tauri/src-tauri/gen/Assets.car b/tauri/src-tauri/gen/Assets.car index c0a2c1f2faa34468fedbc2a7a21bedd1e00909b3..00be322ed334f71fc1b01a81dfd3d64d44394de8 100644 GIT binary patch delta 833 zcmZwGJ5N+W6bJBKR%Cgu_&`8VQ1N}(x$_0J(K=|JX~lQ|L0k$_saH%R%xWuDFj0-ri_9)r;sa8fho=kE0xP>v|wRP zmCggbiS32vbD6z!%2CA}3Mz_HE};!6ZGmuiWBb_t><*mza*jGO_8+hzMg|5XhGeZGM)pc9 zNF7cGegabY0Ss(J45+Yl<*jI^aHL0;JiMfZ&kY}Kz+2O;e>?nO{!HaY7dB9^EO?ceQX(-|n#f|(Aq&FE zNQseELNk8$sgks}nN(VDy?Xg#f3x#$>;_|3hbd^lC1}EBn6AdIRWx&x8H!eOrad{P zyv|sV%Cj<1Q5a<@6MnKsNG1Hgo^CbFTkjjWvBbSGj#)yQ3T4S`)P^A}3i2hlDw_-mS)|wc5xvN$Io|OqU*$ zDb%~<3zmqxP;e?krYC}2=7Kms!Ii<@ZVP5$7Up0c7GM#U;0i3mRk#M%;RdwfCY)ug zLI-X^7uMi5tiuLu!X3B^_n-&&;Q>5^NAS4X+kJ9+n)_r*Ba>jHl4Q7a&OC|8Qc;v% zIIc9AiJ>e-CC{&`GdS3J3JkWO4;%zY$UuRv4t7lSD_1&YQCMO!XcWeX_d%Q(7nHb^ t!V99wLY%g{c!z_dK7j>S9rd>xZs diff --git a/tauri/src-tauri/src/main.rs b/tauri/src-tauri/src/main.rs index 667ee952..28a6b30d 100644 --- a/tauri/src-tauri/src/main.rs +++ b/tauri/src-tauri/src/main.rs @@ -4,12 +4,14 @@ mod audio_capture; use std::sync::Mutex; -use tauri::{command, State, Manager, WindowEvent, Emitter, Listener}; +use tauri::{command, State, Manager, WindowEvent, Emitter, Listener, RunEvent}; use tauri_plugin_shell::ShellExt; use tokio::sync::mpsc; struct ServerState { child: Mutex>, + server_pid: Mutex>, + keep_running_on_close: Mutex, } #[command] @@ -18,11 +20,66 @@ async fn start_server( state: State<'_, ServerState>, remote: Option, ) -> Result { - // Check if server is already running + // Check if server is already running (managed by this app instance) if state.child.lock().unwrap().is_some() { return Ok("Server already running on http://localhost:8000".to_string()); } + // If keep_running_on_close is false, kill any orphaned server from previous session + let keep_running = *state.keep_running_on_close.lock().unwrap(); + if !keep_running { + #[cfg(unix)] + { + use std::process::Command; + // Find any process listening on port 8000 + if let Ok(output) = Command::new("lsof") + .args(["-ti", ":8000"]) + .output() + { + let pids = String::from_utf8_lossy(&output.stdout); + for pid_str in pids.lines() { + if let Ok(pid) = pid_str.trim().parse::() { + println!("Found orphaned server on port 8000 (PID: {}), killing it...", pid); + // Kill the process group + let _ = Command::new("kill") + .args(["-9", "--", &format!("-{}", pid)]) + .output(); + let _ = Command::new("kill") + .args(["-9", &pid.to_string()]) + .output(); + } + } + } + } + + #[cfg(windows)] + { + use std::process::Command; + // On Windows, find and kill process on port 8000 + if let Ok(output) = Command::new("netstat") + .args(["-ano"]) + .output() + { + let output_str = String::from_utf8_lossy(&output.stdout); + for line in output_str.lines() { + if line.contains(":8000") && line.contains("LISTENING") { + if let Some(pid_str) = line.split_whitespace().last() { + if let Ok(pid) = pid_str.parse::() { + println!("Found orphaned server on port 8000 (PID: {}), killing it...", pid); + let _ = Command::new("taskkill") + .args(["/PID", &pid.to_string(), "/T", "/F"]) + .output(); + } + } + } + } + } + } + + // Brief wait for port to be released + std::thread::sleep(std::time::Duration::from_millis(200)); + } + // Get app data directory let data_dir = app .path() @@ -77,7 +134,9 @@ async fn start_server( println!("Server process spawned, waiting for ready signal..."); println!("================================================================="); - // Store child process + // Store child process and its PID for process group killing + let pid = child.pid(); + *state.server_pid.lock().unwrap() = Some(pid); *state.child.lock().unwrap() = Some(child); // Wait for server to be ready by listening for startup log @@ -161,12 +220,50 @@ async fn start_server( #[command] async fn stop_server(state: State<'_, ServerState>) -> Result<(), String> { - if let Some(child) = state.child.lock().unwrap().take() { - child.kill().map_err(|e| format!("Failed to kill: {}", e))?; + let pid = state.server_pid.lock().unwrap().take(); + let _child = state.child.lock().unwrap().take(); + + if let Some(pid) = pid { + println!("stop_server: Killing server process group with PID: {}", pid); + + #[cfg(unix)] + { + use std::process::Command; + // Kill process group with SIGTERM first + let _ = Command::new("kill") + .args(["-TERM", "--", &format!("-{}", pid)]) + .output(); + + // Brief wait then force kill + std::thread::sleep(std::time::Duration::from_millis(100)); + + let _ = Command::new("kill") + .args(["-9", "--", &format!("-{}", pid)]) + .output(); + let _ = Command::new("kill") + .args(["-9", &pid.to_string()]) + .output(); + } + + #[cfg(windows)] + { + use std::process::Command; + let _ = Command::new("taskkill") + .args(["/PID", &pid.to_string(), "/T", "/F"]) + .output(); + } + + println!("stop_server: Process group kill completed"); } + Ok(()) } +#[command] +fn set_keep_server_running(state: State<'_, ServerState>, keep_running: bool) { + *state.keep_running_on_close.lock().unwrap() = keep_running; +} + #[command] async fn start_system_audio_capture( state: State<'_, audio_capture::AudioCaptureState>, @@ -195,6 +292,8 @@ pub fn run() { .plugin(tauri_plugin_shell::init()) .manage(ServerState { child: Mutex::new(None), + server_pid: Mutex::new(None), + keep_running_on_close: Mutex::new(false), }) .manage(audio_capture::AudioCaptureState::new()) .setup(|app| { @@ -216,6 +315,7 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ start_server, stop_server, + set_keep_server_running, start_system_audio_capture, stop_system_audio_capture, is_system_audio_supported @@ -264,8 +364,92 @@ pub fn run() { }); } }) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + .build(tauri::generate_context!()) + .expect("error while building tauri application") + .run(|app, event| { + match &event { + RunEvent::Exit => { + println!("================================================================="); + println!("RunEvent::Exit received - checking server cleanup"); + let state = app.state::(); + let keep_running = *state.keep_running_on_close.lock().unwrap(); + println!("keep_running_on_close = {}", keep_running); + + if !keep_running { + // Get the stored PID for process group killing + let pid = state.server_pid.lock().unwrap().take(); + // Also take the child to clean up + let _child = state.child.lock().unwrap().take(); + + if let Some(pid) = pid { + println!("Killing server process group with PID: {}", pid); + + // Kill the entire process group on Unix systems + // Using negative PID sends signal to all processes in the group + #[cfg(unix)] + { + use std::process::Command; + // First try SIGTERM to the process group + let pgid_kill = Command::new("kill") + .args(["-TERM", "--", &format!("-{}", pid)]) + .output(); + + match pgid_kill { + Ok(output) => { + if output.status.success() { + println!("SIGTERM sent to process group -{}", pid); + } else { + // Process group kill failed, try direct kill + println!("Process group kill failed, trying direct kill"); + let _ = Command::new("kill") + .args(["-TERM", &pid.to_string()]) + .output(); + } + } + Err(e) => { + eprintln!("Failed to execute kill command: {}", e); + } + } + + // Give it a moment, then force kill if needed + std::thread::sleep(std::time::Duration::from_millis(100)); + + // Force kill with SIGKILL + let _ = Command::new("kill") + .args(["-9", "--", &format!("-{}", pid)]) + .output(); + let _ = Command::new("kill") + .args(["-9", &pid.to_string()]) + .output(); + + println!("Server process group kill completed"); + } + + #[cfg(windows)] + { + // On Windows, use taskkill with /T to kill child processes + use std::process::Command; + let _ = Command::new("taskkill") + .args(["/PID", &pid.to_string(), "/T", "/F"]) + .output(); + println!("Server process tree kill completed"); + } + } else { + println!("No server PID found (already stopped or never started)"); + } + } else { + println!("Keeping server running per user setting"); + } + println!("================================================================="); + } + RunEvent::ExitRequested { api, .. } => { + println!("RunEvent::ExitRequested received"); + // Don't prevent exit, just log it + let _ = api; + } + _ => {} + } + }); } fn main() {