From 0e7f3095c3d7fc16bf6a10be6c6fb0856ebf748f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 14 Oct 2026 12:15:17 -0400 Subject: [PATCH] T04: close the pty master before reaping, so a program ignoring SIGHUP ends --- src/term/pty.rs | 21 ++++++++++++++------- src/term/unix.rs | 14 ++++++++------ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/term/pty.rs b/src/term/pty.rs index b26787a..7f84f1e 100644 --- a/src/term/pty.rs +++ b/src/term/pty.rs @@ -269,14 +269,11 @@ mod tests { assert_eq!(ran.text, format!("8 {}", u16::MAX / 8)); } - #[test] - fn closing_a_session_hangs_up_its_program() { + /// Starts `script`, waits for it to print READY, closes it, and waits for the reap's log. + fn close_after_ready(script: &str, reaped: &str) { crate::testlog::init(); let launch = Launch { - program: Some(( - "/bin/sh".into(), - vec!["-c".into(), "printf READY; exec cat".into()], - )), + program: Some(("/bin/sh".into(), vec!["-c".into(), script.into()])), cwd: std::env::temp_dir(), env: Vec::new(), }; @@ -295,7 +292,7 @@ mod tests { }); let pid = pty.pid; drop(pty); - let hung_up = format!("pty {pid}: hung up, signal: 1 (SIGHUP)"); + let hung_up = format!("pty {pid}: hung up, {reaped}"); let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5); while !crate::testlog::logged(&hung_up) { assert!(std::time::Instant::now() < deadline, "no `{hung_up}`"); @@ -306,6 +303,16 @@ mod tests { assert!(!alive, "the program was reaped"); } + #[test] + fn closing_a_session_hangs_up_its_program() { + close_after_ready("printf READY; exec cat", "signal: 1 (SIGHUP)"); + } + + #[test] + fn a_program_that_ignores_sighup_ends_when_the_pty_closes() { + close_after_ready("trap '' HUP; printf READY; exec cat", "exit status: 0"); + } + #[test] fn a_missing_folder_stops_the_start() { let launch = Launch { diff --git a/src/term/unix.rs b/src/term/unix.rs index 554ddfc..96ccea8 100644 --- a/src/term/unix.rs +++ b/src/term/unix.rs @@ -3,7 +3,7 @@ use std::{ ffi::{CStr, CString}, fs::File, io::{self, ErrorKind, Read}, - mem::MaybeUninit, + mem::{ManuallyDrop, MaybeUninit}, os::{ fd::AsRawFd, unix::{ffi::OsStrExt, net::UnixStream, process::CommandExt}, @@ -35,7 +35,7 @@ const CHILD: usize = 1; /// A program on a pseudo-terminal, started here so a failed `chdir` fails the start. pub(crate) struct Pty { child: Child, - file: File, + file: ManuallyDrop, signals: UnixStream, sig: SigId, } @@ -119,7 +119,7 @@ pub(crate) fn spawn( } Ok(Pty { child, - file: File::from(master), + file: ManuallyDrop::new(File::from(master)), signals, sig, }) @@ -214,7 +214,7 @@ impl EventedReadWrite for Pty { ) -> io::Result<()> { interest.key = READ_WRITE; unsafe { - poll.add_with_mode(&self.file, interest, mode)?; + poll.add_with_mode(&*self.file, interest, mode)?; poll.add_with_mode(&self.signals, Event::readable(CHILD), PollMode::Level) } } @@ -226,12 +226,12 @@ impl EventedReadWrite for Pty { mode: PollMode, ) -> io::Result<()> { interest.key = READ_WRITE; - poll.modify_with_mode(&self.file, interest, mode)?; + poll.modify_with_mode(&*self.file, interest, mode)?; poll.modify_with_mode(&self.signals, Event::readable(CHILD), PollMode::Level) } fn deregister(&mut self, poll: &Arc) -> io::Result<()> { - poll.delete(&self.file)?; + poll.delete(&*self.file)?; poll.delete(&self.signals) } @@ -284,6 +284,8 @@ impl Drop for Pty { fn drop(&mut self) { unsafe { libc::kill(self.child.id() as libc::pid_t, libc::SIGHUP) }; unregister(self.sig); + // Closed before the wait, so a program that ignores SIGHUP reads EOF. + unsafe { ManuallyDrop::drop(&mut self.file) }; match self.child.wait() { Ok(status) => log::info!("pty {}: hung up, {status}", self.id()), Err(error) => log::error!("pty {}: cannot reap the program: {error}", self.id()),