@@ -7,8 +7,7 @@ use std::time::{Duration, Instant};
77use crate :: managed_config:: managed_php_fpm_restart_backoff_secs;
88use crate :: managed_process:: ManagedPhpFpmSpawnPlan ;
99use crate :: managed_spawn:: {
10- ensure_managed_php_fpm_binary_spawn_safe, managed_php_fpm_path_env,
11- wait_for_managed_php_fpm_socket,
10+ managed_php_fpm_path_env, open_managed_php_fpm_executable, wait_for_managed_php_fpm_socket,
1211} ;
1312
1413const MANAGED_PHP_FPM_STABLE_RESTART_SECS : u64 = 30 ;
@@ -47,11 +46,11 @@ pub(super) fn spawn_managed_php_fpm_cleanup(
4746 Ok ( mut guard) => guard. take ( ) ,
4847 Err ( poisoned) => poisoned. into_inner ( ) . take ( ) ,
4948 } ;
50- if let Some ( mut child) = child {
49+ if let Some ( child) = child {
5150 // Drop can run on a Tokio worker after the last request releases an
5251 // old runtime snapshot. If cleanup-thread creation fails, do not
5352 // block that worker on Child::wait().
54- let _ = child . kill ( ) ;
53+ signal_managed_php_fpm_group ( & child , rustix :: process :: Signal :: KILL ) ;
5554 }
5655 cleanup_managed_php_fpm_files ( & socket, & config_path, & pid_path) ;
5756 }
@@ -66,63 +65,73 @@ pub(super) fn cleanup_managed_php_fpm_files(socket: &Path, config_path: &Path, p
6665
6766pub ( super ) fn terminate_managed_php_fpm_child ( child : & mut std:: process:: Child ) {
6867 match child. try_wait ( ) {
69- Ok ( Some ( _) ) => return ,
68+ Ok ( Some ( _) ) => {
69+ signal_managed_php_fpm_group ( child, rustix:: process:: Signal :: KILL ) ;
70+ return ;
71+ }
7072 Ok ( None ) => { }
7173 Err ( _) => {
72- let _ = child . kill ( ) ;
74+ signal_managed_php_fpm_group ( child , rustix :: process :: Signal :: KILL ) ;
7375 let _ = child. wait ( ) ;
7476 return ;
7577 }
7678 }
7779
78- let _ = rustix:: process:: kill_process (
79- rustix:: process:: Pid :: from_child ( child) ,
80- rustix:: process:: Signal :: TERM ,
81- ) ;
80+ signal_managed_php_fpm_group ( child, rustix:: process:: Signal :: TERM ) ;
8281 let deadline = Instant :: now ( ) + Duration :: from_secs ( 5 ) ;
8382 loop {
8483 match child. try_wait ( ) {
85- Ok ( Some ( _) ) => return ,
84+ Ok ( Some ( _) ) => {
85+ signal_managed_php_fpm_group ( child, rustix:: process:: Signal :: KILL ) ;
86+ return ;
87+ }
8688 Ok ( None ) if Instant :: now ( ) < deadline => {
8789 std:: thread:: sleep ( Duration :: from_millis ( 100 ) ) ;
8890 }
8991 _ => {
90- let _ = child . kill ( ) ;
92+ signal_managed_php_fpm_group ( child , rustix :: process :: Signal :: KILL ) ;
9193 let _ = child. wait ( ) ;
9294 return ;
9395 }
9496 }
9597 }
9698}
9799
100+ fn signal_managed_php_fpm_group ( child : & std:: process:: Child , signal : rustix:: process:: Signal ) {
101+ let _ = rustix:: process:: kill_process_group ( rustix:: process:: Pid :: from_child ( child) , signal) ;
102+ }
103+
98104pub ( super ) fn spawn_managed_php_fpm_child (
99105 plan : & ManagedPhpFpmSpawnPlan ,
100106 shutdown : Option < & AtomicBool > ,
101107) -> io:: Result < ( std:: process:: Child , Instant ) > {
102- ensure_managed_php_fpm_binary_spawn_safe ( & plan. scope , & plan. binary ) ?;
108+ use std:: os:: unix:: process:: CommandExt as _;
109+
110+ let executable = open_managed_php_fpm_executable ( & plan. scope , & plan. binary ) ?;
103111 let _ = std:: fs:: remove_file ( & plan. socket ) ;
104112 let _ = std:: fs:: remove_file ( & plan. pid_path ) ;
105113
106- let mut child = std:: process:: Command :: new ( & plan. binary )
114+ let mut command = executable. command ( ) ;
115+ command
116+ . process_group ( 0 )
107117 . arg ( "-F" )
108118 . arg ( "-y" )
109119 . arg ( & plan. config_path )
110120 . env_clear ( )
111121 . env ( "PATH" , managed_php_fpm_path_env ( ) )
112122 . stdin ( std:: process:: Stdio :: null ( ) )
113123 . stdout ( std:: process:: Stdio :: null ( ) )
114- . stderr ( std:: process:: Stdio :: null ( ) )
115- . spawn ( )
116- . map_err ( |error| {
117- io:: Error :: new (
118- error. kind ( ) ,
119- format ! (
120- "{}: failed to start managed php-fpm binary {}: {error}" ,
121- plan. scope,
122- plan. binary. display( )
123- ) ,
124- )
125- } ) ?;
124+ . stderr ( std:: process:: Stdio :: null ( ) ) ;
125+ let mut child = command. spawn ( ) . map_err ( |error| {
126+ io:: Error :: new (
127+ error. kind ( ) ,
128+ format ! (
129+ "{}: failed to start managed php-fpm binary {}: {error}" ,
130+ plan. scope,
131+ plan. binary. display( )
132+ ) ,
133+ )
134+ } ) ?;
126135
127136 match wait_for_managed_php_fpm_socket (
128137 & mut child,
@@ -185,12 +194,16 @@ fn run_managed_php_fpm_watchdog(
185194 } ;
186195 match guard. as_mut ( ) . map ( std:: process:: Child :: try_wait) {
187196 Some ( Ok ( Some ( status) ) ) => {
188- * guard = None ;
197+ if let Some ( exited_child) = guard. take ( ) {
198+ signal_managed_php_fpm_group ( & exited_child, rustix:: process:: Signal :: KILL ) ;
199+ }
189200 Some ( format ! ( "exited with status {status}" ) )
190201 }
191202 Some ( Ok ( None ) ) => None ,
192203 Some ( Err ( error) ) => {
193- * guard = None ;
204+ if let Some ( mut failed_child) = guard. take ( ) {
205+ terminate_managed_php_fpm_child ( & mut failed_child) ;
206+ }
194207 Some ( format ! ( "status check failed: {error}" ) )
195208 }
196209 None => Some ( "missing child process handle" . to_owned ( ) ) ,
@@ -274,3 +287,54 @@ fn managed_php_fpm_sleep_until_restart(shutdown: &AtomicBool, restart_failures:
274287 std:: thread:: sleep ( ( deadline - now) . min ( Duration :: from_millis ( 100 ) ) ) ;
275288 }
276289}
290+
291+ #[ cfg( all( test, target_os = "linux" ) ) ]
292+ mod tests {
293+ use super :: terminate_managed_php_fpm_child;
294+ use std:: os:: unix:: process:: CommandExt as _;
295+ use std:: time:: { Duration , Instant } ;
296+
297+ #[ test]
298+ fn forced_cleanup_terminates_managed_process_group ( ) {
299+ let pid_path = fluxheim_common:: test_support:: unique_temp_path ( "php-fpm-worker-pid" ) ;
300+ let script = format ! ( "sleep 30 & echo $! > '{}'; wait" , pid_path. display( ) ) ;
301+ let mut command = std:: process:: Command :: new ( "/bin/sh" ) ;
302+ command
303+ . process_group ( 0 )
304+ . arg ( "-c" )
305+ . arg ( script)
306+ . stdin ( std:: process:: Stdio :: null ( ) )
307+ . stdout ( std:: process:: Stdio :: null ( ) )
308+ . stderr ( std:: process:: Stdio :: null ( ) ) ;
309+ let mut child = command. spawn ( ) . expect ( "spawn process group" ) ;
310+ let deadline = Instant :: now ( ) + Duration :: from_secs ( 2 ) ;
311+ while !pid_path. exists ( ) && Instant :: now ( ) < deadline {
312+ std:: thread:: sleep ( Duration :: from_millis ( 10 ) ) ;
313+ }
314+ let worker_pid = std:: fs:: read_to_string ( & pid_path)
315+ . expect ( "worker pid file" )
316+ . trim ( )
317+ . parse :: < u32 > ( )
318+ . expect ( "worker pid" ) ;
319+
320+ terminate_managed_php_fpm_child ( & mut child) ;
321+
322+ assert ! ( child. try_wait( ) . expect( "master status" ) . is_some( ) ) ;
323+ let worker_proc = std:: path:: PathBuf :: from ( format ! ( "/proc/{worker_pid}/stat" ) ) ;
324+ let deadline = Instant :: now ( ) + Duration :: from_secs ( 2 ) ;
325+ loop {
326+ match std:: fs:: read_to_string ( & worker_proc) {
327+ Ok ( stat) if !stat. contains ( ") Z " ) && Instant :: now ( ) < deadline => {
328+ std:: thread:: sleep ( Duration :: from_millis ( 10 ) ) ;
329+ }
330+ Ok ( stat) => {
331+ assert ! ( stat. contains( ") Z " ) , "worker remained alive: {stat}" ) ;
332+ break ;
333+ }
334+ Err ( error) if error. kind ( ) == std:: io:: ErrorKind :: NotFound => break ,
335+ Err ( error) => panic ! ( "failed to inspect worker process: {error}" ) ,
336+ }
337+ }
338+ let _ = std:: fs:: remove_file ( pid_path) ;
339+ }
340+ }
0 commit comments