fix(tauri): ensure sidecar process is killed when app window is closed

Previously, the app only sent an HTTP shutdown request to the Python
sidecar with no timeout and no fallback. If the request failed, the
sidecar process became orphaned, causing memory leaks.

- Add 3-second timeout to the HTTP shutdown request
- Add fallback to force-kill the sidecar by PID (taskkill /F /T on
  Windows, kill -9 on Unix) after HTTP shutdown attempt
- Store child PID in AppState for use in both window close handler
  and Tauri command shutdown

Fixes #944
fix/sidecar-process-cleanup-on-close
zanllp 2026-04-08 21:06:25 +08:00
parent 47234f9a64
commit b408d253a1
1 changed files with 41 additions and 8 deletions

View File

@ -7,6 +7,8 @@ use std::fs::OpenOptions;
use std::io::prelude::*;
use std::io::Error;
use std::io::Write;
use std::process::Stdio;
use std::time::Duration;
use tauri::api::process::Command;
use tauri::api::process::CommandEvent;
use tauri::WindowEvent;
@ -20,6 +22,7 @@ fn greet(name: &str) -> String {
}
struct AppState {
port: u16,
child_pid: u32,
}
#[derive(serde::Serialize)]
struct AppConf {
@ -42,17 +45,44 @@ fn read_config_file(path: &str) -> Result<String, Error> {
Ok(contents)
}
fn shutdown_api_server(port: u16) {
fn shutdown_api_server(port: u16, child_pid: u32) {
let url = format!("http://127.0.0.1:{}/infinite_image_browsing/shutdown", port);
let res = reqwest::blocking::Client::new().post(url).send();
if let Err(e) = res {
eprintln!("{}", e);
let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(3))
.build();
if let Ok(client) = client {
let res = client.post(&url).send();
if let Err(e) = res {
eprintln!("HTTP shutdown request failed: {}", e);
}
}
// Fallback: force kill the process by PID to prevent orphaned processes
kill_process_by_pid(child_pid);
}
fn kill_process_by_pid(pid: u32) {
#[cfg(target_os = "windows")]
{
let _ = std::process::Command::new("taskkill")
.args(&["/F", "/T", "/PID", &pid.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
}
#[cfg(not(target_os = "windows"))]
{
let _ = std::process::Command::new("kill")
.args(&["-9", &pid.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
}
}
#[tauri::command]
fn shutdown_api_server_command(state: tauri::State<'_, AppState>) {
shutdown_api_server(state.port);
shutdown_api_server(state.port, state.child_pid);
}
fn main() {
@ -67,11 +97,14 @@ fn main() {
args.push("--sd_webui_dir");
args.push(&conf.sdwebui_dir);
}
let (mut rx, _child) = Command::new_sidecar("iib_api_server")
let (mut rx, child) = Command::new_sidecar("iib_api_server")
.expect("failed to create `iib_api_server` binary command")
.args(args)
.spawn()
.expect("Failed to spawn sidecar");
let child_pid = child.pid();
// child handle is intentionally dropped here; we use the PID to kill the process on shutdown
drop(child);
let log_file = OpenOptions::new()
.create(true)
.write(true)
@ -101,14 +134,14 @@ fn main() {
}
});
tauri::Builder::default()
.manage(AppState { port })
.manage(AppState { port, child_pid })
.invoke_handler(tauri::generate_handler![
greet,
get_tauri_conf,
shutdown_api_server_command
])
.on_window_event(move |event| match event.event() {
WindowEvent::CloseRequested { .. } => shutdown_api_server(port),
WindowEvent::CloseRequested { .. } => shutdown_api_server(port, child_pid),
_ => (),
})
.run(tauri::generate_context!())