From 026ae53bf048f7cb222f49a3cada2d3249d96da8 Mon Sep 17 00:00:00 2001 From: branchseer Date: Mon, 16 Feb 2026 01:18:54 +0800 Subject: [PATCH 1/7] fix: increase timeout for flaky nonzero PTY exit test --- crates/pty_terminal/tests/terminal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/pty_terminal/tests/terminal.rs b/crates/pty_terminal/tests/terminal.rs index 76435d91..3b0a3cf8 100644 --- a/crates/pty_terminal/tests/terminal.rs +++ b/crates/pty_terminal/tests/terminal.rs @@ -352,7 +352,7 @@ fn read_to_end_returns_exit_status_success() { } #[test] -#[timeout(5000)] +#[timeout(15000)] fn read_to_end_returns_exit_status_nonzero() { let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { std::process::exit(42); From 79d6100fe247516a7e8cc1c52ac35725bb52d5b5 Mon Sep 17 00:00:00 2001 From: branchseer Date: Mon, 16 Feb 2026 01:41:42 +0800 Subject: [PATCH 2/7] fix: apply flaky PTY timeout bump only on Windows --- crates/pty_terminal/tests/terminal.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/pty_terminal/tests/terminal.rs b/crates/pty_terminal/tests/terminal.rs index 3b0a3cf8..947434f5 100644 --- a/crates/pty_terminal/tests/terminal.rs +++ b/crates/pty_terminal/tests/terminal.rs @@ -352,7 +352,8 @@ fn read_to_end_returns_exit_status_success() { } #[test] -#[timeout(15000)] +#[cfg_attr(windows, timeout(15000))] +#[cfg_attr(not(windows), timeout(5000))] fn read_to_end_returns_exit_status_nonzero() { let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { std::process::exit(42); From 14cd60cb6e994b8e1f1dbb09e79af12a94e85beb Mon Sep 17 00:00:00 2001 From: branchseer Date: Mon, 16 Feb 2026 02:19:40 +0800 Subject: [PATCH 3/7] fix: prevent PTY wait deadlock and serialize Windows tests --- crates/pty_terminal/src/terminal.rs | 41 ++++++++++++++++++++++++--- crates/pty_terminal/tests/terminal.rs | 38 +++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/crates/pty_terminal/src/terminal.rs b/crates/pty_terminal/src/terminal.rs index 9e44a10c..44969bb7 100644 --- a/crates/pty_terminal/src/terminal.rs +++ b/crates/pty_terminal/src/terminal.rs @@ -237,6 +237,14 @@ impl ChildHandle { } } +fn set_exit_status_from_wait_result( + exit_status: &OnceLock, + wait_result: std::io::Result, +) { + let status = wait_result.unwrap_or_else(|_| ExitStatus::with_exit_code(1)); + let _ = exit_status.set(status); +} + impl Terminal { /// Spawns a new child process in a headless terminal with the given size and command. /// @@ -270,10 +278,7 @@ impl Terminal { let writer = Arc::clone(&writer); let exit_status = Arc::clone(&exit_status); move || { - // Wait for child and set exit status - if let Ok(status) = child.wait() { - let _ = exit_status.set(status); - } + set_exit_status_from_wait_result(&exit_status, child.wait()); // Close writer to signal EOF to the reader *writer.lock().unwrap() = None; } @@ -296,3 +301,31 @@ impl Terminal { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn set_exit_status_from_wait_result_sets_error_fallback() { + let exit_status = OnceLock::new(); + set_exit_status_from_wait_result( + &exit_status, + Err(std::io::Error::other("forced wait error for test")), + ); + + let status = exit_status.wait(); + assert!(!status.success()); + assert_eq!(status.exit_code(), 1); + } + + #[test] + fn set_exit_status_from_wait_result_preserves_child_status() { + let exit_status = OnceLock::new(); + set_exit_status_from_wait_result(&exit_status, Ok(ExitStatus::with_exit_code(42))); + + let status = exit_status.wait(); + assert!(!status.success()); + assert_eq!(status.exit_code(), 42); + } +} diff --git a/crates/pty_terminal/tests/terminal.rs b/crates/pty_terminal/tests/terminal.rs index 947434f5..a2c734cf 100644 --- a/crates/pty_terminal/tests/terminal.rs +++ b/crates/pty_terminal/tests/terminal.rs @@ -1,3 +1,5 @@ +#[cfg(windows)] +use std::sync::{LazyLock, Mutex, MutexGuard}; use std::{ io::{BufRead, BufReader, IsTerminal, Read, Write, stderr, stdin, stdout}, time::{Duration, Instant}, @@ -8,10 +10,19 @@ use portable_pty::CommandBuilder; use pty_terminal::{geo::ScreenSize, terminal::Terminal}; use subprocess_test::command_for_fn; +#[cfg(windows)] +fn windows_test_serial_guard() -> MutexGuard<'static, ()> { + static WINDOWS_TEST_MUTEX: LazyLock> = LazyLock::new(|| Mutex::new(())); + WINDOWS_TEST_MUTEX.lock().unwrap() +} + #[test] #[timeout(5000)] #[expect(clippy::print_stdout, reason = "subprocess test output")] fn is_terminal() { + #[cfg(windows)] + let _guard = windows_test_serial_guard(); + let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { println!("{} {} {}", stdin().is_terminal(), stdout().is_terminal(), stderr().is_terminal()); })); @@ -29,6 +40,9 @@ fn is_terminal() { #[timeout(5000)] #[expect(clippy::print_stdout, reason = "subprocess test output")] fn write_basic_echo() { + #[cfg(windows)] + let _guard = windows_test_serial_guard(); + let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { use std::io::{BufRead, Write, stdin, stdout}; let stdin = stdin(); @@ -58,6 +72,9 @@ fn write_basic_echo() { #[timeout(5000)] #[expect(clippy::print_stdout, reason = "subprocess test output")] fn write_multiple_lines() { + #[cfg(windows)] + let _guard = windows_test_serial_guard(); + let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { use std::io::{BufRead, Write, stdin, stdout}; let stdin = stdin(); @@ -109,6 +126,9 @@ fn write_multiple_lines() { #[timeout(5000)] #[expect(clippy::print_stdout, reason = "subprocess test output")] fn write_after_exit() { + #[cfg(windows)] + let _guard = windows_test_serial_guard(); + let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { print!("exiting"); })); @@ -137,6 +157,9 @@ fn write_after_exit() { #[timeout(5000)] #[expect(clippy::print_stdout, reason = "subprocess test output")] fn write_interactive_prompt() { + #[cfg(windows)] + let _guard = windows_test_serial_guard(); + let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { use std::io::{Write, stdin, stdout}; let mut stdout = stdout(); @@ -175,6 +198,9 @@ fn write_interactive_prompt() { #[timeout(5000)] #[expect(clippy::print_stdout, reason = "subprocess test output")] fn resize_terminal() { + #[cfg(windows)] + let _guard = windows_test_serial_guard(); + let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { use std::io::{Write, stdin, stdout}; #[cfg(unix)] @@ -272,6 +298,9 @@ fn resize_terminal() { #[timeout(5000)] #[expect(clippy::print_stdout, reason = "subprocess test output")] fn send_ctrl_c_interrupts_process() { + #[cfg(windows)] + let _guard = windows_test_serial_guard(); + let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { use std::io::{Write, stdout}; @@ -338,6 +367,9 @@ fn send_ctrl_c_interrupts_process() { #[timeout(5000)] #[expect(clippy::print_stdout, reason = "subprocess test output")] fn read_to_end_returns_exit_status_success() { + #[cfg(windows)] + let _guard = windows_test_serial_guard(); + let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { println!("success"); })); @@ -352,9 +384,11 @@ fn read_to_end_returns_exit_status_success() { } #[test] -#[cfg_attr(windows, timeout(15000))] -#[cfg_attr(not(windows), timeout(5000))] +#[timeout(5000)] fn read_to_end_returns_exit_status_nonzero() { + #[cfg(windows)] + let _guard = windows_test_serial_guard(); + let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { std::process::exit(42); })); From 40eb45bb8deec4f32d1930a898adce9594e5e2c2 Mon Sep 17 00:00:00 2001 From: branchseer Date: Mon, 16 Feb 2026 11:08:15 +0800 Subject: [PATCH 4/7] fix: preserve PTY wait errors and remove test serialization --- crates/pty_terminal/src/terminal.rs | 31 +++++++----- crates/pty_terminal/tests/terminal.rs | 49 +++---------------- crates/pty_terminal_test/src/lib.rs | 6 ++- crates/pty_terminal_test/tests/milestone.rs | 4 +- .../vite_task_bin/tests/e2e_snapshots/main.rs | 2 +- 5 files changed, 34 insertions(+), 58 deletions(-) diff --git a/crates/pty_terminal/src/terminal.rs b/crates/pty_terminal/src/terminal.rs index 44969bb7..baecb370 100644 --- a/crates/pty_terminal/src/terminal.rs +++ b/crates/pty_terminal/src/terminal.rs @@ -32,7 +32,7 @@ pub struct PtyWriter { /// A cloneable handle to a child process spawned in a PTY. pub struct ChildHandle { child_killer: Box, - exit_status: Arc>, + exit_status: Arc>>, } impl Clone for ChildHandle { @@ -221,9 +221,16 @@ impl PtyWriter { impl ChildHandle { /// Blocks until the child process has exited and returns its exit status. + /// + /// # Errors + /// + /// Returns an error if waiting for the child process exit status fails. #[must_use] - pub fn wait(&self) -> ExitStatus { - self.exit_status.wait().clone() + pub fn wait(&self) -> std::io::Result { + match self.exit_status.wait() { + Ok(status) => Ok(status.clone()), + Err(error) => Err(std::io::Error::new(error.kind(), error.to_string())), + } } /// Kills the child process. @@ -238,11 +245,10 @@ impl ChildHandle { } fn set_exit_status_from_wait_result( - exit_status: &OnceLock, + exit_status: &OnceLock>, wait_result: std::io::Result, ) { - let status = wait_result.unwrap_or_else(|_| ExitStatus::with_exit_code(1)); - let _ = exit_status.set(status); + let _ = exit_status.set(wait_result); } impl Terminal { @@ -271,7 +277,7 @@ impl Terminal { 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()); + let exit_status: Arc>> = Arc::new(OnceLock::new()); // Background thread: wait for child to exit, set exit status, then close writer to trigger EOF thread::spawn({ @@ -307,7 +313,7 @@ mod tests { use super::*; #[test] - fn set_exit_status_from_wait_result_sets_error_fallback() { + fn set_exit_status_from_wait_result_preserves_error() { let exit_status = OnceLock::new(); set_exit_status_from_wait_result( &exit_status, @@ -315,8 +321,9 @@ mod tests { ); let status = exit_status.wait(); - assert!(!status.success()); - assert_eq!(status.exit_code(), 1); + assert!(status.is_err()); + assert_eq!(status.as_ref().unwrap_err().kind(), std::io::ErrorKind::Other); + assert_eq!(status.as_ref().unwrap_err().to_string(), "forced wait error for test"); } #[test] @@ -325,7 +332,7 @@ mod tests { set_exit_status_from_wait_result(&exit_status, Ok(ExitStatus::with_exit_code(42))); let status = exit_status.wait(); - assert!(!status.success()); - assert_eq!(status.exit_code(), 42); + assert!(!status.as_ref().unwrap().success()); + assert_eq!(status.as_ref().unwrap().exit_code(), 42); } } diff --git a/crates/pty_terminal/tests/terminal.rs b/crates/pty_terminal/tests/terminal.rs index a2c734cf..44489124 100644 --- a/crates/pty_terminal/tests/terminal.rs +++ b/crates/pty_terminal/tests/terminal.rs @@ -1,5 +1,3 @@ -#[cfg(windows)] -use std::sync::{LazyLock, Mutex, MutexGuard}; use std::{ io::{BufRead, BufReader, IsTerminal, Read, Write, stderr, stdin, stdout}, time::{Duration, Instant}, @@ -10,19 +8,10 @@ use portable_pty::CommandBuilder; use pty_terminal::{geo::ScreenSize, terminal::Terminal}; use subprocess_test::command_for_fn; -#[cfg(windows)] -fn windows_test_serial_guard() -> MutexGuard<'static, ()> { - static WINDOWS_TEST_MUTEX: LazyLock> = LazyLock::new(|| Mutex::new(())); - WINDOWS_TEST_MUTEX.lock().unwrap() -} - #[test] #[timeout(5000)] #[expect(clippy::print_stdout, reason = "subprocess test output")] fn is_terminal() { - #[cfg(windows)] - let _guard = windows_test_serial_guard(); - let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { println!("{} {} {}", stdin().is_terminal(), stdout().is_terminal(), stderr().is_terminal()); })); @@ -31,7 +20,7 @@ fn is_terminal() { Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap(); let mut discard = Vec::new(); pty_reader.read_to_end(&mut discard).unwrap(); - let _ = child_handle.wait(); + let _ = child_handle.wait().unwrap(); let output = pty_reader.screen_contents(); assert_eq!(output.trim(), "true true true"); } @@ -40,9 +29,6 @@ fn is_terminal() { #[timeout(5000)] #[expect(clippy::print_stdout, reason = "subprocess test output")] fn write_basic_echo() { - #[cfg(windows)] - let _guard = windows_test_serial_guard(); - let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { use std::io::{BufRead, Write, stdin, stdout}; let stdin = stdin(); @@ -61,7 +47,7 @@ fn write_basic_echo() { let mut discard = Vec::new(); pty_reader.read_to_end(&mut discard).unwrap(); - let _ = child_handle.wait(); + let _ = child_handle.wait().unwrap(); let output = pty_reader.screen_contents(); // PTY echoes the input, so we see "hello world\nhello world" @@ -72,9 +58,6 @@ fn write_basic_echo() { #[timeout(5000)] #[expect(clippy::print_stdout, reason = "subprocess test output")] fn write_multiple_lines() { - #[cfg(windows)] - let _guard = windows_test_serial_guard(); - let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { use std::io::{BufRead, Write, stdin, stdout}; let stdin = stdin(); @@ -115,7 +98,7 @@ fn write_multiple_lines() { let mut discard = Vec::new(); pty_reader.read_to_end(&mut discard).unwrap(); - let _ = child_handle.wait(); + let _ = child_handle.wait().unwrap(); let output = pty_reader.screen_contents(); // PTY echoes input, then child prints "Echo: {line}\n" for each @@ -126,9 +109,6 @@ fn write_multiple_lines() { #[timeout(5000)] #[expect(clippy::print_stdout, reason = "subprocess test output")] fn write_after_exit() { - #[cfg(windows)] - let _guard = windows_test_serial_guard(); - let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { print!("exiting"); })); @@ -139,7 +119,7 @@ fn write_after_exit() { // Read all output - this blocks until child exits and EOF is reached let mut discard = Vec::new(); pty_reader.read_to_end(&mut discard).unwrap(); - let _ = child_handle.wait(); + let _ = child_handle.wait().unwrap(); // Writer shutdown is done by a background thread after child wait returns. // Poll briefly for the writer state to flip to closed before asserting write failure. @@ -157,9 +137,6 @@ fn write_after_exit() { #[timeout(5000)] #[expect(clippy::print_stdout, reason = "subprocess test output")] fn write_interactive_prompt() { - #[cfg(windows)] - let _guard = windows_test_serial_guard(); - let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { use std::io::{Write, stdin, stdout}; let mut stdout = stdout(); @@ -188,7 +165,7 @@ fn write_interactive_prompt() { let mut discard = Vec::new(); pty_reader.read_to_end(&mut discard).unwrap(); - let _ = child_handle.wait(); + let _ = child_handle.wait().unwrap(); let output = pty_reader.screen_contents(); assert_eq!(output.trim(), "Name: Alice\nHello, Alice"); @@ -198,9 +175,6 @@ fn write_interactive_prompt() { #[timeout(5000)] #[expect(clippy::print_stdout, reason = "subprocess test output")] fn resize_terminal() { - #[cfg(windows)] - let _guard = windows_test_serial_guard(); - let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { use std::io::{Write, stdin, stdout}; #[cfg(unix)] @@ -298,9 +272,6 @@ fn resize_terminal() { #[timeout(5000)] #[expect(clippy::print_stdout, reason = "subprocess test output")] fn send_ctrl_c_interrupts_process() { - #[cfg(windows)] - let _guard = windows_test_serial_guard(); - let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { use std::io::{Write, stdout}; @@ -367,9 +338,6 @@ fn send_ctrl_c_interrupts_process() { #[timeout(5000)] #[expect(clippy::print_stdout, reason = "subprocess test output")] fn read_to_end_returns_exit_status_success() { - #[cfg(windows)] - let _guard = windows_test_serial_guard(); - let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { println!("success"); })); @@ -378,7 +346,7 @@ fn read_to_end_returns_exit_status_success() { Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap(); let mut discard = Vec::new(); pty_reader.read_to_end(&mut discard).unwrap(); - let status = child_handle.wait(); + let status = child_handle.wait().unwrap(); assert!(status.success()); assert_eq!(status.exit_code(), 0); } @@ -386,9 +354,6 @@ fn read_to_end_returns_exit_status_success() { #[test] #[timeout(5000)] fn read_to_end_returns_exit_status_nonzero() { - #[cfg(windows)] - let _guard = windows_test_serial_guard(); - let cmd = CommandBuilder::from(command_for_fn!((), |(): ()| { std::process::exit(42); })); @@ -397,7 +362,7 @@ fn read_to_end_returns_exit_status_nonzero() { Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap(); let mut discard = Vec::new(); pty_reader.read_to_end(&mut discard).unwrap(); - let status = child_handle.wait(); + let status = child_handle.wait().unwrap(); assert!(!status.success()); assert_eq!(status.exit_code(), 42); } diff --git a/crates/pty_terminal_test/src/lib.rs b/crates/pty_terminal_test/src/lib.rs index 3f53c69d..3045fd93 100644 --- a/crates/pty_terminal_test/src/lib.rs +++ b/crates/pty_terminal_test/src/lib.rs @@ -91,10 +91,14 @@ impl Reader { /// Reads all remaining PTY output until the child exits, then returns the exit status. /// + /// # Errors + /// + /// Returns an error if waiting for the child process exit status fails. + /// /// # Panics /// /// Panics if reading from the PTY fails. - pub fn wait_for_exit(&mut self) -> ExitStatus { + pub fn wait_for_exit(&mut self) -> std::io::Result { let mut discard = Vec::new(); self.pty.read_to_end(&mut discard).expect("PTY read_to_end failed"); self.child_handle.wait() diff --git a/crates/pty_terminal_test/tests/milestone.rs b/crates/pty_terminal_test/tests/milestone.rs index 234519bf..e878e069 100644 --- a/crates/pty_terminal_test/tests/milestone.rs +++ b/crates/pty_terminal_test/tests/milestone.rs @@ -67,7 +67,7 @@ fn milestone_raw_mode_keystrokes() { // Write 'q' to quit and wait for the child to exit writer.write_all(b"q").unwrap(); writer.flush().unwrap(); - let status = reader.wait_for_exit(); + let status = reader.wait_for_exit().unwrap(); assert!(status.success()); } @@ -122,6 +122,6 @@ fn milestone_does_not_pollute_screen() { writer.write_all(b"q").unwrap(); writer.flush().unwrap(); - let status = reader.wait_for_exit(); + let status = reader.wait_for_exit().unwrap(); assert!(status.success()); } diff --git a/crates/vite_task_bin/tests/e2e_snapshots/main.rs b/crates/vite_task_bin/tests/e2e_snapshots/main.rs index 07e65543..b9c88af5 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/main.rs +++ b/crates/vite_task_bin/tests/e2e_snapshots/main.rs @@ -391,7 +391,7 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture } } - let status = terminal.reader.wait_for_exit(); + let status = terminal.reader.wait_for_exit().unwrap(); let screen = terminal.reader.screen_contents(); { From d3c938e807c2a860da9d24006e6b43870752451c Mon Sep 17 00:00:00 2001 From: branchseer Date: Mon, 16 Feb 2026 11:14:06 +0800 Subject: [PATCH 5/7] refactor: inline PTY wait result set and remove unit tests --- crates/pty_terminal/src/terminal.rs | 38 +---------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/crates/pty_terminal/src/terminal.rs b/crates/pty_terminal/src/terminal.rs index baecb370..92a094f8 100644 --- a/crates/pty_terminal/src/terminal.rs +++ b/crates/pty_terminal/src/terminal.rs @@ -244,13 +244,6 @@ impl ChildHandle { } } -fn set_exit_status_from_wait_result( - exit_status: &OnceLock>, - wait_result: std::io::Result, -) { - let _ = exit_status.set(wait_result); -} - impl Terminal { /// Spawns a new child process in a headless terminal with the given size and command. /// @@ -284,7 +277,7 @@ impl Terminal { let writer = Arc::clone(&writer); let exit_status = Arc::clone(&exit_status); move || { - set_exit_status_from_wait_result(&exit_status, child.wait()); + let _ = exit_status.set(child.wait()); // Close writer to signal EOF to the reader *writer.lock().unwrap() = None; } @@ -307,32 +300,3 @@ impl Terminal { }) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn set_exit_status_from_wait_result_preserves_error() { - let exit_status = OnceLock::new(); - set_exit_status_from_wait_result( - &exit_status, - Err(std::io::Error::other("forced wait error for test")), - ); - - let status = exit_status.wait(); - assert!(status.is_err()); - assert_eq!(status.as_ref().unwrap_err().kind(), std::io::ErrorKind::Other); - assert_eq!(status.as_ref().unwrap_err().to_string(), "forced wait error for test"); - } - - #[test] - fn set_exit_status_from_wait_result_preserves_child_status() { - let exit_status = OnceLock::new(); - set_exit_status_from_wait_result(&exit_status, Ok(ExitStatus::with_exit_code(42))); - - let status = exit_status.wait(); - assert!(!status.as_ref().unwrap().success()); - assert_eq!(status.as_ref().unwrap().exit_code(), 42); - } -} From cfde2e12cbbb57fea65468a0db9e8e0e763d7491 Mon Sep 17 00:00:00 2001 From: branchseer Date: Mon, 16 Feb 2026 11:21:38 +0800 Subject: [PATCH 6/7] refactor: store shared PTY wait errors via Arc --- crates/pty_terminal/src/terminal.rs | 12 +++++++----- crates/pty_terminal_test/src/lib.rs | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/pty_terminal/src/terminal.rs b/crates/pty_terminal/src/terminal.rs index 92a094f8..65fd82b8 100644 --- a/crates/pty_terminal/src/terminal.rs +++ b/crates/pty_terminal/src/terminal.rs @@ -10,6 +10,8 @@ use portable_pty::{ChildKiller, ExitStatus, MasterPty}; use crate::geo::ScreenSize; +type ChildWaitResult = Result>; + /// The read half of a PTY connection. Implements [`Read`]. /// /// Reading feeds data through an internal vt100 parser (shared with [`PtyWriter`]), @@ -32,7 +34,7 @@ pub struct PtyWriter { /// A cloneable handle to a child process spawned in a PTY. pub struct ChildHandle { child_killer: Box, - exit_status: Arc>>, + exit_status: Arc>, } impl Clone for ChildHandle { @@ -226,10 +228,10 @@ impl ChildHandle { /// /// Returns an error if waiting for the child process exit status fails. #[must_use] - pub fn wait(&self) -> std::io::Result { + pub fn wait(&self) -> anyhow::Result { match self.exit_status.wait() { Ok(status) => Ok(status.clone()), - Err(error) => Err(std::io::Error::new(error.kind(), error.to_string())), + Err(error) => Err(anyhow::Error::new(Arc::clone(error))), } } @@ -270,14 +272,14 @@ impl Terminal { 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()); + let exit_status: Arc> = Arc::new(OnceLock::new()); // Background thread: wait for child to exit, set exit status, then close writer to trigger EOF thread::spawn({ let writer = Arc::clone(&writer); let exit_status = Arc::clone(&exit_status); move || { - let _ = exit_status.set(child.wait()); + let _ = exit_status.set(child.wait().map_err(Arc::new)); // Close writer to signal EOF to the reader *writer.lock().unwrap() = None; } diff --git a/crates/pty_terminal_test/src/lib.rs b/crates/pty_terminal_test/src/lib.rs index 3045fd93..187cbac3 100644 --- a/crates/pty_terminal_test/src/lib.rs +++ b/crates/pty_terminal_test/src/lib.rs @@ -98,7 +98,7 @@ impl Reader { /// # Panics /// /// Panics if reading from the PTY fails. - pub fn wait_for_exit(&mut self) -> std::io::Result { + pub fn wait_for_exit(&mut self) -> anyhow::Result { let mut discard = Vec::new(); self.pty.read_to_end(&mut discard).expect("PTY read_to_end failed"); self.child_handle.wait() From eafbd3bf31c43aeb286b1e02b0225bb0e3ff1467 Mon Sep 17 00:00:00 2001 From: branchseer Date: Mon, 16 Feb 2026 11:27:58 +0800 Subject: [PATCH 7/7] fix: drop redundant must_use on PTY wait --- crates/pty_terminal/src/terminal.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/pty_terminal/src/terminal.rs b/crates/pty_terminal/src/terminal.rs index 65fd82b8..98fb11e7 100644 --- a/crates/pty_terminal/src/terminal.rs +++ b/crates/pty_terminal/src/terminal.rs @@ -227,7 +227,6 @@ impl ChildHandle { /// # Errors /// /// Returns an error if waiting for the child process exit status fails. - #[must_use] pub fn wait(&self) -> anyhow::Result { match self.exit_status.wait() { Ok(status) => Ok(status.clone()),