diff --git a/src-tauri/crates/berd-call/src/host_session.rs b/src-tauri/crates/berd-call/src/host_session.rs index c358edb38..359145dc8 100644 --- a/src-tauri/crates/berd-call/src/host_session.rs +++ b/src-tauri/crates/berd-call/src/host_session.rs @@ -1729,32 +1729,20 @@ impl SessionProcess { let write_fd = fds[1]; let executable = std::env::current_exe() .map_err(|error| format!("could not locate berd-call executable: {error}"))?; + // Only the write end reaches the child; `pipe` leaves both inheritable. + unsafe { libc::fcntl(read_fd, libc::F_SETFD, libc::FD_CLOEXEC) }; let mut command = Command::new(executable); - command + // No pre_exec, so std uses posix_spawn: forking a process with AppKit's threads can abort the child. + // The host owns Ctrl-C, so terminal signals stay out of the child's process group. + let result = command .args(arguments) .arg("--pcm-output-fd") .arg(write_fd.to_string()) + .process_group(0) .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .stderr(Stdio::inherit()); - unsafe { - command.pre_exec(move || { - // The host owns Ctrl-C: keep terminal signals away from the - // child and restore the default mask the host blocked. - libc::setpgid(0, 0); - let mut signals = std::mem::zeroed::(); - libc::sigemptyset(&mut signals); - libc::pthread_sigmask(libc::SIG_SETMASK, &signals, std::ptr::null_mut()); - libc::close(read_fd); - let flags = libc::fcntl(write_fd, libc::F_GETFD); - if flags < 0 || libc::fcntl(write_fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC) < 0 - { - return Err(std::io::Error::last_os_error()); - } - Ok(()) - }); - } - let result = command.spawn(); + .stderr(Stdio::inherit()) + .spawn(); unsafe { libc::close(write_fd) }; let mut child = match result { Ok(child) => child, diff --git a/src-tauri/crates/berd-call/src/main.rs b/src-tauri/crates/berd-call/src/main.rs index 3a116ed65..eed2d517e 100644 --- a/src-tauri/crates/berd-call/src/main.rs +++ b/src-tauri/crates/berd-call/src/main.rs @@ -614,6 +614,12 @@ fn main() { print_pretty_json(&response).unwrap_or_else(|error| operational_error("stop", error)); } Some("session") => { + // A host blocks its stop signals, and the session inherits that mask. + unsafe { + let mut signals = std::mem::zeroed::(); + libc::sigemptyset(&mut signals); + libc::pthread_sigmask(libc::SIG_SETMASK, &signals, std::ptr::null_mut()); + } let config = parse_or_exit(parse_args(&args), &args); let pcm_output_fd = parse_or_exit(parse_pcm_output_fd(&args), &args); let result = match config.mode { diff --git a/src-tauri/crates/berd-call/tests/session_protocol.rs b/src-tauri/crates/berd-call/tests/session_protocol.rs index 6f0e9e460..391cd6ebf 100644 --- a/src-tauri/crates/berd-call/tests/session_protocol.rs +++ b/src-tauri/crates/berd-call/tests/session_protocol.rs @@ -497,6 +497,44 @@ fn session_rejects_a_read_only_pcm_descriptor_before_hello() { .contains("PCM output file descriptor is not writable")); } +#[test] +fn a_session_stops_on_sigterm_even_when_its_host_blocks_it() { + let (mut command, _pcm, _host) = session_command(); + unsafe { + command.pre_exec(|| { + // The host blocks its stop signals, and a spawned process inherits that mask. + let mut signals = std::mem::zeroed::(); + libc::sigemptyset(&mut signals); + libc::sigaddset(&mut signals, libc::SIGINT); + libc::sigaddset(&mut signals, libc::SIGTERM); + libc::pthread_sigmask(libc::SIG_BLOCK, &signals, std::ptr::null_mut()); + Ok(()) + }); + } + let mut child = command + .args(["--tts-backend", "openai"]) + .env("OPENAI_API_KEY", "test-key-not-used") + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .unwrap(); + unsafe { libc::kill(child.id() as i32, libc::SIGTERM) }; + let deadline = std::time::Instant::now() + Duration::from_secs(5); + let status = loop { + if let Some(status) = child.try_wait().unwrap() { + break status; + } + if std::time::Instant::now() > deadline { + child.kill().unwrap(); + panic!("the session ignored SIGTERM"); + } + std::thread::sleep(Duration::from_millis(20)); + }; + use std::os::unix::process::ExitStatusExt; + assert_eq!(status.signal(), Some(libc::SIGTERM)); +} + #[test] fn framed_hello_reports_input_initialization_failure_before_ready() { let missing =