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.
|
> 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://git.i3omb.com/gronod/ICCery)
|
||||||
[](https://tauri.app)
|
[](https://tauri.app)
|
||||||
[](LICENCE.md)
|
[](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:
|
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
@@ -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"
|
||||||
|
|||||||
@@ -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"] }
|
||||||
|
|||||||
@@ -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,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"
|
||||||
|
|||||||
Reference in New Issue
Block a user