Skip to content

Commit 027b029

Browse files
authored
Use process tree and kill tree instead of parent only. Fix #90 (#91)
Bump to 2.1.2
1 parent 34ddbd5 commit 027b029

File tree

6 files changed

+94
-29
lines changed

6 files changed

+94
-29
lines changed

exec_helpers/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"ExecResult",
5050
)
5151

52-
__version__ = "2.1.1"
52+
__version__ = "2.1.2"
5353
__author__ = "Alexey Stepanov"
5454
__author_email__ = "penguinolog@gmail.com"
5555
__maintainers__ = {

exec_helpers/_ssh_client_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def __prepare__( # pylint: disable=unused-argument
116116
117117
.. versionadded:: 1.2.0
118118
"""
119-
return collections.OrderedDict() # pragma: no cover
119+
return collections.OrderedDict()
120120

121121
def __call__( # type: ignore
122122
cls: "_MemorizedSSH",

exec_helpers/proc_enums.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def exit_code_to_enum(code: typing.Union[int, ExitCodes]) -> typing.Union[int, E
138138
"""Convert exit code to enum if possible."""
139139
if isinstance(code, int) and code in ExitCodes.__members__.values():
140140
return ExitCodes(code)
141-
return code
141+
return code # pragma: no cover
142142

143143

144144
def exit_codes_to_enums(

exec_helpers/subprocess_runner.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@
2323
import concurrent.futures
2424
import errno
2525
import logging
26+
import platform
2627
import subprocess # nosec # Expected usage
2728
import threading
2829
import typing
2930

31+
import psutil # type: ignore
3032
import threaded
3133

3234
from exec_helpers import api
@@ -37,6 +39,38 @@
3739
logger = 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
4175
class 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

93127
class 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:

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ threaded>=2.0 # Apache-2.0
55
PyYAML>=3.12 # MIT
66
advanced-descriptors>=1.0 # Apache-2.0
77
typing >= 3.6 ; python_version < "3.8"
8+
psutil >= 5.0

0 commit comments

Comments
 (0)