(active: ActiveSuite, kill_grace: int)
| 157 | |
| 158 | |
| 159 | def terminate_process_tree(active: ActiveSuite, kill_grace: int) -> None: |
| 160 | process = active.process |
| 161 | leader_exited = process.poll() is not None |
| 162 | if os.name == "nt": |
| 163 | if leader_exited: |
| 164 | # The leader can exit on its own between the timeout decision and |
| 165 | # this call. Refusing outright made the harness itself lose a race: |
| 166 | # a natural exit at the wrong moment failed the whole wave, which is |
| 167 | # how a deliberately-hanging fixture suite reddened a release run. |
| 168 | # taskkill /T cannot walk a tree from a dead PID, so prove cleanup |
| 169 | # the only way still available -- nothing is parented to it. |
| 170 | if windows_descendants(process.pid, kill_grace): |
| 171 | raise RuntimeError( |
| 172 | f"suite {active.name!r} leader exited leaving live descendants" |
| 173 | ) |
| 174 | return |
| 175 | try: |
| 176 | completed = subprocess.run( |
| 177 | [ |
| 178 | "taskkill.exe", |
| 179 | "/PID", |
| 180 | str(process.pid), |
| 181 | "/T", |
| 182 | "/F", |
| 183 | ], |
| 184 | check=False, |
| 185 | stdin=subprocess.DEVNULL, |
| 186 | stdout=subprocess.DEVNULL, |
| 187 | stderr=subprocess.DEVNULL, |
| 188 | timeout=kill_grace, |
| 189 | ) |
| 190 | except (OSError, subprocess.TimeoutExpired): |
| 191 | completed = None |
| 192 | if completed is None or completed.returncode != 0: |
| 193 | if process.poll() is None: |
| 194 | process.kill() |
| 195 | try: |
| 196 | process.wait(timeout=kill_grace) |
| 197 | except subprocess.TimeoutExpired: |
| 198 | pass |
| 199 | raise RuntimeError( |
| 200 | f"suite {active.name!r} taskkill could not prove process-tree cleanup" |
| 201 | ) |
| 202 | try: |
| 203 | process.wait(timeout=kill_grace) |
| 204 | except subprocess.TimeoutExpired as exc: |
| 205 | process.kill() |
| 206 | raise RuntimeError( |
| 207 | f"suite {active.name!r} process tree resisted forced termination" |
| 208 | ) from exc |
| 209 | return |
| 210 | |
| 211 | def group_active() -> bool: |
| 212 | try: |
| 213 | os.killpg(process.pid, 0) |
| 214 | return True |
| 215 | except ProcessLookupError: |
| 216 | return False |
no test coverage detected