fix(process): resolve process manager deadlock on chartread & release v0.2.1 #83
@@ -2,7 +2,7 @@
|
||||
|
||||
> Modern, cross-platform native desktop application for printer profiling, powered by ArgyllCMS.
|
||||
|
||||
[](https://git.i3omb.com/gronod/ICCery)
|
||||
[](https://git.i3omb.com/gronod/ICCery)
|
||||
[](https://git.i3omb.com/gronod/ICCery)
|
||||
[](https://tauri.app)
|
||||
[](LICENCE.md)
|
||||
|
||||
+5
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "iccery",
|
||||
"private": true,
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"tauri": "tauri"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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<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 {
|
||||
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 {
|
||||
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() {
|
||||
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())
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user