@@ -99,40 +99,41 @@ def kill_process_tree(self, pid: int, include_parent: bool = True) -> bool:
9999 def _kill_with_psutil (self , pid : int , include_parent : bool ) -> bool :
100100 try :
101101 parent = psutil .Process (pid )
102- children = parent .children (recursive = True )
102+ # Use a set to avoid duplicates and handle circular dependencies if any
103+ all_procs = set (parent .children (recursive = True ))
104+ if include_parent :
105+ all_procs .add (parent )
103106
104- # Send SIGTERM/Terminate to everyone
105- for child in children :
106- try :
107- child .terminate ()
108- except psutil .NoSuchProcess :
109- pass
107+ # Sort by depth if possible, or just kill children first
108+ procs = sorted (list (all_procs ), key = lambda p : len (p .parents ()), reverse = True )
110109
111- if include_parent :
110+ # Phase 1: SIGTERM / Terminate
111+ for p in procs :
112112 try :
113- parent .terminate ()
114- except psutil .NoSuchProcess :
113+ if p .is_running ():
114+ p .terminate ()
115+ except (psutil .NoSuchProcess , psutil .AccessDenied ):
115116 pass
116-
117- # Wait for them to exit
118- procs = children + ([parent ] if include_parent else [])
117+
118+ # Wait for they to exit
119119 gone , alive = psutil .wait_procs (procs , timeout = self .timeout / 2 )
120120
121- # Force kill remaining
121+ # Phase 2: Force kill remaining (SIGKILL)
122122 for p in alive :
123123 try :
124- _logger .warning ("Forcing kill on PID %d" , p .pid )
125- p .kill ()
126- except psutil .NoSuchProcess :
124+ if p .is_running ():
125+ _logger .warning ("Forcing kill on PID %d (%s)" , p .pid , p .name ())
126+ p .kill ()
127+ except (psutil .NoSuchProcess , psutil .AccessDenied ):
127128 pass
128129
129- # Final wait to avoid zombies
130+ # Final wait to reap zombies
130131 if alive :
131- psutil .wait_procs (alive , timeout = 1 .0 )
132+ psutil .wait_procs (alive , timeout = 2 .0 )
132133
133- return True
134+ return not any ( p . is_running () for p in procs )
134135 except psutil .NoSuchProcess :
135- return False
136+ return True # Already gone
136137 except Exception as e :
137138 _logger .error ("Error killing process tree with psutil: %s" , e )
138139 return False
@@ -157,32 +158,68 @@ def _kill_with_taskkill(self, pid: int, include_parent: bool) -> bool:
157158 def _kill_with_signals (self , pid : int , include_parent : bool ) -> bool :
158159 """Unix fallback using signals and process groups."""
159160 try :
160- # If start_new_session=True was used, pid is the pgid
161+ # Try to get the process group ID
162+ pgid = - 1
161163 try :
162164 pgid = os .getpgid (pid )
163- if pgid == pid :
164- # Kill the whole group
165+ except Exception :
166+ pass
167+
168+ if pgid > 1 : # Avoid killing system-wide groups
169+ try :
170+ # Terminate the whole group
165171 os .killpg (pgid , signal .SIGTERM )
166- time .sleep (self .timeout / 2 )
172+ # Wait a bit
173+ for _ in range (int (self .timeout * 2 )):
174+ if not self ._is_alive (pid ):
175+ break
176+ time .sleep (0.5 )
177+
167178 if self ._is_alive (pid ):
179+ _logger .warning ("Forcing kill on process group %d" , pgid )
168180 os .killpg (pgid , signal .SIGKILL )
169181 return True
170- except Exception :
171- pass
182+ except Exception as e :
183+ _logger .debug ("Group kill failed, falling back to PID: %s" , e )
184+
185+ # Fallback to killing only the pid and trying to find children via /proc
186+ if not _IS_WINDOWS :
187+ self ._kill_unix_children_fallback (pid )
172188
173- # Fallback to killing only the pid if group killing failed
174189 if include_parent :
175- os .kill (pid , signal .SIGTERM )
176- time .sleep (self .timeout / 2 )
177- if self ._is_alive (pid ):
178- os .kill (pid , signal .SIGKILL )
190+ try :
191+ os .kill (pid , signal .SIGTERM )
192+ time .sleep (self .timeout / 2 )
193+ if self ._is_alive (pid ):
194+ os .kill (pid , signal .SIGKILL )
195+ except ProcessLookupError :
196+ pass
179197 return True
180- except ProcessLookupError :
181- return False
182198 except Exception as e :
183199 _logger .error ("Error killing process with signals: %s" , e )
184200 return False
185201
202+ def _kill_unix_children_fallback (self , ppid : int ) -> None :
203+ """Fallback to kill children on Unix without psutil or pgid."""
204+ try :
205+ # Try using pgrep to find children
206+ result = subprocess .run (
207+ ["pgrep" , "-P" , str (ppid )],
208+ capture_output = True ,
209+ text = True ,
210+ check = False
211+ )
212+ if result .returncode == 0 :
213+ for line in result .stdout .splitlines ():
214+ child_pid = int (line .strip ())
215+ self ._kill_unix_children_fallback (child_pid )
216+ try :
217+ os .kill (child_pid , signal .SIGKILL )
218+ except Exception :
219+ pass
220+ except Exception :
221+ pass
222+
186223 def kill_by_name (self , name : str , ignore_case : bool = True ) -> int :
187224 """
188225 Kill all processes matching the given name.
@@ -217,22 +254,27 @@ def kill_by_name(self, name: str, ignore_case: bool = True) -> int:
217254 return killed_count
218255
219256 def _is_alive (self , pid : int ) -> bool :
257+ if pid <= 0 :
258+ return False
220259 if psutil :
221260 try :
222- return psutil .Process (pid ).is_running ()
261+ p = psutil .Process (pid )
262+ return p .is_running () and p .status () != psutil .STATUS_ZOMBIE
223263 except psutil .NoSuchProcess :
224264 return False
225265
226266 try :
227267 if _IS_WINDOWS :
268+ # Tasklist can be slow, but it's reliable for existence
228269 res = subprocess .run (
229- ["tasklist" , "/FI" , f"PID eq { pid } " ],
270+ ["tasklist" , "/FI" , f"PID eq { pid } " , "/NH" ],
230271 capture_output = True ,
231272 text = True ,
232273 check = False ,
233274 )
234275 return str (pid ) in res .stdout
235276 else :
277+ # On Unix, kill(pid, 0) checks if process exists
236278 os .kill (pid , 0 )
237279 return True
238280 except (ProcessLookupError , PermissionError ):
0 commit comments