@@ -879,4 +879,89 @@ mod tests {
879879 "This is a test doc!\n "
880880 ) ;
881881 }
882+
883+ /// End-to-end test simulating the PyInstaller scenario from issue #1332.
884+ ///
885+ /// PyInstaller wraps the real application in a thin bootloader process.
886+ /// Without process groups, killing the bootloader orphans the real app.
887+ /// This test verifies that with `process_group` enabled, killing the
888+ /// wrapper also kills the grandchild process.
889+ #[ cfg( not( windows) ) ]
890+ #[ test]
891+ fn test_pyinstaller_simulation_without_process_group ( ) {
892+ // Without process_group: killing the wrapper does NOT kill the grandchild.
893+ let cmd = Command :: new ( "sh" ) . args ( [ "test/pyinstaller_sim.sh" ] ) ;
894+ let ( mut rx, child) = cmd. spawn ( ) . unwrap ( ) ;
895+
896+ // Collect the child PID from stdout
897+ let grandchild_pid = tauri:: async_runtime:: block_on ( async {
898+ let mut pid = None ;
899+ while let Some ( event) = rx. recv ( ) . await {
900+ if let CommandEvent :: Stdout ( line) = & event {
901+ let line_str = String :: from_utf8_lossy ( line) ;
902+ if let Some ( rest) = line_str. strip_prefix ( "CHILD_PID=" ) {
903+ pid = rest. trim ( ) . parse :: < i32 > ( ) . ok ( ) ;
904+ }
905+ }
906+ if pid. is_some ( ) {
907+ break ;
908+ }
909+ }
910+ pid. expect ( "should have received CHILD_PID from script" )
911+ } ) ;
912+
913+ // Verify the grandchild is running
914+ let ret = unsafe { libc:: kill ( grandchild_pid, 0 ) } ;
915+ assert_eq ! ( ret, 0 , "grandchild should be running before kill" ) ;
916+
917+ // Kill just the direct child (no process group)
918+ child. kill ( ) . unwrap ( ) ;
919+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 100 ) ) ;
920+
921+ // The grandchild is STILL alive — this is the bug
922+ let ret = unsafe { libc:: kill ( grandchild_pid, 0 ) } ;
923+ assert_eq ! ( ret, 0 , "grandchild should survive when process_group is off" ) ;
924+
925+ // Clean up the orphaned grandchild
926+ unsafe { libc:: kill ( grandchild_pid, libc:: SIGKILL ) } ;
927+ }
928+
929+ #[ cfg( not( windows) ) ]
930+ #[ test]
931+ fn test_pyinstaller_simulation_with_process_group ( ) {
932+ // With process_group: killing the wrapper ALSO kills the grandchild.
933+ let cmd = Command :: new ( "sh" )
934+ . args ( [ "test/pyinstaller_sim.sh" ] )
935+ . set_process_group ( true ) ;
936+ let ( mut rx, child) = cmd. spawn ( ) . unwrap ( ) ;
937+
938+ // Collect the grandchild PID from stdout
939+ let grandchild_pid = tauri:: async_runtime:: block_on ( async {
940+ let mut pid = None ;
941+ while let Some ( event) = rx. recv ( ) . await {
942+ if let CommandEvent :: Stdout ( line) = & event {
943+ let line_str = String :: from_utf8_lossy ( line) ;
944+ if let Some ( rest) = line_str. strip_prefix ( "CHILD_PID=" ) {
945+ pid = rest. trim ( ) . parse :: < i32 > ( ) . ok ( ) ;
946+ }
947+ }
948+ if pid. is_some ( ) {
949+ break ;
950+ }
951+ }
952+ pid. expect ( "should have received CHILD_PID from script" )
953+ } ) ;
954+
955+ // Verify the grandchild is running
956+ let ret = unsafe { libc:: kill ( grandchild_pid, 0 ) } ;
957+ assert_eq ! ( ret, 0 , "grandchild should be running before kill" ) ;
958+
959+ // Kill the process group
960+ child. kill ( ) . unwrap ( ) ;
961+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 100 ) ) ;
962+
963+ // The grandchild should now be DEAD
964+ let ret = unsafe { libc:: kill ( grandchild_pid, 0 ) } ;
965+ assert_ne ! ( ret, 0 , "grandchild should be killed when process_group is on" ) ;
966+ }
882967}
0 commit comments