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.
This commit is contained in:
Jamie Pine
2026-01-26 17:02:00 -08:00
parent b1cf7926c7
commit 88d41f342b
5 changed files with 222 additions and 8 deletions
+12 -1
View File
@@ -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()) {
@@ -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
+15
View File
@@ -54,6 +54,21 @@ export async function stopServer(): Promise<void> {
}
}
/**
* Set whether the server should keep running when the app closes (Tauri only)
*/
export async function setKeepServerRunning(keepRunning: boolean): Promise<void> {
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
*/
Binary file not shown.
+191 -7
View File
@@ -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<Option<tauri_plugin_shell::process::CommandChild>>,
server_pid: Mutex<Option<u32>>,
keep_running_on_close: Mutex<bool>,
}
#[command]
@@ -18,11 +20,66 @@ async fn start_server(
state: State<'_, ServerState>,
remote: Option<bool>,
) -> Result<String, String> {
// 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::<i32>() {
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::<u32>() {
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::<ServerState>();
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() {