|
3 | 3 | import signal |
4 | 4 | import sys |
5 | 5 | import textwrap |
| 6 | +import threading |
6 | 7 | import unittest |
7 | 8 | import warnings |
8 | 9 | from unittest import mock |
@@ -1162,6 +1163,63 @@ async def run(): |
1162 | 1163 |
|
1163 | 1164 | self.loop.run_until_complete(run()) |
1164 | 1165 |
|
| 1166 | + @unittest.skipUnless(hasattr(os, 'waitid'), 'needs os.waitid()') |
| 1167 | + @unittest.skipUnless(hasattr(signal, 'SIGSTOP'), 'needs SIGSTOP') |
| 1168 | + def test_stopped_child_does_not_block_event_loop(self): |
| 1169 | + # gh-158287: on macOS, waitid() with WEXITED incorrectly reports |
| 1170 | + # stopped children (si_code == CLD_STOPPED). The child watcher |
| 1171 | + # must not mistake this for process termination and attempt to reap |
| 1172 | + # the child with waitpid(), which blocks the event loop thread. |
| 1173 | + proc = None |
| 1174 | + timed_out = False |
| 1175 | + |
| 1176 | + def watchdog(): |
| 1177 | + nonlocal timed_out |
| 1178 | + timed_out = True |
| 1179 | + # If the event loop gets stuck in waitpid() because of this bug, |
| 1180 | + # killing the child process unblocks waitpid() so the test fails |
| 1181 | + # instead of hanging indefinitely. |
| 1182 | + if proc is not None: |
| 1183 | + if hasattr(signal, 'SIGCONT'): |
| 1184 | + try: |
| 1185 | + os.kill(proc.pid, signal.SIGCONT) |
| 1186 | + except ProcessLookupError: |
| 1187 | + pass |
| 1188 | + try: |
| 1189 | + os.kill(proc.pid, signal.SIGKILL) |
| 1190 | + except ProcessLookupError: |
| 1191 | + pass |
| 1192 | + |
| 1193 | + timer = threading.Timer(support.SHORT_TIMEOUT, watchdog) |
| 1194 | + timer.start() |
| 1195 | + try: |
| 1196 | + async def run(): |
| 1197 | + nonlocal proc |
| 1198 | + proc = await asyncio.create_subprocess_exec(*PROGRAM_BLOCKED) |
| 1199 | + try: |
| 1200 | + proc.send_signal(signal.SIGSTOP) |
| 1201 | + # Verify event loop remains responsive while child is stopped |
| 1202 | + for _ in range(3): |
| 1203 | + await asyncio.sleep(0.05) |
| 1204 | + self.assertFalse(timed_out, "event loop was blocked by stopped child") |
| 1205 | + self.assertIsNone(proc.returncode) |
| 1206 | + finally: |
| 1207 | + if hasattr(signal, 'SIGCONT'): |
| 1208 | + try: |
| 1209 | + proc.send_signal(signal.SIGCONT) |
| 1210 | + except ProcessLookupError: |
| 1211 | + pass |
| 1212 | + try: |
| 1213 | + proc.kill() |
| 1214 | + except ProcessLookupError: |
| 1215 | + pass |
| 1216 | + await proc.wait() |
| 1217 | + |
| 1218 | + self.loop.run_until_complete(run()) |
| 1219 | + finally: |
| 1220 | + timer.cancel() |
| 1221 | + timer.join() |
| 1222 | + |
1165 | 1223 | @unittest.skipUnless( |
1166 | 1224 | unix_events.can_use_pidfd(), |
1167 | 1225 | "operating system does not support pidfds", |
|
0 commit comments