1111#[ cfg( unix) ]
1212use {
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) ]
1918use {
@@ -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