Skip to content

fix(debugger): cancel timed-out commands on dump sessions - #138

Merged
svnscha merged 1 commit into
developfrom
fix/cancel-dump-timeouts
Oct 8, 2026
Merged

svnscha merged 1 commit into
developfrom
fix/cancel-dump-timeouts

Conversation

@svnscha

@svnscha svnscha commented Oct 8, 2026

Copy link
Copy Markdown
Owner

Supersedes #124, whose diagnosis and mechanism this takes.

The defect

A command that outran its timeout was only cancelled on a live target, on the reasoning that a dump has nothing to break into. But the engine is still executing the command, so on a dump it ran to completion while every later command queued behind it and timed out as well. The session was wedged until closed. Easy to reach now that open_kd_dump exists: !process 0 7 over a large kernel dump runs for minutes.

Measured on a real cdb against a committed dump, 3 second timeout:

before:  the command times out, then .lastevent ALSO times out after 20s
         DebuggerError: Command timed out after 20 seconds: .lastevent
                        (session may need a manual break-in)
after:   the command is cancelled in 3.00s, .lastevent returns in 0.00s

The old hint was unfollowable for a dump user anyway: _require_live_session rejects send_ctrl_break on a dump session.

What changed

Every session gets its own process group, so CTRL+BREAK can be aimed at it alone, and the cancel is no longer gated on is_live_session. The engine reports "User interrupted operation" and returns to the prompt.

On a failed resync the two kinds now differ, because their recovery options do. A live target keeps its session and the manual break-in hint, since send_ctrl_break accepts it. A dump has no way back, so the session is closed rather than left wedged with output that can no longer be attributed to the command that produced it.

On the Unicode-log path the markers of abandoned commands are remembered so their segments are stepped over rather than leading the next command's reply. That list is bounded, since a marker whose command never answers would otherwise sit at its head while the list grew with every timeout.

What was deliberately left out

#124 also added a stray-break "absorb": a no-op .echo after a successful resync, on the premise that a break reaching an idle debugger stays pending and aborts the next real command. Measured, such a break is dropped, not pended. The only command at risk is one starting within about 10ms of the signal, and with the absorb in place 3 of 12 such commands were still truncated, so it did not close the window it targeted. In practice the next command arrives after a model round trip. That is about 25 source and 16 test lines not carried.

Also corrected: the comments referenced issues #449 and #468, which do not exist in this repository.

Tests

uv run pytest src/mcp_windbg/tests/ -q   ->  201 passed, 2 skipped

Baseline before this change is 198 passed. The three added are the hybrid close for each session kind, plus test_dump_cancel_live.py: a real-engine check that a busy loop on a dump session is cancelled and the next command returns in under 5 seconds. That one matters, because whether CTRL+BREAK cancels a command the engine is executing on a static dump is a property of cdb, not of our fake, and the hermetic suite cannot tell. It carries the live marker, so it runs in CI and is deselected by -m "not live".

Credit

The diagnosis, the reproduction and the cancel mechanism are @robster7674's, from #124.

A command that outran its timeout was only cancelled on a live target, on the
reasoning that a dump has nothing to break into. The engine is still executing
the command though, so on a dump it ran to completion while every command sent
after it queued behind it and timed out in turn. The session was wedged until
closed, which is easy to reach now that open_kd_dump exists: a `!process 0 7`
over a large kernel dump runs for minutes.

Every session now gets its own process group, so CTRL+BREAK can be aimed at it
alone, and the cancel is no longer gated on the session being live. The engine
reports "User interrupted operation" and returns to the prompt.

Measured on a real dump with a 3 second timeout, before and after:

  before: the command timed out, then `.lastevent` timed out too after 20s
          with "(session may need a manual break-in)" - advice a dump user
          cannot follow, because send_ctrl_break rejects a dump session
  after:  the command is cancelled in 3.0s and `.lastevent` returns in 0.0s

If the cancel does not resync, the two kinds now differ, because their recovery
options do. A live target keeps its session and the manual break-in hint, since
send_ctrl_break accepts it. A dump has no way back, so the session is closed
rather than left wedged with output that can no longer be attributed to the
command that produced it.

On the Unicode-log path the markers of abandoned commands are remembered so
their segments are stepped over instead of leading the next command's reply.
That list is bounded: a marker whose command never answers would otherwise sit
at its head and the list would grow with every timeout.

Not taken from the original change: the stray-break absorb. Its premise was
that a break reaching an idle debugger stays pending and aborts the next real
command. Measured, such a break is dropped, and the only command at risk is one
starting within about 10ms of the signal, which the absorb did not reliably
prevent either. In practice the next command arrives after a model round trip.
@svnscha
svnscha merged commit 95505f7 into develop Oct 8, 2026
6 checks passed
@svnscha
svnscha deleted the fix/cancel-dump-timeouts branch October 8, 2026 21:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant