aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorakbash-bot <300245827+akbash-bot@users.noreply.github.com>2026-09-06 22:03:14 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-09-06 22:03:14 +0300
commit2329bc181d1d21a365704f261e11ce0ee86524a5 (patch)
treec27dab17e4577a1f017bb4f3c8cc1ec48cfeac49 /src
parent9d111db9c83823eb4ce8623d9707e28153b01334 (diff)
fix: resolve queued submissions when pty actor exits
refs #3685
Diffstat (limited to 'src')
-rw-r--r--src/pty/actor/unix.rs108
1 files changed, 104 insertions, 4 deletions
diff --git a/src/pty/actor/unix.rs b/src/pty/actor/unix.rs
index d7a8bb78..4d57dd16 100644
--- a/src/pty/actor/unix.rs
+++ b/src/pty/actor/unix.rs
@@ -570,10 +570,7 @@ impl PtyIoActorRunner {
}
}
- self.fail_active_submission(std::io::Error::new(
- std::io::ErrorKind::BrokenPipe,
- "PTY actor closed during input submission",
- ));
+ self.close_input_queue();
if let Some(on_reader_exit) = self.on_reader_exit.take() {
on_reader_exit();
}
@@ -921,6 +918,16 @@ impl PtyIoActorRunner {
}
}
+ fn close_input_queue(&mut self) {
+ self.data_rx.close();
+ self.fail_active_submission(input_submission_closed_error());
+ while let Some(command) = self.data_rx.blocking_recv() {
+ if let PtyIoDataCommand::SubmitUserInput { reply, .. } = command {
+ let _ = reply.send(Err(input_submission_closed_error()));
+ }
+ }
+ }
+
fn flush_pending_writes_once(&mut self) -> std::io::Result<Option<SubmissionBoundary>> {
while let Some(write) = self.pending_writes.front() {
let chunk = &write.bytes[self.current_write_offset..];
@@ -1019,6 +1026,13 @@ impl PtyIoActorRunner {
}
}
+fn input_submission_closed_error() -> std::io::Error {
+ std::io::Error::new(
+ std::io::ErrorKind::BrokenPipe,
+ "PTY actor closed during input submission",
+ )
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -1263,6 +1277,92 @@ mod tests {
}
#[test]
+ fn actor_fails_buffered_submissions_on_exit() {
+ let (handle, mut peer, _read_rx) = actor_with_socket_pair(false);
+ let active = handle
+ .queue_user_input_submission(
+ Bytes::from_static(b"first"),
+ Bytes::from_static(b"\r"),
+ Duration::from_secs(1),
+ )
+ .expect("first submission queues");
+ let mut prompt = [0; 5];
+ peer.read_exact(&mut prompt).expect("peer receives prompt");
+ let buffered = handle
+ .queue_user_input_submission(
+ Bytes::from_static(b"second"),
+ Bytes::from_static(b"\r"),
+ Duration::ZERO,
+ )
+ .expect("second submission queues");
+
+ drop(peer);
+ let active_err = active
+ .recv_timeout(Duration::from_secs(1))
+ .expect("actor reports active submission")
+ .expect_err("peer closure fails active submission");
+ let buffered_err = buffered
+ .recv_timeout(Duration::from_secs(1))
+ .expect("actor reports buffered submission")
+ .expect_err("peer closure fails buffered submission");
+
+ assert_eq!(active_err.kind(), std::io::ErrorKind::BrokenPipe);
+ assert_eq!(buffered_err.kind(), std::io::ErrorKind::BrokenPipe);
+ }
+
+ #[test]
+ fn actor_rejects_submission_after_io_loop_exits() {
+ let (actor_socket, peer) = UnixStream::pair().expect("socket pair");
+ actor_socket
+ .set_nonblocking(true)
+ .expect("actor socket nonblocking");
+ let owned = unsafe { OwnedFd::from_raw_fd(actor_socket.into_raw_fd()) };
+ let handle_slot = Arc::new(Mutex::new(None::<PtyIoActorHandle>));
+ let (attempt_tx, attempt_rx) = std_mpsc::channel();
+ let config = PtyIoActorConfig {
+ pane_id: 1,
+ master_fd: owned,
+ initially_quiesced: false,
+ on_read: Box::new(|_| PtyReadResult::empty()),
+ on_reader_exit: Some(Box::new({
+ let handle_slot = Arc::clone(&handle_slot);
+ move || {
+ let handle = handle_slot
+ .lock()
+ .unwrap_or_else(|poisoned| poisoned.into_inner())
+ .as_ref()
+ .expect("actor handle installed")
+ .clone();
+ let attempt = handle.queue_user_input_submission(
+ Bytes::from_static(b"prompt"),
+ Bytes::from_static(b"\r"),
+ Duration::ZERO,
+ );
+ attempt_tx.send(attempt).expect("attempt receiver alive");
+ }
+ })),
+ };
+ let handle = PtyIoActor::spawn(config).expect("actor spawn");
+ *handle_slot
+ .lock()
+ .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(handle);
+
+ drop(peer);
+ let err = match attempt_rx
+ .recv_timeout(Duration::from_secs(1))
+ .expect("reader exit callback attempts submission")
+ {
+ Ok(completion) => completion
+ .recv_timeout(Duration::from_secs(1))
+ .expect("actor reports submission")
+ .expect_err("closed actor rejects submission"),
+ Err(err) => err,
+ };
+
+ assert_eq!(err.kind(), std::io::ErrorKind::BrokenPipe);
+ }
+
+ #[test]
fn actor_wakes_idle_poll_for_user_input() {
let (poll_tx, poll_rx) = std_mpsc::channel();
let (handle, mut peer, _read_rx) =