(active: ActiveSuite, kill_grace: int)
| 226 | |
| 227 | |
| 228 | def terminate_process_tree(active: ActiveSuite, kill_grace: int) -> None: |
| 229 | process = active.process |
| 230 | leader_exited = process.poll() is not None |
| 231 | if os.name == "nt": |
| 232 | if leader_exited: |
| 233 | # The leader can exit on its own between the timeout decision and |
| 234 | # this call. Refusing outright made the harness itself lose a race: |
| 235 | # a natural exit at the wrong moment failed the whole wave, which is |
| 236 | # how a deliberately-hanging fixture suite reddened a release run. |
| 237 | # taskkill /T cannot walk a tree from a dead PID, so prove cleanup |
| 238 | # the only way still available -- nothing is parented to it. |
| 239 | blocker = windows_tree_cleanup_blocker(process.pid) |
| 240 | if blocker is not None: |
| 241 | raise RuntimeError( |
| 242 | f"suite {active.name!r} leader exited and tree cleanup " |
| 243 | f"could not be proven: {blocker}" |
| 244 | ) |
| 245 | return |
| 246 | try: |
| 247 | completed = subprocess.run( |
| 248 | [ |
| 249 | "taskkill.exe", |
| 250 | "/PID", |
| 251 | str(process.pid), |
| 252 | "/T", |
| 253 | "/F", |
| 254 | ], |
| 255 | check=False, |
| 256 | stdin=subprocess.DEVNULL, |
| 257 | stdout=subprocess.DEVNULL, |
| 258 | stderr=subprocess.DEVNULL, |
| 259 | timeout=kill_grace, |
| 260 | ) |
| 261 | except (OSError, subprocess.TimeoutExpired): |
| 262 | completed = None |
| 263 | if completed is None or completed.returncode != 0: |
| 264 | if process.poll() is None: |
| 265 | process.kill() |
| 266 | try: |
| 267 | process.wait(timeout=kill_grace) |
| 268 | except subprocess.TimeoutExpired: |
| 269 | pass |
| 270 | raise RuntimeError( |
| 271 | f"suite {active.name!r} taskkill could not prove process-tree cleanup" |
| 272 | ) |
| 273 | try: |
| 274 | process.wait(timeout=kill_grace) |
| 275 | except subprocess.TimeoutExpired as exc: |
| 276 | process.kill() |
| 277 | raise RuntimeError( |
| 278 | f"suite {active.name!r} process tree resisted forced termination" |
| 279 | ) from exc |
| 280 | return |
| 281 | |
| 282 | def group_active() -> bool: |
| 283 | try: |
| 284 | os.killpg(process.pid, 0) |
| 285 | return True |
no test coverage detected