2323import concurrent .futures
2424import errno
2525import logging
26+ import platform
2627import subprocess # nosec # Expected usage
2728import threading
2829import typing
2930
31+ import psutil # type: ignore
3032import threaded
3133
3234from exec_helpers import api
3739logger = logging .getLogger (__name__ ) # type: logging.Logger
3840
3941
42+ # Adopt from:
43+ # https://stackoverflow.com/questions/1230669/subprocess-deleting-child-processes-in-windows
44+ def kill_proc_tree (pid : int , including_parent : bool = True ) -> None : # pragma: no cover
45+ """Kill process tree.
46+
47+ :param pid: PID of parent process to kill
48+ :type pid: int
49+ :param including_parent: kill also parent process
50+ :type including_parent: bool
51+ """
52+ parent = psutil .Process (pid )
53+ children = parent .children (recursive = True )
54+ for child in children : # type: psutil.Process
55+ child .kill ()
56+ _ , alive = psutil .wait_procs (children , timeout = 5 )
57+ for proc in alive : # type: psutil.Process
58+ proc .kill () # 2nd shot
59+ if including_parent :
60+ parent .kill ()
61+ parent .wait (5 )
62+
63+
64+ # Subprocess extra arguments.
65+ # Flags from:
66+ # https://stackoverflow.com/questions/13243807/popen-waiting-for-child-process-even-when-the-immediate-child-has-terminated
67+ kw = {} # type: typing.Dict[str, typing.Any]
68+ if "Windows" == platform .system (): # pragma: no cover
69+ kw ["creationflags" ] = 0x00000200
70+ else : # pragma: no cover
71+ kw ["start_new_session" ] = True
72+
73+
4074# noinspection PyTypeHints
4175class SubprocessExecuteAsyncResult (api .ExecuteAsyncResult ):
4276 """Override original NamedTuple with proper typing."""
@@ -87,7 +121,7 @@ def __prepare__( # pylint: disable=unused-argument
87121
88122 .. versionadded:: 1.2.0
89123 """
90- return collections .OrderedDict () # pragma: no cover
124+ return collections .OrderedDict ()
91125
92126
93127class Subprocess (api .ExecHelper , metaclass = SingletonMeta ):
@@ -172,16 +206,17 @@ def poll_stderr() -> None:
172206 except subprocess .TimeoutExpired :
173207 exit_code = interface .poll () # Update exit code
174208
175- concurrent .futures .wait ([stdout_future , stderr_future ], timeout = 1 ) # Minimal timeout to complete polling
209+ concurrent .futures .wait ([stdout_future , stderr_future ], timeout = 0.5 ) # Minimal timeout to complete polling
176210
177211 # Process closed?
178212 if exit_code is not None :
179213 result .exit_code = exit_code
180214 return result
181215 # Kill not ended process and wait for close
182216 try :
217+ kill_proc_tree (interface .pid , including_parent = False ) # kill -9 for all subprocesses
183218 interface .kill () # kill -9
184- concurrent .futures .wait ([stdout_future , stderr_future ], timeout = 5 )
219+ concurrent .futures .wait ([stdout_future , stderr_future ], timeout = 0. 5 )
185220 # Force stop cycle if no exit code after kill
186221 stdout_future .cancel ()
187222 stderr_future .cancel ()
@@ -254,6 +289,7 @@ def execute_async(
254289 cwd = kwargs .get ("cwd" , None ),
255290 env = kwargs .get ("env" , None ),
256291 universal_newlines = False ,
292+ ** kw
257293 )
258294
259295 if stdin is None :
@@ -274,6 +310,7 @@ def execute_async(
274310 elif exc .errno in (errno .EPIPE , errno .ESHUTDOWN ): # pragma: no cover
275311 self .logger .warning ("STDIN Send failed: broken PIPE" )
276312 else :
313+ kill_proc_tree (process .pid )
277314 process .kill ()
278315 raise
279316 try :
0 commit comments