|
2 | 2 | import signal |
3 | 3 | import sys |
4 | 4 | import textwrap |
| 5 | +import threading |
5 | 6 | import unittest |
6 | 7 | import warnings |
7 | 8 | from unittest import mock |
@@ -1053,6 +1054,63 @@ async def run(): |
1053 | 1054 |
|
1054 | 1055 | self.loop.run_until_complete(run()) |
1055 | 1056 |
|
| 1057 | + @unittest.skipUnless(hasattr(os, 'waitid'), 'needs os.waitid()') |
| 1058 | + @unittest.skipUnless(hasattr(signal, 'SIGSTOP'), 'needs SIGSTOP') |
| 1059 | + def test_stopped_child_does_not_block_event_loop(self): |
| 1060 | + # gh-158287: on macOS, waitid() with WEXITED incorrectly reports |
| 1061 | + # stopped children (si_code == CLD_STOPPED). The child watcher |
| 1062 | + # must not mistake this for process termination and attempt to reap |
| 1063 | + # the child with waitpid(), which blocks the event loop thread. |
| 1064 | + proc = None |
| 1065 | + timed_out = False |
| 1066 | + |
| 1067 | + def watchdog(): |
| 1068 | + nonlocal timed_out |
| 1069 | + timed_out = True |
| 1070 | + # If the event loop gets stuck in waitpid() because of this bug, |
| 1071 | + # killing the child process unblocks waitpid() so the test fails |
| 1072 | + # instead of hanging indefinitely. |
| 1073 | + if proc is not None: |
| 1074 | + if hasattr(signal, 'SIGCONT'): |
| 1075 | + try: |
| 1076 | + os.kill(proc.pid, signal.SIGCONT) |
| 1077 | + except ProcessLookupError: |
| 1078 | + pass |
| 1079 | + try: |
| 1080 | + os.kill(proc.pid, signal.SIGKILL) |
| 1081 | + except ProcessLookupError: |
| 1082 | + pass |
| 1083 | + |
| 1084 | + timer = threading.Timer(support.SHORT_TIMEOUT, watchdog) |
| 1085 | + timer.start() |
| 1086 | + try: |
| 1087 | + async def run(): |
| 1088 | + nonlocal proc |
| 1089 | + proc = await asyncio.create_subprocess_exec(*PROGRAM_BLOCKED) |
| 1090 | + try: |
| 1091 | + proc.send_signal(signal.SIGSTOP) |
| 1092 | + # Verify event loop remains responsive while child is stopped |
| 1093 | + for _ in range(3): |
| 1094 | + await asyncio.sleep(0.05) |
| 1095 | + self.assertFalse(timed_out, "event loop was blocked by stopped child") |
| 1096 | + self.assertIsNone(proc.returncode) |
| 1097 | + finally: |
| 1098 | + if hasattr(signal, 'SIGCONT'): |
| 1099 | + try: |
| 1100 | + proc.send_signal(signal.SIGCONT) |
| 1101 | + except ProcessLookupError: |
| 1102 | + pass |
| 1103 | + try: |
| 1104 | + proc.kill() |
| 1105 | + except ProcessLookupError: |
| 1106 | + pass |
| 1107 | + await proc.wait() |
| 1108 | + |
| 1109 | + self.loop.run_until_complete(run()) |
| 1110 | + finally: |
| 1111 | + timer.cancel() |
| 1112 | + timer.join() |
| 1113 | + |
1056 | 1114 | @unittest.skipUnless( |
1057 | 1115 | unix_events.can_use_pidfd(), |
1058 | 1116 | "operating system does not support pidfds", |
|
0 commit comments