2121from __future__ import unicode_literals
2222
2323import abc
24- import collections
24+
2525# noinspection PyCompatibility
2626import concurrent .futures
2727import errno
2828import logging
2929import os
30+ import platform
3031import subprocess # nosec # Expected usage
3132import threading
3233import typing # noqa: F401 # pylint: disable=unused-import
3334
3435import six
36+ import psutil # type: ignore
3537import threaded
3638
3739from exec_helpers import api
4446devnull = open (os .devnull ) # subprocess.DEVNULL is py3.3+
4547
4648
49+ # Adopt from:
50+ # https://stackoverflow.com/questions/1230669/subprocess-deleting-child-processes-in-windows
51+ def kill_proc_tree (pid , including_parent = True ): # type: (int, bool) -> None # pragma: no cover
52+ """Kill process tree.
53+
54+ :param pid: PID of parent process to kill
55+ :type pid: int
56+ :param including_parent: kill also parent process
57+ :type including_parent: bool
58+ """
59+ parent = psutil .Process (pid )
60+ children = parent .children (recursive = True )
61+ for child in children : # type: psutil.Process
62+ child .kill ()
63+ _ , alive = psutil .wait_procs (children , timeout = 5 )
64+ for proc in alive : # type: psutil.Process
65+ proc .kill () # 2nd shot
66+ if including_parent :
67+ parent .kill ()
68+ parent .wait (5 )
69+
70+
71+ # Subprocess extra arguments.
72+ # Flags from:
73+ # https://stackoverflow.com/questions/13243807/popen-waiting-for-child-process-even-when-the-immediate-child-has-terminated
74+ kw = {} # type: typing.Dict[str, typing.Any]
75+ if "Windows" == platform .system (): # pragma: no cover
76+ kw ["creationflags" ] = 0x00000200
77+ else : # pragma: no cover
78+ kw ["preexec_fn" ] = os .setsid
79+
80+
81+ # noinspection PyTypeHints
4782class SubprocessExecuteAsyncResult (api .ExecuteAsyncResult ):
4883 """Override original NamedTuple with proper typing."""
4984
@@ -70,19 +105,6 @@ def __call__(cls, *args, **kwargs): # type: (SingletonMeta, typing.Any, typing.
70105 cls ._instances [cls ] = super (SingletonMeta , cls ).__call__ (* args , ** kwargs )
71106 return cls ._instances [cls ]
72107
73- @classmethod
74- def __prepare__ (
75- mcs ,
76- name , # type: str
77- bases , # type: typing.Iterable[typing.Type]
78- ** kwargs # type: typing.Any
79- ): # type: (...) -> collections.OrderedDict # pylint: disable=unused-argument
80- """Metaclass magic for object storage.
81-
82- .. versionadded:: 1.2.0
83- """
84- return collections .OrderedDict () # pragma: no cover
85-
86108
87109class Subprocess (six .with_metaclass (SingletonMeta , api .ExecHelper )):
88110 """Subprocess helper with timeouts and lock-free FIFO."""
@@ -138,17 +160,24 @@ def _exec_command(
138160
139161 .. versionadded:: 1.2.0
140162 """
141- @threaded .threadpooled # type: ignore
163+ @threaded .threadpooled
142164 def poll_stdout (): # type: () -> None
143165 """Sync stdout poll."""
144166 result .read_stdout (src = stdout , log = logger , verbose = verbose )
145167 interface .wait () # wait for the end of execution
146168
147- @threaded .threadpooled # type: ignore
169+ @threaded .threadpooled
148170 def poll_stderr (): # type: () -> None
149171 """Sync stderr poll."""
150172 result .read_stderr (src = stderr , log = logger , verbose = verbose )
151173
174+ def close_streams (): # type: () -> None
175+ """Enforce FIFO closure."""
176+ if stdout is not None and not stdout .closed :
177+ stdout .close ()
178+ if stderr is not None and not stderr .closed :
179+ stderr .close ()
180+
152181 # Store command with hidden data
153182 cmd_for_log = self ._mask_command (cmd = command , log_mask_re = log_mask_re )
154183
@@ -167,11 +196,13 @@ def poll_stderr(): # type: () -> None
167196 # Process closed?
168197 if exit_code is not None :
169198 result .exit_code = exit_code
199+ close_streams ()
170200 return result
171201 # Kill not ended process and wait for close
172202 try :
203+ kill_proc_tree (interface .pid , including_parent = False ) # kill -9 for all subprocesses
173204 interface .kill () # kill -9
174- concurrent .futures .wait ([stdout_future , stderr_future ], timeout = 5 )
205+ concurrent .futures .wait ([stdout_future , stderr_future ], timeout = 0. 5 )
175206 # Force stop cycle if no exit code after kill
176207 stdout_future .cancel ()
177208 stderr_future .cancel ()
@@ -182,6 +213,8 @@ def poll_stderr(): # type: () -> None
182213 result .exit_code = exit_code
183214 return result
184215 raise # Some other error
216+ finally :
217+ close_streams ()
185218
186219 wait_err_msg = _log_templates .CMD_WAIT_ERROR .format (result = result , timeout = timeout )
187220 logger .debug (wait_err_msg )
@@ -244,6 +277,7 @@ def execute_async(
244277 cwd = kwargs .get ("cwd" , None ),
245278 env = kwargs .get ("env" , None ),
246279 universal_newlines = False ,
280+ ** kw
247281 )
248282
249283 if stdin is None :
@@ -264,6 +298,7 @@ def execute_async(
264298 elif exc .errno in (errno .EPIPE , errno .ESHUTDOWN ): # pragma: no cover
265299 self .logger .warning ("STDIN Send failed: broken PIPE" )
266300 else :
301+ kill_proc_tree (process .pid )
267302 process .kill ()
268303 raise
269304 try :
0 commit comments