Skip to content

Commit d04f7e6

Browse files
committed
fix(manager): stop SIGTERMing stale PIDs after module exit
The parent-death pipe never worked: both ends lived in aw-tauri, so the monitor thread died with the parent and could not signal anything. What actually closed the write end was the module thread returning after its child had already exited, at which point the monitor sent SIGTERM to the old PID, which the OS may have already reused for an unrelated process. The pipe was also created without O_CLOEXEC, so every later module inherited earlier modules' pipe ends. Remove it. On Linux, use PR_SET_PDEATHSIG in pre_exec, which does what the pipe was meant to do. Windows keeps its job object; macOS has no equivalent, which is no regression since the pipe never worked there.
1 parent 85d2b69 commit d04f7e6

1 file changed

Lines changed: 29 additions & 104 deletions

File tree

‎src-tauri/src/manager.rs‎

Lines changed: 29 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@
1111
#[cfg(unix)]
1212
use {
1313
nix::sys::signal::{self, Signal},
14-
nix::unistd::{close, pipe, read, Pid},
14+
nix::unistd::Pid,
1515
std::os::unix::fs::PermissionsExt,
16-
std::os::unix::io::IntoRawFd,
1716
};
1817
#[cfg(windows)]
1918
use {
@@ -420,51 +419,31 @@ fn create_job_object() -> Result<HANDLE, std::io::Error> {
420419
}
421420
}
422421

423-
#[cfg(unix)]
424-
fn monitor_parent_process(child_pid: u32, read_fd: i32) {
425-
thread::spawn(move || {
426-
// Read from the pipe - when parent dies, the write end is closed by the OS
427-
// and we'll get EOF (read returns 0)
428-
let mut buf = [0u8; 1];
429-
loop {
430-
match read(read_fd, &mut buf) {
431-
Ok(0) => {
432-
// EOF means parent died (write end of pipe closed)
433-
info!(
434-
"Parent process died (pipe closed), terminating child {}",
435-
child_pid
436-
);
437-
438-
// Close our read end of the pipe
439-
let _ = close(read_fd);
440-
441-
// Send SIGTERM to the child process
442-
if let Err(e) = send_sigterm(child_pid) {
443-
error!("Failed to terminate child process {}: {}", child_pid, e);
444-
} else {
445-
debug!("Successfully sent SIGTERM to child process {}", child_pid);
446-
}
447-
break;
448-
}
449-
Ok(_) => {
450-
// Should never receive data, but if we do, just continue monitoring
451-
// This handles spurious wake-ups gracefully
452-
}
453-
Err(e) => {
454-
// Error reading from pipe - parent likely died
455-
error!("Error reading from parent monitor pipe: {}", e);
456-
let _ = close(read_fd);
457-
458-
if let Err(e) = send_sigterm(child_pid) {
459-
error!("Failed to terminate child process {}: {}", child_pid, e);
460-
} else {
461-
debug!("Successfully sent SIGTERM to child process {}", child_pid);
462-
}
463-
break;
464-
}
422+
/// Have the kernel send SIGTERM to the module if aw-tauri dies.
423+
///
424+
/// PDEATHSIG fires when the *thread* that spawned the child exits, not the process. That holds
425+
/// here because each module thread blocks on its child until the child exits.
426+
///
427+
/// Linux only: macOS has no equivalent, and on Windows the job object covers this.
428+
#[cfg(target_os = "linux")]
429+
fn kill_on_parent_death(command: &mut Command) {
430+
use nix::sys::prctl;
431+
use std::os::unix::process::CommandExt;
432+
433+
let parent = nix::unistd::getpid();
434+
// SAFETY: prctl and getppid are async-signal-safe syscalls and the closure doesn't allocate.
435+
unsafe {
436+
command.pre_exec(move || {
437+
prctl::set_pdeathsig(Signal::SIGTERM)?;
438+
// aw-tauri may have died between fork and prctl, in which case no signal will come.
439+
if nix::unistd::getppid() != parent {
440+
return Err(std::io::Error::other(
441+
"aw-tauri exited before module started",
442+
));
465443
}
466-
}
467-
});
444+
Ok(())
445+
});
446+
}
468447
}
469448

470449
// Splits a configured args string, warning (and falling back to no args) on malformed shell
@@ -723,21 +702,10 @@ fn start_generic_module_thread(
723702
}
724703
};
725704

726-
// Create pipe for Unix parent death detection
727-
#[cfg(unix)]
728-
let (pipe_read_fd, _pipe_write_keeper) = match pipe() {
729-
Ok((read_fd, write_fd)) => {
730-
// read_fd is read end, write_fd stays open in parent and auto-closes when parent dies
731-
(read_fd.into_raw_fd(), Some(std::fs::File::from(write_fd)))
732-
}
733-
Err(e) => {
734-
error!("Failed to create pipe for parent monitoring: {}", e);
735-
(-1, None)
736-
}
737-
};
738-
739705
// Start the child process
740706
let mut command = Command::new(&path);
707+
#[cfg(target_os = "linux")]
708+
kill_on_parent_death(&mut command);
741709

742710
// Use custom args if provided, otherwise only pass port arg if it's not the default (5600)
743711
if let Some(ref args) = custom_args {
@@ -762,10 +730,6 @@ fn start_generic_module_thread(
762730
CloseHandle(handle);
763731
}
764732
}
765-
#[cfg(unix)]
766-
if pipe_read_fd >= 0 {
767-
let _ = close(pipe_read_fd);
768-
}
769733
return;
770734
}
771735
};
@@ -787,12 +751,6 @@ fn start_generic_module_thread(
787751
}
788752
}
789753

790-
// On Unix, start parent process monitor with pipe
791-
#[cfg(unix)]
792-
if pipe_read_fd >= 0 {
793-
monitor_parent_process(child_pid, pipe_read_fd);
794-
}
795-
796754
// Send a message to the manager that the module has started
797755
tx.send(ModuleMessage::Started {
798756
name: name.to_string(),
@@ -841,22 +799,10 @@ fn start_notify_module_thread(
841799
}
842800
};
843801

844-
// Create pipe for Unix parent death detection
845-
// Create pipe for Unix parent death detection
846-
#[cfg(unix)]
847-
let (pipe_read_fd, _pipe_write_keeper) = match pipe() {
848-
Ok((read_fd, write_fd)) => {
849-
// read_fd is read end, write_fd stays open in parent and auto-closes when parent dies
850-
(read_fd.into_raw_fd(), Some(std::fs::File::from(write_fd)))
851-
}
852-
Err(e) => {
853-
error!("Failed to create pipe for parent monitoring: {}", e);
854-
(-1, None)
855-
}
856-
};
857-
858802
// Start the child process with --output-only flag
859803
let mut command = Command::new(&path);
804+
#[cfg(target_os = "linux")]
805+
kill_on_parent_death(&mut command);
860806

861807
// Always add --output-only flag for aw-notify
862808
let mut args = vec!["--output-only".to_string()];
@@ -895,10 +841,6 @@ fn start_notify_module_thread(
895841
CloseHandle(handle);
896842
}
897843
}
898-
#[cfg(unix)]
899-
if pipe_read_fd >= 0 {
900-
let _ = close(pipe_read_fd);
901-
}
902844
// Fallback to generic module handler to avoid recursion
903845
start_generic_module_thread(name, path, custom_args, server_port, tx);
904846
return;
@@ -910,17 +852,11 @@ fn start_notify_module_thread(
910852
CloseHandle(handle);
911853
}
912854
}
913-
#[cfg(unix)]
914-
if pipe_read_fd >= 0 {
915-
let _ = close(pipe_read_fd);
916-
}
917855
return;
918856
}
919857
}
920858
};
921859

922-
let child_pid = child.id();
923-
924860
// On Windows, assign child to job object
925861
#[cfg(windows)]
926862
if let Some(handle) = job_handle {
@@ -936,12 +872,6 @@ fn start_notify_module_thread(
936872
}
937873
}
938874

939-
// On Unix, start parent process monitor with pipe
940-
#[cfg(unix)]
941-
if pipe_read_fd >= 0 {
942-
monitor_parent_process(child_pid, pipe_read_fd);
943-
}
944-
945875
// Report the caller-provided args, NOT the internally-expanded `args`: the
946876
// `--output-only`/`--port` flags are re-added on every start, so storing the expanded
947877
// command line would re-inject them and compound across a stop/start or restart (the
@@ -1010,11 +940,6 @@ fn start_notify_module_thread(
1010940
CloseHandle(handle);
1011941
}
1012942
}
1013-
#[cfg(unix)]
1014-
if pipe_read_fd >= 0 {
1015-
let _ = close(pipe_read_fd);
1016-
}
1017-
1018943
// Fallback to generic module handler
1019944
start_generic_module_thread(name, path, custom_args, server_port, tx);
1020945
return;

0 commit comments

Comments
 (0)