From 9770f46d4a4185250172a28e2f15a9bd03838922 Mon Sep 17 00:00:00 2001 From: Avi Date: Mon, 28 Sep 2026 09:56:22 -0500 Subject: [PATCH] =?UTF-8?q?fix(ipc):=20read=20stdin=20on=20a=20blocking=20?= =?UTF-8?q?thread=20=E2=80=94=20EAGAIN=20no=20longer=20kills=20the=20backe?= =?UTF-8?q?nd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The user's crash, captured in the app's own console (Tools/keynctr-debug/el.log): 'Could not read from stdin: Resource temporarily unavailable (os error 11)'. tokio::io::stdin flips the Electron pipe non-blocking; under load a read surfaced EAGAIN despite pending data and the serve loop treated it as fatal, exiting code 1 and failing every in-flight request (the 'Rust backend exited unexpectedly' card). Now a dedicated blocking-thread reader feeds an mpsc channel: blocking reads cannot spuriously fail, EOF ends the loop cleanly, transient errors log and continue. Verified live: 200-line request stream -> 200 responses, exit 0; 219 unit + 6 e2e green; clippy 0; release rebuilt 09:54. --- src/ipc.rs | 45 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) 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; }