Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions crates/pty_terminal/src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use portable_pty::{ChildKiller, ExitStatus, MasterPty};

use crate::geo::ScreenSize;

type ChildWaitResult = Result<ExitStatus, Arc<std::io::Error>>;

/// The read half of a PTY connection. Implements [`Read`].
///
/// Reading feeds data through an internal vt100 parser (shared with [`PtyWriter`]),
Expand All @@ -32,7 +34,7 @@ pub struct PtyWriter {
/// A cloneable handle to a child process spawned in a PTY.
pub struct ChildHandle {
child_killer: Box<dyn ChildKiller + Send + Sync>,
exit_status: Arc<OnceLock<ExitStatus>>,
exit_status: Arc<OnceLock<ChildWaitResult>>,
}

impl Clone for ChildHandle {
Expand Down Expand Up @@ -221,9 +223,15 @@ impl PtyWriter {

impl ChildHandle {
/// Blocks until the child process has exited and returns its exit status.
#[must_use]
pub fn wait(&self) -> ExitStatus {
self.exit_status.wait().clone()
///
/// # Errors
///
/// Returns an error if waiting for the child process exit status fails.
pub fn wait(&self) -> anyhow::Result<ExitStatus> {
match self.exit_status.wait() {
Ok(status) => Ok(status.clone()),
Err(error) => Err(anyhow::Error::new(Arc::clone(error))),
}
}

/// Kills the child process.
Expand Down Expand Up @@ -263,17 +271,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<OnceLock<ExitStatus>> = Arc::new(OnceLock::new());
let exit_status: Arc<OnceLock<ChildWaitResult>> = 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 || {
// Wait for child and set exit status
if let Ok(status) = child.wait() {
let _ = exit_status.set(status);
}
let _ = exit_status.set(child.wait().map_err(Arc::new));
// Close writer to signal EOF to the reader
*writer.lock().unwrap() = None;
}
Expand Down
14 changes: 7 additions & 7 deletions crates/pty_terminal/tests/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,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");
}
Expand All @@ -47,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"
Expand Down Expand Up @@ -98,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
Expand All @@ -119,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.
Expand Down Expand Up @@ -165,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");
Expand Down Expand Up @@ -346,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);
}
Expand All @@ -362,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);
}
6 changes: 5 additions & 1 deletion crates/pty_terminal_test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) -> anyhow::Result<ExitStatus> {
let mut discard = Vec::new();
self.pty.read_to_end(&mut discard).expect("PTY read_to_end failed");
self.child_handle.wait()
Expand Down
4 changes: 2 additions & 2 deletions crates/pty_terminal_test/tests/milestone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down Expand Up @@ -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());
}
2 changes: 1 addition & 1 deletion crates/vite_task_bin/tests/e2e_snapshots/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

{
Expand Down