diff --git a/README.md b/README.md index b015510..ff6fc9a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > Modern, cross-platform native desktop application for printer profiling, powered by ArgyllCMS. -[![Release](https://img.shields.io/badge/version-v0.2.0-blue.svg)](https://git.i3omb.com/gronod/ICCery) +[![Release](https://img.shields.io/badge/version-v0.2.1-blue.svg)](https://git.i3omb.com/gronod/ICCery) [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux-lightgrey.svg)](https://git.i3omb.com/gronod/ICCery) [![Framework](https://img.shields.io/badge/framework-Tauri%20v2%20%2B%20Rust-orange.svg)](https://tauri.app) [![License](https://img.shields.io/badge/license-Proprietary%20%2F%20EULA-blue.svg)](LICENCE.md) diff --git a/ROADMAP.md b/ROADMAP.md index a9b2c5d..1f35140 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -9,7 +9,7 @@ This document outlines the architectural roadmap, completed milestones, and upco ICCery is a native, cross-platform desktop application built with: - **Backend**: Rust + Tauri v2, managing asynchronous process pipes, native printer devmode configurations (Windows GDI & Linux CUPS), and filesystem operations. - **Frontend**: Vanilla JS (ES Modules) + HTML5/CSS3 with a modern dark theme and responsive layout. -- **Visualisation**: Three.js WebGL engine for 3D CIELAB color gamut volumes and sRGB reference comparisons. +- **Visualization**: Three.js WebGL engine for 3D CIELAB color gamut volumes and sRGB reference comparisons. - **Engine**: ArgyllCMS command-line utilities orchestrated over isolated standard stream IPC (`stdin`, `stdout`, `stderr`). --- @@ -51,6 +51,10 @@ ICCery is a native, cross-platform desktop application built with: - [x] Eliminated all hardcoded placeholder and fallback crutches across JavaScript modules. - [x] Comprehensive documentation, release testing, and packaging automation. +### Hotfix Release (`v0.2.1`) +- [x] Resolved P0 process manager deadlock and premature stdin pipe closure affecting interactive `chartread` instrument workflows. +- [x] Decoupled `ChildStdin` mutex management from child process wait/reap tasks. + --- ## 3. Future Roadmap diff --git a/package.json b/package.json index 794e7fc..872db3e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "iccery", "private": true, - "version": "0.2.0", + "version": "0.2.1", "type": "module", "scripts": { "tauri": "tauri" @@ -9,4 +9,4 @@ "devDependencies": { "@tauri-apps/cli": "^2" } -} \ No newline at end of file +} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6c9a4d0..856512c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iccery" -version = "0.2.0" +version = "0.2.1" description = "Modern Printer Profiling UI frontend for ArgyllCMS" authors = ["Gordon"] edition = "2021" @@ -21,7 +21,7 @@ tauri-build = { version = "2", features = [] } tauri = { version = "2", features = [] } serde = { version = "1", features = ["derive"] } serde_json = "1" -tokio = { version = "1", features = ["process", "io-util", "sync", "time", "rt-multi-thread"] } +tokio = { version = "1", features = ["process", "io-util", "sync", "time", "rt-multi-thread", "macros"] } tauri-plugin-dialog = "2.7.2" base64 = "0.22" image = { version = "0.25", default-features = false, features = ["png", "tiff"] } diff --git a/src-tauri/src/process_manager.rs b/src-tauri/src/process_manager.rs index ed31858..e3b8ad7 100644 --- a/src-tauri/src/process_manager.rs +++ b/src-tauri/src/process_manager.rs @@ -3,19 +3,21 @@ use std::process::Stdio; use std::sync::Arc; use tauri::AppHandle; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; -use tokio::process::{Child, Command}; +use tokio::process::{ChildStdin, Command}; use tokio::sync::Mutex; use crate::events::{emit_error, emit_stderr, emit_stdout}; pub struct ProcessManager { - processes: Arc>>>>, + stdins: Arc>>>>, + killers: Arc>>>, } impl ProcessManager { pub fn new() -> Self { Self { - processes: Arc::new(Mutex::new(HashMap::new())), + stdins: Arc::new(Mutex::new(HashMap::new())), + killers: Arc::new(Mutex::new(HashMap::new())), } } @@ -46,6 +48,7 @@ impl ProcessManager { Ok(mut child) => { let stdout = child.stdout.take().expect("Failed to open stdout"); let stderr = child.stderr.take().expect("Failed to open stderr"); + let stdin = child.stdin.take().expect("Failed to open stdin"); let id_clone = id.clone(); let app_clone = app.clone(); @@ -71,28 +74,43 @@ impl ProcessManager { } }); - let child_arc = Arc::new(Mutex::new(child)); + let stdin_arc = Arc::new(Mutex::new(stdin)); + let (kill_tx, kill_rx) = tokio::sync::oneshot::channel::<()>(); + { - let mut processes = self.processes.lock().await; - processes.insert(id.clone(), child_arc.clone()); + let mut stdins = self.stdins.lock().await; + stdins.insert(id.clone(), stdin_arc); + let mut killers = self.killers.lock().await; + killers.insert(id.clone(), kill_tx); } let id_clone_exit = id.clone(); let app_clone_exit = app.clone(); - let processes_clone = self.processes.clone(); + let stdins_clone = self.stdins.clone(); + let killers_clone = self.killers.clone(); tokio::spawn(async move { - let exit_code = { - let mut c = child_arc.lock().await; - match c.wait().await { - Ok(status) => status.code().unwrap_or(0), - Err(_) => 1, + let exit_code = tokio::select! { + res = child.wait() => { + match res { + Ok(status) => status.code().unwrap_or(0), + Err(_) => 1, + } + } + _ = kill_rx => { + let _ = child.start_kill(); + match child.wait().await { + Ok(status) => status.code().unwrap_or(1), + Err(_) => 1, + } } }; - // Reap child from process manager map upon exit + // Reap child from process manager maps upon exit { - let mut processes = processes_clone.lock().await; - processes.remove(&id_clone_exit); + let mut stdins = stdins_clone.lock().await; + stdins.remove(&id_clone_exit); + let mut killers = killers_clone.lock().await; + killers.remove(&id_clone_exit); } crate::events::emit_exit(&app_clone_exit, &id_clone_exit, exit_code); @@ -108,23 +126,31 @@ impl ProcessManager { } pub async fn send_stdin(&self, id: &str, input: &str) -> Result<(), String> { - let processes = self.processes.lock().await; - if let Some(child_arc) = processes.get(id) { - let mut child = child_arc.lock().await; - if let Some(stdin) = child.stdin.as_mut() { - stdin.write_all(input.as_bytes()).await.map_err(|e| e.to_string())?; - stdin.flush().await.map_err(|e| e.to_string())?; - return Ok(()); - } + let stdin_arc = { + let stdins = self.stdins.lock().await; + stdins.get(id).cloned() + }; + if let Some(stdin_arc) = stdin_arc { + let mut stdin = stdin_arc.lock().await; + stdin.write_all(input.as_bytes()).await.map_err(|e| e.to_string())?; + stdin.flush().await.map_err(|e| e.to_string())?; + return Ok(()); } Err("Process not found or stdin not available".to_string()) } pub async fn kill(&self, id: &str) -> Result<(), String> { - let mut processes = self.processes.lock().await; - if let Some(child_arc) = processes.remove(id) { - let mut child = child_arc.lock().await; - child.kill().await.map_err(|e| e.to_string())?; + // Drop and close stdin immediately + { + let mut stdins = self.stdins.lock().await; + stdins.remove(id); + } + let killer = { + let mut killers = self.killers.lock().await; + killers.remove(id) + }; + if let Some(kill_tx) = killer { + let _ = kill_tx.send(()); return Ok(()); } Err("Process not found".to_string()) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index d5b06f5..087b01f 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.2.0", + "version": "0.2.1", "identifier": "com.gronod.iccery", "build": { "frontendDist": "../src" @@ -47,4 +47,4 @@ } } } -} \ No newline at end of file +}