diff --git a/src/ipc.rs b/src/ipc.rs index 93f8330..8674757 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -259,7 +259,6 @@ pub struct ReplyEnvelope { /// feed reads) run without holding that lock so they cannot delay interactive /// ones such as selecting a profile. pub async fn serve() -> Result<(), AppError> { - use tokio::io::AsyncBufReadExt; use tokio::task::JoinSet; // Shared state, so the NIP-46 signer's background task and the request loop @@ -292,15 +291,45 @@ pub async fn serve() -> Result<(), AppError> { let stdout = Arc::new(tokio::sync::Mutex::new(tokio::io::stdout())); - let stdin = tokio::io::stdin(); - let mut lines = tokio::io::BufReader::new(stdin).lines(); + // Read stdin on a dedicated blocking thread, not tokio::io::stdin(). + // Tokio flips the pipe non-blocking and relies on readiness events; when + // Electron's pipe is busy (large npm/cargo output flowing while the GUI + // is active) a read can surface EAGAIN ("Resource temporarily + // unavailable", os error 11) despite data being pending, which used to + // kill the serve loop and every in-flight request with it. A plain + // blocking read cannot fail that way: it just waits. + let (line_tx, mut line_rx) = tokio::sync::mpsc::channel::(64); + std::thread::Builder::new() + .name("stdin-reader".to_string()) + .spawn(move || { + use std::io::BufRead; + let stdin = std::io::stdin(); + let mut reader = std::io::BufReader::new(stdin.lock()); + loop { + let mut line = String::new(); + match reader.read_line(&mut line) { + Ok(0) => break, // EOF: the GUI closed the pipe. + Ok(_) => { + if line_tx.blocking_send(line).is_err() { + break; // Serve loop gone; nobody to answer. + } + } + // Transient (EINTR/EAGAIN on a raw fd is impossible with + // blocking reads, but never take the whole loop down for + // a hiccup): keep reading. + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue, + Err(e) => { + eprintln!("[ipc] stdin read error, stopping: {e}"); + break; + } + } + } + }) + .map_err(|e| AppError::io("Could not start the stdin reader", e))?; + let mut tasks = JoinSet::new(); - while let Some(line) = lines - .next_line() - .await - .map_err(|e| AppError::io("Could not read from stdin", e))? - { + while let Some(line) = line_rx.recv().await { if line.trim().is_empty() { continue; }