From b389a56329e50d9714316e19e556760092189044 Mon Sep 17 00:00:00 2001 From: branchseer Date: Mon, 16 Feb 2026 00:36:00 +0800 Subject: [PATCH] fix: defer PTY slave drop to prevent macOS data loss race On macOS, when the parent's slave fd is closed immediately after spawn and the child exits quickly, all slave references close before the reader issues its first read(). macOS returns EIO on the master PTY without draining the output buffer, causing data loss. Move the slave fd into the background monitoring thread and drop it after child.wait() returns. This keeps the PTY in a connected state while the child runs, guaranteeing buffered output is preserved regardless of reader timing. --- crates/pty_terminal/src/terminal.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/pty_terminal/src/terminal.rs b/crates/pty_terminal/src/terminal.rs index 9e44a10c..7e509443 100644 --- a/crates/pty_terminal/src/terminal.rs +++ b/crates/pty_terminal/src/terminal.rs @@ -258,24 +258,32 @@ impl Terminal { let reader = pty_pair.master.try_clone_reader()?; let writer: Arc>>> = Arc::new(Mutex::new(Some(pty_pair.master.take_writer()?))); - // Spawn child and immediately drop slave to ensure EOF is signaled when child exits let mut child = pty_pair.slave.spawn_command(cmd)?; let child_killer = child.clone_killer(); - drop(pty_pair.slave); // Critical: drop slave so EOF is signaled when child exits let master = pty_pair.master; let exit_status: Arc> = Arc::new(OnceLock::new()); - // Background thread: wait for child to exit, set exit status, then close writer to trigger EOF + // Background thread: wait for child to exit, then clean up. + // + // The slave is kept alive until after `child.wait()` returns rather than + // being dropped immediately after spawn. On macOS, if the parent's slave + // fd is closed early (before spawn) and the child exits quickly, ALL + // slave references close before the reader issues its first `read()`. + // macOS then returns EIO on the master without draining the output buffer, + // causing data loss. Holding the slave until the background thread takes + // over guarantees the PTY stays connected while the child runs. thread::spawn({ let writer = Arc::clone(&writer); let exit_status = Arc::clone(&exit_status); + let slave = pty_pair.slave; move || { // Wait for child and set exit status if let Ok(status) = child.wait() { let _ = exit_status.set(status); } - // Close writer to signal EOF to the reader + // Close writer first, then drop slave to trigger EOF on the reader. *writer.lock().unwrap() = None; + drop(slave); } });