From f25313af9d0ba897b507ab188442e28b437a76ad Mon Sep 17 00:00:00 2001 From: Peter Willendrup Date: Sun, 22 Mar 2026 19:45:04 +0100 Subject: [PATCH 1/2] Even more elaborate process-control / termination logic --- tools/Python/mccodelib/utils.py | 107 ++++++++++++++++++++++++++++---- 1 file changed, 94 insertions(+), 13 deletions(-) diff --git a/tools/Python/mccodelib/utils.py b/tools/Python/mccodelib/utils.py index da87eb9f5..5c6a3ca85 100644 --- a/tools/Python/mccodelib/utils.py +++ b/tools/Python/mccodelib/utils.py @@ -4,6 +4,8 @@ import re import os import sys +import time +import signal from os.path import splitext, join import subprocess from datetime import datetime @@ -773,7 +775,61 @@ def get_file_contents(filepath): else: return '' -def run_subtool_noread(cmd, cwd=None, timeout=None): +try: + import psutil +except ImportError: + psutil = None + +def _kill_process_tree(pid, sig=signal.SIGKILL, timeout=3.0): + """Kill process tree rooted at pid. Uses psutil if available, otherwise best-effort.""" + if psutil: + try: + parent = psutil.Process(pid) + except psutil.NoSuchProcess: + return + children = parent.children(recursive=True) + # send sig to children first + for p in children: + try: + p.send_signal(sig) + except Exception: + try: + p.kill() + except Exception: + pass + # then parent + try: + parent.send_signal(sig) + except Exception: + try: + parent.kill() + except Exception: + pass + # wait up to timeout for processes to disappear + gone, alive = psutil.wait_procs([parent] + children, timeout=timeout) + return alive + else: + # Fallback: on Unix killpg, on Windows try proc.kill + if sys.platform == "win32": + try: + os.kill(pid, signal.SIGTERM) + except Exception: + try: + os.kill(pid, signal.SIGKILL) + except Exception: + pass + else: + try: + os.killpg(os.getpgid(pid), signal.SIGTERM) + except Exception: + try: + os.kill(pid, signal.SIGTERM) + except Exception: + pass + # no reliable wait for children without psutil + return [] + +def run_subtool_noread(cmd, cwd=None, timeout=None, kill_timeout=3.0): """Run external command without reading output; kill whole process group on timeout. Returns (returncode, timed_out: bool). """ @@ -790,7 +846,7 @@ def run_subtool_noread(cmd, cwd=None, timeout=None): preexec_fn = os.setpgrp # start new session -> new process group try: - process = subprocess.Popen( + proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -803,25 +859,50 @@ def run_subtool_noread(cmd, cwd=None, timeout=None): ) try: - process.communicate(timeout=timeout) - return process.returncode, False + proc.communicate(timeout=timeout) + return proc.returncode, False except subprocess.TimeoutExpired: - # Kill the whole process group + # escalate: try gentle signal first try: if sys.platform == "win32": - # send CTRL_BREAK_EVENT to the process group - process.send_signal(signal.CTRL_BREAK_EVENT) + try: + proc.send_signal(signal.CTRL_BREAK_EVENT) + except Exception: + proc.terminate() else: - os.killpg(os.getpgid(process.pid), signal.SIGKILL) + os.killpg(os.getpgid(proc.pid), signal.SIGTERM) + except Exception: + try: + proc.terminate() + except Exception: + pass + + # wait a short time + try: + proc.wait(timeout=kill_timeout) except Exception: - # fallback to killing the process + # still alive -> force kill whole tree + _kill_process_tree(proc.pid, sig=signal.SIGKILL, timeout=kill_timeout) try: - process.kill() + proc.wait(timeout=kill_timeout) except Exception: pass - # Wait for termination - process.wait() - return process.returncode if process.returncode is not None else -1, True + + return (proc.returncode if proc.returncode is not None else -1), True + finally: + # close fds + try: + proc.stdout.close() + except Exception: + pass + try: + proc.stderr.close() + except Exception: + pass + try: + proc.stdin.close() + except Exception: + pass except Exception as e: # unicode/read error safe-guard From 3a625b05b75acdec2a8176b2fe773d633769ec1a Mon Sep 17 00:00:00 2001 From: Peter Willendrup Date: Sun, 22 Mar 2026 20:28:53 +0100 Subject: [PATCH 2/2] SIGKILL only with psutil / not windows --- tools/Python/mccodelib/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/Python/mccodelib/utils.py b/tools/Python/mccodelib/utils.py index 5c6a3ca85..fd5bf48da 100644 --- a/tools/Python/mccodelib/utils.py +++ b/tools/Python/mccodelib/utils.py @@ -780,7 +780,7 @@ def get_file_contents(filepath): except ImportError: psutil = None -def _kill_process_tree(pid, sig=signal.SIGKILL, timeout=3.0): +def _kill_process_tree(pid, timeout=3.0): """Kill process tree rooted at pid. Uses psutil if available, otherwise best-effort.""" if psutil: try: @@ -791,7 +791,7 @@ def _kill_process_tree(pid, sig=signal.SIGKILL, timeout=3.0): # send sig to children first for p in children: try: - p.send_signal(sig) + p.send_signal(signal.SIGKILL) except Exception: try: p.kill()