Skip to content

Commit 7c427ad

Browse files
authored
Merge pull request #4685 from simpkins/process_hang
Fix infinite hang in wait_for_process_to_die() on Windows
2 parents abdfea7 + 4848319 commit 7c427ad

File tree

4 files changed

+85
-18
lines changed

4 files changed

+85
-18
lines changed

CHANGES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,10 @@ RELEASE 4.9.0 - Sun, 02 Mar 2025 17:22:20 -0700
238238
`local_pool`, hoping it will fix a race condition that can occurs when
239239
Ninja defers to SCons to build.
240240

241+
From Adam Simpkins:
242+
- Fixed a hang in `wait_for_process_to_die()` on Windows, affecting
243+
clean-up of the SCons daemon used for Ninja builds.
244+
241245

242246
RELEASE 4.8.1 - Tue, 03 Sep 2024 17:22:20 -0700
243247

RELEASE.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ FIXES
3737
- New CacheDir initialization code failed on Python 3.7 for unknown
3838
reason (worked on 3.8+). Adjusted the approach a bit. Fixes #4694.
3939

40+
- Fixed a hang in `wait_for_process_to_die()` on Windows, affecting
41+
clean-up of the SCons daemon used for Ninja builds.
42+
4043
IMPROVEMENTS
4144
------------
4245

SCons/Util/UtilTests.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
2727
import hashlib
2828
import io
2929
import os
30+
import subprocess
3031
import sys
3132
import unittest
3233
import unittest.mock
3334
import warnings
3435
from collections import UserDict, UserList, UserString, namedtuple
36+
from typing import Callable
3537

3638
import TestCmd
3739

@@ -73,6 +75,8 @@
7375
to_String,
7476
to_bytes,
7577
to_str,
78+
wait_for_process_to_die,
79+
_wait_for_process_to_die_non_psutil,
7680
)
7781
from SCons.Util.envs import is_valid_construction_var
7882
from SCons.Util.hashes import (
@@ -82,6 +86,13 @@
8286
_set_allowed_viable_default_hashes,
8387
)
8488

89+
try:
90+
import psutil
91+
has_psutil = True
92+
except ImportError:
93+
has_psutil = False
94+
95+
8596
# These Util classes have no unit tests. Some don't make sense to test?
8697
# DisplayEngine, Delegate, MethodWrapper, UniqueList, Unbuffered, Null, NullSeq
8798

@@ -847,6 +858,21 @@ def test_intern(self) -> None:
847858
s4 = silent_intern("spam")
848859
assert id(s1) == id(s4)
849860

861+
@unittest.skipUnless(has_psutil, "requires psutil")
862+
def test_wait_for_process_to_die_success_psutil(self) -> None:
863+
self._test_wait_for_process(wait_for_process_to_die)
864+
865+
def test_wait_for_process_to_die_success_non_psutil(self) -> None:
866+
self._test_wait_for_process(_wait_for_process_to_die_non_psutil)
867+
868+
def _test_wait_for_process(
869+
self, wait_fn: Callable[[int], None]
870+
) -> None:
871+
cmd = [sys.executable, "-c", ""]
872+
p = subprocess.Popen(cmd)
873+
p.wait()
874+
wait_fn(p.pid)
875+
850876

851877
class HashTestCase(unittest.TestCase):
852878

SCons/Util/__init__.py

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,36 +1297,70 @@ def print_time():
12971297
return print_time
12981298

12991299

1300-
def wait_for_process_to_die(pid) -> None:
1300+
def wait_for_process_to_die(pid: int) -> None:
13011301
"""
1302-
Wait for specified process to die, or alternatively kill it
1303-
NOTE: This function operates best with psutil pypi package
1302+
Wait for the specified process to die.
1303+
13041304
TODO: Add timeout which raises exception
13051305
"""
13061306
# wait for the process to fully killed
13071307
try:
13081308
import psutil # pylint: disable=import-outside-toplevel
13091309
while True:
1310+
# TODO: this should use psutil.process_exists() or psutil.Process.wait()
1311+
# The psutil docs explicitly recommend against using process_iter()/pids()
1312+
# for checking the existence of a process.
13101313
if pid not in [proc.pid for proc in psutil.process_iter()]:
13111314
break
13121315
time.sleep(0.1)
13131316
except ImportError:
13141317
# if psutil is not installed we can do this the hard way
1315-
while True:
1316-
if sys.platform == 'win32':
1317-
import ctypes # pylint: disable=import-outside-toplevel
1318-
PROCESS_QUERY_INFORMATION = 0x1000
1319-
processHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid)
1320-
if processHandle == 0:
1321-
break
1322-
ctypes.windll.kernel32.CloseHandle(processHandle)
1323-
time.sleep(0.1)
1324-
else:
1325-
try:
1326-
os.kill(pid, 0)
1327-
except OSError:
1328-
break
1329-
time.sleep(0.1)
1318+
_wait_for_process_to_die_non_psutil(pid, timeout=-1.0)
1319+
1320+
1321+
def _wait_for_process_to_die_non_psutil(pid: int, timeout: float = 60.0) -> None:
1322+
start_time = time.time()
1323+
while True:
1324+
if not _is_process_alive(pid):
1325+
break
1326+
if timeout >= 0.0 and time.time() - start_time > timeout:
1327+
raise TimeoutError(f"timed out waiting for process {pid}")
1328+
time.sleep(0.1)
1329+
1330+
1331+
if sys.platform == 'win32':
1332+
def _is_process_alive(pid: int) -> bool:
1333+
import ctypes # pylint: disable=import-outside-toplevel
1334+
PROCESS_QUERY_INFORMATION = 0x1000
1335+
STILL_ACTIVE = 259
1336+
1337+
processHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid)
1338+
if processHandle == 0:
1339+
return False
1340+
1341+
# OpenProcess() may successfully return a handle even for terminated
1342+
# processes when something else in the system is still holding a
1343+
# reference to their handle. Call GetExitCodeProcess() to check if the
1344+
# process has already exited.
1345+
try:
1346+
exit_code = ctypes.c_ulong()
1347+
success = ctypes.windll.kernel32.GetExitCodeProcess(
1348+
processHandle, ctypes.byref(exit_code))
1349+
if success:
1350+
return exit_code.value == STILL_ACTIVE
1351+
finally:
1352+
ctypes.windll.kernel32.CloseHandle(processHandle)
1353+
1354+
return True
1355+
1356+
else:
1357+
def _is_process_alive(pid: int) -> bool:
1358+
try:
1359+
os.kill(pid, 0)
1360+
return True
1361+
except OSError:
1362+
return False
1363+
13301364

13311365
# From: https://stackoverflow.com/questions/1741972/how-to-use-different-formatters-with-the-same-logging-handler-in-python
13321366
class DispatchingFormatter(Formatter):

0 commit comments

Comments
 (0)