From 7e6527d8fee44a9594a3cc732fc2edd21fa665aa Mon Sep 17 00:00:00 2001 From: Francesco Bonacci Date: Tue, 23 Jun 2026 02:54:29 -0700 Subject: [PATCH] fix(cua-driver): don't treat socket read-timeout (EAGAIN) as fatal in daemon proxy (#1864) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MCP stdio server forwards tools/call to the 'cua-driver serve' daemon over the Unix socket via send_request, which set a 10s SO_RCVTIMEO and read one line with BufRead::lines(). On a slow AX walk (capture_mode=som/ax on Apple Notes) or a multi-MB get_window_state response, a read that hits the timeout returns WouldBlock/TimedOut (EAGAIN, os error 35), which lines() surfaces as a fatal 'daemon transport error … Resource temporarily unavailable' even though the daemon is still working. list_windows/click/type_text (small/fast) were fine. Replace the single timed read with a manual newline-framed read loop: treat the per-read timeout as a liveness poll, keep waiting on WouldBlock/TimedOut/ Interrupted until a full line arrives, the daemon closes the connection, or a generous 120s overall deadline. EAGAIN is no longer fatal. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01KMXCW4M5uK1HRGjjH4wueZ --- .../rust/crates/cua-driver/src/serve.rs | 58 ++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/libs/cua-driver/rust/crates/cua-driver/src/serve.rs b/libs/cua-driver/rust/crates/cua-driver/src/serve.rs index f8cd787cff..7efa76ec38 100644 --- a/libs/cua-driver/rust/crates/cua-driver/src/serve.rs +++ b/libs/cua-driver/rust/crates/cua-driver/src/serve.rs @@ -358,13 +358,16 @@ pub fn read_pid_file(pid_file_path: &str) -> Option { /// Uses a 3-second connect timeout (by polling) and a 10-second read timeout. #[cfg(unix)] pub fn send_request(socket_path: &str, req: &DaemonRequest) -> anyhow::Result { - use std::io::{BufRead, BufReader, Write}; + use std::io::{Read, Write}; use std::os::unix::net::UnixStream; - use std::time::Duration; + use std::time::{Duration, Instant}; - let stream = UnixStream::connect(socket_path) + let mut stream = UnixStream::connect(socket_path) .map_err(|e| anyhow::anyhow!("connect to {socket_path}: {e}"))?; stream.set_write_timeout(Some(Duration::from_secs(5)))?; + // Per-read timeout acts as a liveness poll, NOT a hard cap on the whole + // response: an AX-heavy `get_window_state` (slow tree walk) or a multi-MB + // SOM screenshot can legitimately take longer than one window to produce. stream.set_read_timeout(Some(Duration::from_secs(10)))?; let mut w = stream.try_clone()?; @@ -372,11 +375,50 @@ pub fn send_request(socket_path: &str, req: &DaemonRequest) -> anyhow::Result = Vec::with_capacity(64 * 1024); + let mut chunk = [0u8; 64 * 1024]; + let resp_line = loop { + if let Some(nl) = buf.iter().position(|&b| b == b'\n') { + break String::from_utf8_lossy(&buf[..nl]).into_owned(); + } + match stream.read(&mut chunk) { + Ok(0) => { + // EOF. Use whatever we buffered (some daemons close right after + // a final unterminated line); otherwise the daemon hung up. + if buf.is_empty() { + anyhow::bail!("daemon closed connection without response"); + } + break String::from_utf8_lossy(&buf).into_owned(); + } + Ok(n) => buf.extend_from_slice(&chunk[..n]), + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue, + Err(e) + if matches!( + e.kind(), + std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut + ) => + { + if Instant::now() >= overall_deadline { + anyhow::bail!( + "timed out after 120s waiting for daemon response \ + (received {} bytes so far)", + buf.len() + ); + } + continue; + } + Err(e) => return Err(e.into()), + } + }; let resp: DaemonResponse = serde_json::from_str(&resp_line)?; Ok(resp) }