fix(process): resolve process manager deadlock on chartread & release v0.2.1 #83

Merged
gronod merged 7 commits from development into main 2026-08-25 13:07:38 +01:00
6 changed files with 65 additions and 35 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
> Modern, cross-platform native desktop application for printer profiling, powered by ArgyllCMS. > 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) [![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) [![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) [![License](https://img.shields.io/badge/license-Proprietary%20%2F%20EULA-blue.svg)](LICENCE.md)
+5 -1
View File
@@ -9,7 +9,7 @@ This document outlines the architectural roadmap, completed milestones, and upco
ICCery is a native, cross-platform desktop application built with: 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. - **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. - **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`). - **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] Eliminated all hardcoded placeholder and fallback crutches across JavaScript modules.
- [x] Comprehensive documentation, release testing, and packaging automation. - [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 ## 3. Future Roadmap
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "iccery", "name": "iccery",
"private": true, "private": true,
"version": "0.2.0", "version": "0.2.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"tauri": "tauri" "tauri": "tauri"
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "iccery" name = "iccery"
version = "0.2.0" version = "0.2.1"
description = "Modern Printer Profiling UI frontend for ArgyllCMS" description = "Modern Printer Profiling UI frontend for ArgyllCMS"
authors = ["Gordon"] authors = ["Gordon"]
edition = "2021" edition = "2021"
@@ -21,7 +21,7 @@ tauri-build = { version = "2", features = [] }
tauri = { version = "2", features = [] } tauri = { version = "2", features = [] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" 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" tauri-plugin-dialog = "2.7.2"
base64 = "0.22" base64 = "0.22"
image = { version = "0.25", default-features = false, features = ["png", "tiff"] } image = { version = "0.25", default-features = false, features = ["png", "tiff"] }
+53 -27
View File
@@ -3,19 +3,21 @@ use std::process::Stdio;
use std::sync::Arc; use std::sync::Arc;
use tauri::AppHandle; use tauri::AppHandle;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::{Child, Command}; use tokio::process::{ChildStdin, Command};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::events::{emit_error, emit_stderr, emit_stdout}; use crate::events::{emit_error, emit_stderr, emit_stdout};
pub struct ProcessManager { pub struct ProcessManager {
processes: Arc<Mutex<HashMap<String, Arc<Mutex<Child>>>>>, stdins: Arc<Mutex<HashMap<String, Arc<Mutex<ChildStdin>>>>>,
killers: Arc<Mutex<HashMap<String, tokio::sync::oneshot::Sender<()>>>>,
} }
impl ProcessManager { impl ProcessManager {
pub fn new() -> Self { pub fn new() -> Self {
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) => { Ok(mut child) => {
let stdout = child.stdout.take().expect("Failed to open stdout"); let stdout = child.stdout.take().expect("Failed to open stdout");
let stderr = child.stderr.take().expect("Failed to open stderr"); 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 id_clone = id.clone();
let app_clone = app.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; let mut stdins = self.stdins.lock().await;
processes.insert(id.clone(), child_arc.clone()); 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 id_clone_exit = id.clone();
let app_clone_exit = app.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 { tokio::spawn(async move {
let exit_code = { let exit_code = tokio::select! {
let mut c = child_arc.lock().await; res = child.wait() => {
match c.wait().await { match res {
Ok(status) => status.code().unwrap_or(0), Ok(status) => status.code().unwrap_or(0),
Err(_) => 1, 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; let mut stdins = stdins_clone.lock().await;
processes.remove(&id_clone_exit); 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); 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> { pub async fn send_stdin(&self, id: &str, input: &str) -> Result<(), String> {
let processes = self.processes.lock().await; let stdin_arc = {
if let Some(child_arc) = processes.get(id) { let stdins = self.stdins.lock().await;
let mut child = child_arc.lock().await; stdins.get(id).cloned()
if let Some(stdin) = child.stdin.as_mut() { };
stdin.write_all(input.as_bytes()).await.map_err(|e| e.to_string())?; if let Some(stdin_arc) = stdin_arc {
stdin.flush().await.map_err(|e| e.to_string())?; let mut stdin = stdin_arc.lock().await;
return Ok(()); 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()) Err("Process not found or stdin not available".to_string())
} }
pub async fn kill(&self, id: &str) -> Result<(), String> { pub async fn kill(&self, id: &str) -> Result<(), String> {
let mut processes = self.processes.lock().await; // Drop and close stdin immediately
if let Some(child_arc) = processes.remove(id) { {
let mut child = child_arc.lock().await; let mut stdins = self.stdins.lock().await;
child.kill().await.map_err(|e| e.to_string())?; 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(()); return Ok(());
} }
Err("Process not found".to_string()) Err("Process not found".to_string())
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "ICCery", "productName": "ICCery",
"version": "0.2.0", "version": "0.2.1",
"identifier": "com.gronod.iccery", "identifier": "com.gronod.iccery",
"build": { "build": {
"frontendDist": "../src" "frontendDist": "../src"