44import re
55import os
66import sys
7+ import time
8+ import signal
79from os .path import splitext , join
810import subprocess
911from datetime import datetime
@@ -773,7 +775,61 @@ def get_file_contents(filepath):
773775 else :
774776 return ''
775777
776- def run_subtool_noread (cmd , cwd = None , timeout = None ):
778+ try :
779+ import psutil
780+ except ImportError :
781+ psutil = None
782+
783+ def _kill_process_tree (pid , timeout = 3.0 ):
784+ """Kill process tree rooted at pid. Uses psutil if available, otherwise best-effort."""
785+ if psutil :
786+ try :
787+ parent = psutil .Process (pid )
788+ except psutil .NoSuchProcess :
789+ return
790+ children = parent .children (recursive = True )
791+ # send sig to children first
792+ for p in children :
793+ try :
794+ p .send_signal (signal .SIGKILL )
795+ except Exception :
796+ try :
797+ p .kill ()
798+ except Exception :
799+ pass
800+ # then parent
801+ try :
802+ parent .send_signal (sig )
803+ except Exception :
804+ try :
805+ parent .kill ()
806+ except Exception :
807+ pass
808+ # wait up to timeout for processes to disappear
809+ gone , alive = psutil .wait_procs ([parent ] + children , timeout = timeout )
810+ return alive
811+ else :
812+ # Fallback: on Unix killpg, on Windows try proc.kill
813+ if sys .platform == "win32" :
814+ try :
815+ os .kill (pid , signal .SIGTERM )
816+ except Exception :
817+ try :
818+ os .kill (pid , signal .SIGKILL )
819+ except Exception :
820+ pass
821+ else :
822+ try :
823+ os .killpg (os .getpgid (pid ), signal .SIGTERM )
824+ except Exception :
825+ try :
826+ os .kill (pid , signal .SIGTERM )
827+ except Exception :
828+ pass
829+ # no reliable wait for children without psutil
830+ return []
831+
832+ def run_subtool_noread (cmd , cwd = None , timeout = None , kill_timeout = 3.0 ):
777833 """Run external command without reading output; kill whole process group on timeout.
778834 Returns (returncode, timed_out: bool).
779835 """
@@ -790,7 +846,7 @@ def run_subtool_noread(cmd, cwd=None, timeout=None):
790846 preexec_fn = os .setpgrp # start new session -> new process group
791847
792848 try :
793- process = subprocess .Popen (
849+ proc = subprocess .Popen (
794850 cmd ,
795851 stdout = subprocess .PIPE ,
796852 stderr = subprocess .PIPE ,
@@ -803,25 +859,50 @@ def run_subtool_noread(cmd, cwd=None, timeout=None):
803859 )
804860
805861 try :
806- process .communicate (timeout = timeout )
807- return process .returncode , False
862+ proc .communicate (timeout = timeout )
863+ return proc .returncode , False
808864 except subprocess .TimeoutExpired :
809- # Kill the whole process group
865+ # escalate: try gentle signal first
810866 try :
811867 if sys .platform == "win32" :
812- # send CTRL_BREAK_EVENT to the process group
813- process .send_signal (signal .CTRL_BREAK_EVENT )
868+ try :
869+ proc .send_signal (signal .CTRL_BREAK_EVENT )
870+ except Exception :
871+ proc .terminate ()
814872 else :
815- os .killpg (os .getpgid (process .pid ), signal .SIGKILL )
873+ os .killpg (os .getpgid (proc .pid ), signal .SIGTERM )
874+ except Exception :
875+ try :
876+ proc .terminate ()
877+ except Exception :
878+ pass
879+
880+ # wait a short time
881+ try :
882+ proc .wait (timeout = kill_timeout )
816883 except Exception :
817- # fallback to killing the process
884+ # still alive -> force kill whole tree
885+ _kill_process_tree (proc .pid , sig = signal .SIGKILL , timeout = kill_timeout )
818886 try :
819- process . kill ( )
887+ proc . wait ( timeout = kill_timeout )
820888 except Exception :
821889 pass
822- # Wait for termination
823- process .wait ()
824- return process .returncode if process .returncode is not None else - 1 , True
890+
891+ return (proc .returncode if proc .returncode is not None else - 1 ), True
892+ finally :
893+ # close fds
894+ try :
895+ proc .stdout .close ()
896+ except Exception :
897+ pass
898+ try :
899+ proc .stderr .close ()
900+ except Exception :
901+ pass
902+ try :
903+ proc .stdin .close ()
904+ except Exception :
905+ pass
825906
826907 except Exception as e :
827908 # unicode/read error safe-guard
0 commit comments