Skip to content

Commit 5a07b91

Browse files
branchseerclaudehappy-otter
committed
feat(vite_pty): return ExitStatus from read_to_end
Update Terminal::read_to_end() to return the child process exit status instead of (). Uses OnceLock to synchronize between the background thread (which waits for the child and sets the status) and read_to_end() (which waits on the OnceLock after reading all output). Changes: - Add exit_status: Arc<OnceLock<ExitStatus>> field to Terminal - Background thread now captures and stores exit status before closing writer - read_to_end() returns ExitStatus after reading all output - Re-export ExitStatus from vite_pty crate - Add tests for successful (0) and non-zero (42) exit codes All 16 tests pass on both macOS and Windows (via cargo xtest). Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
1 parent c10ffe2 commit 5a07b91

3 files changed

Lines changed: 69 additions & 19 deletions

File tree

crates/vite_pty/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
pub mod geo;
22
pub mod terminal;
3+
4+
pub use portable_pty::ExitStatus;

crates/vite_pty/src/terminal.rs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use std::{
22
io::{Read, Write},
3-
sync::{Arc, Mutex},
3+
sync::{Arc, Mutex, OnceLock},
44
thread,
55
};
66

77
pub use portable_pty::CommandBuilder;
8-
use portable_pty::{ChildKiller, PtyPair};
8+
use portable_pty::{ChildKiller, ExitStatus, PtyPair};
99

1010
use crate::geo::ScreenSize;
1111

@@ -19,6 +19,9 @@ pub struct Terminal {
1919

2020
/// Unprocessed data buffer for read_until
2121
read_until_buffer: Vec<u8>,
22+
23+
/// Exit status from the child process, set once by background thread
24+
exit_status: Arc<OnceLock<ExitStatus>>,
2225
}
2326

2427
struct Vt100Callbacks {
@@ -64,12 +67,17 @@ impl Terminal {
6467
let child_killer = child.clone_killer();
6568
let writer: Arc<Mutex<Option<Box<dyn Write + Send>>>> =
6669
Arc::new(Mutex::new(Some(pty_pair.master.take_writer()?)));
70+
let exit_status: Arc<OnceLock<ExitStatus>> = Arc::new(OnceLock::new());
6771

68-
// Background thread: wait for child to exit, then close writer to trigger EOF
72+
// Background thread: wait for child to exit, set exit status, then close writer to trigger EOF
6973
thread::spawn({
7074
let writer = Arc::clone(&writer);
75+
let exit_status = Arc::clone(&exit_status);
7176
move || {
72-
let _ = child.wait();
77+
// Wait for child and set exit status
78+
if let Ok(status) = child.wait() {
79+
let _ = exit_status.set(status);
80+
}
7381
// Close writer to signal EOF to the reader
7482
*writer.lock().unwrap() = None;
7583
}
@@ -87,6 +95,7 @@ impl Terminal {
8795
reader,
8896
read_until_buffer: Vec::new(),
8997
writer,
98+
exit_status,
9099
})
91100
}
92101

@@ -138,7 +147,16 @@ impl Terminal {
138147
Ok(())
139148
}
140149

141-
pub fn read_to_end(&mut self) -> anyhow::Result<()> {
150+
/// Reads all remaining output until the child process exits.
151+
///
152+
/// Returns the exit status of the child process.
153+
///
154+
/// # Errors
155+
///
156+
/// Returns an error if:
157+
/// - Reading from the PTY fails
158+
/// - The exit status is not available (should not happen in normal operation)
159+
pub fn read_to_end(&mut self) -> anyhow::Result<ExitStatus> {
142160
// `read_to_end` will move cursor to the end, so clear any buffered data for `read_until`
143161
self.read_until_buffer.clear();
144162

@@ -151,7 +169,11 @@ impl Terminal {
151169
break;
152170
}
153171
}
154-
Ok(())
172+
173+
// Wait for exit status to be set by background thread
174+
let status = self.exit_status.wait().clone();
175+
176+
Ok(status)
155177
}
156178

157179
pub fn write(&mut self, data: &[u8]) -> anyhow::Result<()> {

crates/vite_pty/tests/terminal.rs

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ fn is_terminal() {
1717
}));
1818

1919
let mut terminal = Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
20-
terminal.read_to_end().unwrap();
20+
let _ = terminal.read_to_end().unwrap();
2121
let output = terminal.screen_contents();
2222
assert_eq!(output.trim(), "true true true");
2323
}
@@ -31,7 +31,7 @@ fn read_until_single() {
3131

3232
let mut terminal = Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
3333
terminal.read_until("hello").unwrap();
34-
terminal.read_to_end().unwrap();
34+
let _ = terminal.read_to_end().unwrap();
3535
let output = terminal.screen_contents();
3636
// After reading until "hello", the buffer should contain " world"
3737
// read_to_end should process the buffered data and continue reading
@@ -51,7 +51,7 @@ fn read_until_multiple_sequential() {
5151
terminal.read_until("first").unwrap();
5252
terminal.read_until("second").unwrap();
5353
terminal.read_until("third").unwrap();
54-
terminal.read_to_end().unwrap();
54+
let _ = terminal.read_to_end().unwrap();
5555
let output = terminal.screen_contents();
5656
// All three words should be in the screen
5757
assert!(output.contains("first"));
@@ -86,7 +86,7 @@ fn read_until_with_read_to_end() {
8686
let mut terminal = Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
8787
terminal.read_until("middle").unwrap();
8888
// At this point, " suffix" should be buffered
89-
terminal.read_to_end().unwrap();
89+
let _ = terminal.read_to_end().unwrap();
9090
let output = terminal.screen_contents();
9191
// The full output should include everything
9292
assert!(output.contains("prefix"));
@@ -122,7 +122,7 @@ fn read_until_boundary_spanning() {
122122
let mut terminal = Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
123123
// Search for a pattern that's likely to span boundaries
124124
terminal.read_until("abcd").unwrap();
125-
terminal.read_to_end().unwrap();
125+
let _ = terminal.read_to_end().unwrap();
126126
let output = terminal.screen_contents();
127127
assert!(output.contains("abcdef"));
128128
}
@@ -142,7 +142,7 @@ fn read_until_exact_boundary() {
142142
let mut terminal = Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
143143
// This should find "second" even if "first" was in a previous read
144144
terminal.read_until("second").unwrap();
145-
terminal.read_to_end().unwrap();
145+
let _ = terminal.read_to_end().unwrap();
146146
let output = terminal.screen_contents();
147147
assert!(output.contains("first"));
148148
assert!(output.contains("second"));
@@ -162,7 +162,7 @@ fn read_until_after_read_to_end() {
162162
terminal.read_until("world").unwrap();
163163

164164
// Read everything else
165-
terminal.read_to_end().unwrap();
165+
let _ = terminal.read_to_end().unwrap();
166166
let output = terminal.screen_contents();
167167
assert!(output.contains("hello world foo bar"));
168168

@@ -195,7 +195,7 @@ fn write_basic_echo() {
195195

196196
// Read until we see the echo
197197
terminal.read_until("hello world").unwrap();
198-
terminal.read_to_end().unwrap();
198+
let _ = terminal.read_to_end().unwrap();
199199

200200
let output = terminal.screen_contents();
201201
// PTY echoes the input, so we see "hello world\nhello world"
@@ -231,7 +231,7 @@ fn write_multiple_lines() {
231231
terminal.write(b"third\n").unwrap();
232232
terminal.read_until("Echo: third").unwrap();
233233

234-
terminal.read_to_end().unwrap();
234+
let _ = terminal.read_to_end().unwrap();
235235
let output = terminal.screen_contents();
236236
// PTY echoes input, so we see both the typed input and the echo response
237237
assert_eq!(output.trim(), "first\nEcho: firstsecond\nEcho: secondthird\nEcho: third");
@@ -247,7 +247,7 @@ fn write_after_exit() {
247247
let mut terminal = Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
248248

249249
// Read all output - this blocks until child exits and EOF is reached
250-
terminal.read_to_end().unwrap();
250+
let _ = terminal.read_to_end().unwrap();
251251

252252
// The background thread should have set writer to None by now
253253
// since read_to_end only returns after EOF (child exit)
@@ -282,7 +282,7 @@ fn write_interactive_prompt() {
282282
// Wait for greeting
283283
terminal.read_until("Hello, Alice").unwrap();
284284

285-
terminal.read_to_end().unwrap();
285+
let _ = terminal.read_to_end().unwrap();
286286
let output = terminal.screen_contents();
287287
assert_eq!(output.trim(), "Name: Alice\nHello, Alice");
288288
}
@@ -368,7 +368,7 @@ fn resize_terminal() {
368368
// Verify new size is correct
369369
terminal.read_until("resized: 40 40").unwrap();
370370

371-
terminal.read_to_end().unwrap();
371+
let _ = terminal.read_to_end().unwrap();
372372
}
373373

374374
#[test]
@@ -430,5 +430,31 @@ fn send_ctrl_c_interrupts_process() {
430430
// Verify interruption was detected
431431
terminal.read_until("INTERRUPTED").unwrap();
432432

433-
terminal.read_to_end().unwrap();
433+
let _ = terminal.read_to_end().unwrap();
434+
}
435+
436+
#[test]
437+
#[timeout(5000)]
438+
fn read_to_end_returns_exit_status_success() {
439+
let cmd = CommandBuilder::from(command_for_fn!((), |_: ()| {
440+
println!("success");
441+
}));
442+
443+
let mut terminal = Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
444+
let status = terminal.read_to_end().unwrap();
445+
assert!(status.success());
446+
assert_eq!(status.exit_code(), 0);
447+
}
448+
449+
#[test]
450+
#[timeout(5000)]
451+
fn read_to_end_returns_exit_status_nonzero() {
452+
let cmd = CommandBuilder::from(command_for_fn!((), |_: ()| {
453+
std::process::exit(42);
454+
}));
455+
456+
let mut terminal = Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
457+
let status = terminal.read_to_end().unwrap();
458+
assert!(!status.success());
459+
assert_eq!(status.exit_code(), 42);
434460
}

0 commit comments

Comments
 (0)