A finished target was treated like a temporary sampling failure.
CPython’s remote debugging support includes a blocking profiler mode that repeatedly samples another Python process. On Windows, the profiler suspends the target before inspecting it. Issue #152356 reported that blocking sampling could continue after a short-lived target process had exited, leaving the profiler running instead of finalizing its binary output.
PR #152471, authored by Zain Nadeem, corrected the Windows error mapping and added an end-to-end regression that checks both termination and profile replay.
Blocking mode relies on terminal errors to end the sampling loop.
A blocking sampler expects individual samples to fail occasionally. Some failures are recoverable: the profiler can wait and try again. Process exit is different. Once the target no longer exists, no later retry can produce a sample, so the loop must stop and allow the profile writer to close cleanly.
That finalization is observable behavior, not an internal detail. A sampling command that continues waiting after its target ends prevents normal command completion and leaves the binary profile unavailable for the expected replay workflow.
The Windows path violated that lifecycle because the target’s exit surfaced through the same broad exception class used for recoverable runtime failures.
Follow the native failure into the Python retry policy.
The relevant native operation is NtSuspendProcess(). When a target ended between sampling steps, that call failed. The Windows remote-debugging layer translated the failure into RuntimeError. Higher-level blocking sampling treated that exception as a reason to continue rather than a signal that the process was gone.
Comparing platform behavior showed the intended contract: a missing process is represented as ProcessLookupError, which is terminal to the loop. The investigation therefore focused on distinguishing two states after a suspend failure:
- the target is no longer alive, so sampling must terminate;
- the target is still alive, so the native suspension failure remains a genuine runtime error.
Error type carried the control-flow decision.
The issue was not simply that a Windows API returned an error. It was that the translation layer erased the lifecycle meaning of that error. Once the target-exit case became a generic RuntimeError, the blocking loop could not distinguish a permanent terminal state from a retryable sample failure.
NtSuspendProcess failure → RuntimeError → retrytarget exited → ProcessLookupError → stopBecause the loop remained active, the binary writer did not reach its normal finalization path. A correct solution had to restore the missing semantic distinction, not merely place an arbitrary retry cap around the symptom.
Check process liveness before choosing the exception.
The merged patch checks whether the target remains alive after NtSuspendProcess() fails. If the process has terminated, the native layer raises ProcessLookupError. If the target is still alive, the existing RuntimeError behavior is retained for a real suspension failure.
// Simplified decision after a failed suspend
if (!is_process_alive(target)) {
raise ProcessLookupError;
}
raise RuntimeError;This preserves diagnostic fidelity. It does not hide valid Windows suspension errors, and it gives the higher-level loop the terminal signal it already understands on other platforms.
Exercise the full lifecycle, including the output artifact.
The regression test starts a short-lived target and launches blocking profiling in binary-output mode on Windows. It then verifies that the profiler process exits rather than waiting indefinitely. The resulting profile file must be non-empty and replayable.
| Check | What it protects |
|---|---|
| Short-lived target exits | Reproduces the terminal lifecycle boundary |
| Profiler process terminates | Protects against the retry loop continuing forever |
| Binary profile is non-empty | Confirms recorded samples were finalized |
| Profile replay succeeds | Confirms the produced artifact is usable |
This is stronger than checking the exception type in isolation: it validates the relationship between native error classification, loop termination, writer cleanup, and the user-visible profile.
The fix aligned Windows with the profiler’s cross-platform contract.
Zain authored PR #152471 against CPython issue #152356. It updated the Windows remote-debugging code, added a Windows end-to-end test, and included the corresponding news entry. The change was merged on 28 June 2026 as commit 37b238f1a6f0d8738e44ccc516ca2a476c38e5ce.
The patch is deliberately narrow: it changes the exception only when liveness establishes that the process has ended. Other suspension failures continue to surface as runtime errors.
Error taxonomies are part of a control-flow interface.
- Native error translation should preserve whether a failure is terminal, retryable, or genuinely exceptional.
- Retry loops should be tested against lifecycle completion, not only successful steady-state sampling.
- For file-producing tools, termination tests should also validate that the output was finalized and can be consumed.
- Cross-platform code benefits from semantic parity even when each platform reaches that semantic result through different system calls.