diff --git a/package.json b/package.json index ad2927c..975b520 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f602906..969817f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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" diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index d171a83..3e74972 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -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 { + Ok(state.kill_all().await) +} + pub fn get_binary_candidates(binary_name: &str) -> Vec { if cfg!(windows) && !binary_name.to_lowercase().ends_with(".exe") { vec![format!("{}.exe", binary_name), binary_name.to_string()] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b3d248c..837ea38 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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::(); + tauri::async_runtime::block_on(async { + pm.kill_all().await; + }); + } + tauri::RunEvent::WindowEvent { + event: tauri::WindowEvent::CloseRequested { .. }, + .. + } => { + let pm = app_handle.state::(); + tauri::async_runtime::block_on(async { + pm.kill_all().await; + }); + } + _ => {} + } + }); } diff --git a/src-tauri/src/process_manager.rs b/src-tauri/src/process_manager.rs index 594bd48..904925f 100644 --- a/src-tauri/src/process_manager.rs +++ b/src-tauri/src/process_manager.rs @@ -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()); + } + } } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0e5c631..ffdc4f1 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -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"