fix(process): terminate all managed subprocesses on app/window exit (fixes #147, fixes #149) #153

Merged
gronod merged 1 commits from fix/process-kill-on-exit-147-149 into development 2026-08-30 17:40:40 +01:00
6 changed files with 96 additions and 5 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "iccery",
"private": true,
"version": "0.5.0",
"version": "0.5.1",
"type": "module",
"scripts": {
"fetch-argyll": "node scripts/fetch-argyll.mjs",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "iccery"
version = "0.5.0"
version = "0.5.1"
description = "Modern Printer Profiling UI frontend for ArgyllCMS"
authors = ["Gordon"]
edition = "2021"
+7
View File
@@ -30,6 +30,13 @@ pub async fn kill_process(
state.kill(&id).await
}
#[tauri::command]
pub async fn kill_all_processes(
state: State<'_, ProcessManager>,
) -> Result<usize, String> {
Ok(state.kill_all().await)
}
pub fn get_binary_candidates(binary_name: &str) -> Vec<String> {
if cfg!(windows) && !binary_name.to_lowercase().ends_with(".exe") {
vec![format!("{}.exe", binary_name), binary_name.to_string()]
+25 -2
View File
@@ -1,3 +1,5 @@
use tauri::Manager;
mod commands;
mod events;
mod print;
@@ -39,6 +41,7 @@ pub fn run() {
commands::select_directory,
commands::send_stdin,
commands::kill_process,
commands::kill_all_processes,
commands::resolve_binary,
commands::detect_instruments,
commands::get_profile_path,
@@ -66,6 +69,26 @@ pub fn run() {
settings::export_preset_json,
settings::import_preset_json,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|app_handle, event| {
match event {
tauri::RunEvent::Exit | tauri::RunEvent::ExitRequested { .. } => {
let pm = app_handle.state::<process_manager::ProcessManager>();
tauri::async_runtime::block_on(async {
pm.kill_all().await;
});
}
tauri::RunEvent::WindowEvent {
event: tauri::WindowEvent::CloseRequested { .. },
..
} => {
let pm = app_handle.state::<process_manager::ProcessManager>();
tauri::async_runtime::block_on(async {
pm.kill_all().await;
});
}
_ => {}
}
});
}
+61
View File
@@ -168,6 +168,38 @@ impl ProcessManager {
}
Err("Process not found".to_string())
}
/// Kills all currently managed subprocesses, closes their stdins, and signals termination.
/// Returns the number of processes signaled.
pub async fn kill_all(&self) -> usize {
// 1. Close and drop all stdin streams
{
let mut stdins = self.stdins.lock().await;
stdins.clear();
}
// 2. Extract and send kill signal to all active killer channels
let killers_to_signal = {
let mut killers = self.killers.lock().await;
let list: Vec<(String, tokio::sync::oneshot::Sender<()>)> = killers.drain().collect();
list
};
let count = killers_to_signal.len();
if count > 0 {
log::info!(target: "subprocess", "Terminating all managed subprocesses ({count} active)");
}
for (id, killer) in killers_to_signal {
log::info!(target: "subprocess", "Sending kill signal to subprocess '{id}'");
let _ = killer.send(());
}
// Give background tasks a brief moment to initiate child.start_kill()
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
count
}
}
#[cfg(test)]
@@ -183,4 +215,33 @@ mod tests {
assert!(!stdins.contains_key("test_proc"));
}
}
#[tokio::test]
async fn test_kill_all_cleans_maps() {
let pm = ProcessManager::new();
let (tx1, mut rx1) = tokio::sync::oneshot::channel::<()>();
let (tx2, mut rx2) = tokio::sync::oneshot::channel::<()>();
{
let mut killers = pm.killers.lock().await;
killers.insert("proc_1".to_string(), tx1);
killers.insert("proc_2".to_string(), tx2);
}
let killed_count = pm.kill_all().await;
assert_eq!(killed_count, 2);
// Verify channels received signal
assert!(rx1.try_recv().is_ok());
assert!(rx2.try_recv().is_ok());
// Verify maps are empty
{
let stdins = pm.stdins.lock().await;
let killers = pm.killers.lock().await;
assert!(stdins.is_empty());
assert!(killers.is_empty());
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ICCery",
"version": "0.5.0",
"version": "0.5.1",
"identifier": "com.gronod.iccery",
"build": {
"frontendDist": "../src"