Skip to content

feat(backend): suspend_thread for LLDB-backed targets (#21 LLDB completion)#17

Merged
zachgenius merged 3 commits into
masterfrom
feat/v1.6-lldb-suspend-thread
May 12, 2026
Merged

feat(backend): suspend_thread for LLDB-backed targets (#21 LLDB completion)#17
zachgenius merged 3 commits into
masterfrom
feat/v1.6-lldb-suspend-thread

Conversation

@zachgenius

Copy link
Copy Markdown
Owner

Summary

  • Drops the -32001 kNotImplemented stub that thread.suspend returned for LLDB-backed targets. Lands a real per-thread suspend via SBThread::Suspend() on the resolved thread.
  • New DebuggerBackend::suspend_thread(TargetId, ThreadId) virtual; LldbBackend implements it, GdbMiBackend throws "not implemented" (kept on -32001 in the dispatcher).
  • RSP-backed targets keep their existing vCont;t route from feat(backend): suspend_thread for LLDB-backed targets (#21 LLDB completion) #17 phase-2 — only the non-RSP path changes.

Why this works without SetAsync(true)

SBThread::Suspend() is a flag flip. The suspend bit gates the next SBProcess::Continue, not the suspend call itself. We don't need the full async-mode cascade to express a per-thread suspend intent; we only need it for delivery of stop events (the listener path). docs/26 §1 originally called the bit-flip a "phase-1 limitation gated on SetAsync(true)" — that turns out to have been conservative.

The SetAsync(true) flip + pump_until_stopped wrappers around continue/step (the stretch scope) remain deferred. docs/27 §7 already documents that flip as its own commit — every existing endpoint that depends on "Continue() returns after the next stop" cascades when the mode flips, and ironing out the cascade is a multi-day audit. Shipping minimum scope clean here is the cheap correct slice.

Wire change

Path Before After
thread.suspend on RSP-backed target vCont;t:<tid> unchanged
thread.suspend on LLDB-backed live process -32001 kNotImplemented ok, returns ProcessStatus snapshot
thread.suspend on bad target / no process / bad tid -32001 kNotImplemented -32004 kBackendError with typed message
thread.suspend on GdbMi-backed target -32001 kNotImplemented -32001 kNotImplemented (unchanged)

Test plan

  • tests/unit/test_backend_suspend_thread.cpp (new, 4 cases) — live LLDB-launched suspend against the sleeper fixture + the three error paths (bad target_id, no process, unknown tid). All pass.
  • test_dispatcher_nonstop.cpp — "-32001 in phase-1" case replaced with forwards-to-backend + unknown-target → -32004. Both new cases pass.
  • test_dispatcher_vcont_rsp.cpp — "non-RSP target still returns -32001" case updated to assert the new "no process" -32004 against an empty target. RSP path test unchanged and still passing.
  • All 7 other stub-backend test files get a one-line suspend_thread override. ctest --test-dir build --output-on-failure: 70/70 passes (unchanged from baseline; +5 unit cases under the unit_tests umbrella).
  • Build is warning-clean (modulo pre-existing nlohmann/stdlib null-deref warning).

Punted

  • SetAsync(true) flip + pump_until_stopped wrappers around continue_process / continue_thread / step_thread — stretch scope. docs/27 §7 already calls out the cascade as its own commit. Flagged in worklog Next:.
  • continue_thread(target, tid) doing actual per-thread keep-running — needs the SetAsync flip first. Today it's still a sync passthrough.
  • Typed NotImplementedError subclass of backend::Error. Dispatcher currently string-matches "not implemented" on the message to pick -32001 vs -32004. Acceptable for one call site; should be cleaned up when the GDB/MI backend grows its own suspend implementation.
  • GdbMiBackend suspend_thread. MI's per-thread semantics require set non-stop on at session init, which is off today. Its own item.

🤖 Generated with Claude Code

zachgenius added a commit that referenced this pull request May 12, 2026
…ments (#21 reviewer pass)

Reviewer-pass on PR #17 (v1.6 #21 LLDB suspend_thread landing). Three
findings, all addressed:

1. CRITICAL: dispatcher mapped backend errors to -32001 vs -32004 by
   substring-matching `e.what()` for "not implemented". Any backend
   that threw a descriptive error containing those words for a
   legitimate runtime failure would have been silently promoted to
   kNotImplemented. Fix: typed `backend::NotImplementedError` subclass
   of `backend::Error`; dispatcher catches the subclass first
   (→ -32001), generic Error second (→ -32004). GdbMiBackend's
   suspend_thread throws the new type. Catch order matters and is
   pinned in a comment — the compiler does not warn on reversed
   order.

2. Stale comment blocks describing thread.suspend as "still returns
   -32001 kNotImplemented for LLDB-backed targets" — that's the
   pre-#21 contract. Replaced both: the describe.endpoints text and
   the function-level comment in handle_thread_suspend. The function
   comment now enumerates the three dispatch branches in order (RSP
   channel → vCont;t, LLDB-backed → backend.suspend_thread,
   NotImplemented-throwing backend → -32001).

3. MINOR: stale `[legacy]` Catch2 tag on a test that was updated to
   assert the new non-RSP forwarding path. Renamed to `[non-rsp]`.

New tests (test_dispatcher_nonstop.cpp) lock in the typed-error
contract:
- `NotImplementedError → -32001 by exception type` — message
  deliberately does NOT contain "not implemented" to prove the
  dispatcher uses the exception type, not its message.
- `generic Error with 'not implemented' substring stays -32004` —
  inverse case; this is the exact regression the pre-fix code was
  vulnerable to.

Also updated the gdbmi `todo()` helper (currently `[[maybe_unused]]`)
to throw NotImplementedError, so the contract is uniform for future
stubs.

70/70 ctest green. +2 unit cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zachgenius added a commit that referenced this pull request May 12, 2026
…etion)

Drops the -32001 kNotImplemented stub that thread.suspend returned for
LLDB-backed targets and lands a real per-thread suspend.

Why this works without SetAsync(true): SBThread::Suspend() is a flag
flip — the suspend bit gates the NEXT SBProcess::Continue, not the
suspend call itself. The async-mode flip is only required for the
*delivery* of stop events (the listener path), not for marking a thread
as parked. RSP-backed targets keep their existing vCont;t route (#17
phase-2); LLDB-backed targets now flow through the new backend virtual.

- DebuggerBackend grows a `suspend_thread(target_id, tid)` virtual.
  LldbBackend resolves SBThread by id and calls Suspend(); throws
  backend::Error on bad target/tid or no live process. GdbMiBackend
  throws "not implemented" — MI's per-thread semantics differ enough
  to warrant a separate item.
- Dispatcher::handle_thread_suspend forwards to backend_->suspend_thread
  for non-RSP targets. The "not implemented" branch surfaces -32001 so
  agents still see kNotImplemented when the backend genuinely lacks
  support; everything else is -32004 kBackendError.
- New test_backend_suspend_thread.cpp (4 cases) exercises live LLDB-
  launched suspend against the sleeper fixture + error paths.
- test_dispatcher_nonstop.cpp's "-32001 kNotImplemented in phase-1"
  case is replaced with two cases: forwards-to-backend (happy path)
  and unknown-target → -32004. test_dispatcher_vcont_rsp.cpp's "non-
  RSP returns -32001" case is updated to assert the new "no process"
  -32004 against an empty target.
- All 7 stub-backend test files grow a one-line suspend_thread
  override.

Stretch (SetAsync(true) flip + pump_until_stopped wrappers around
continue/step) intentionally deferred — docs/27 §7 already calls out
that cascade as its own commit. Ships minimum scope clean: 70/70 ctest
green, +5 unit cases, no other behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zachgenius added a commit that referenced this pull request May 12, 2026
…ments (#21 reviewer pass)

Reviewer-pass on PR #17 (v1.6 #21 LLDB suspend_thread landing). Three
findings, all addressed:

1. CRITICAL: dispatcher mapped backend errors to -32001 vs -32004 by
   substring-matching `e.what()` for "not implemented". Any backend
   that threw a descriptive error containing those words for a
   legitimate runtime failure would have been silently promoted to
   kNotImplemented. Fix: typed `backend::NotImplementedError` subclass
   of `backend::Error`; dispatcher catches the subclass first
   (→ -32001), generic Error second (→ -32004). GdbMiBackend's
   suspend_thread throws the new type. Catch order matters and is
   pinned in a comment — the compiler does not warn on reversed
   order.

2. Stale comment blocks describing thread.suspend as "still returns
   -32001 kNotImplemented for LLDB-backed targets" — that's the
   pre-#21 contract. Replaced both: the describe.endpoints text and
   the function-level comment in handle_thread_suspend. The function
   comment now enumerates the three dispatch branches in order (RSP
   channel → vCont;t, LLDB-backed → backend.suspend_thread,
   NotImplemented-throwing backend → -32001).

3. MINOR: stale `[legacy]` Catch2 tag on a test that was updated to
   assert the new non-RSP forwarding path. Renamed to `[non-rsp]`.

New tests (test_dispatcher_nonstop.cpp) lock in the typed-error
contract:
- `NotImplementedError → -32001 by exception type` — message
  deliberately does NOT contain "not implemented" to prove the
  dispatcher uses the exception type, not its message.
- `generic Error with 'not implemented' substring stays -32004` —
  inverse case; this is the exact regression the pre-fix code was
  vulnerable to.

Also updated the gdbmi `todo()` helper (currently `[[maybe_unused]]`)
to throw NotImplementedError, so the contract is uniform for future
stubs.

70/70 ctest green. +2 unit cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zachgenius zachgenius force-pushed the feat/v1.6-lldb-suspend-thread branch from 3b5586b to 22aa0f6 Compare May 12, 2026 11:26
zachgenius and others added 3 commits May 12, 2026 21:29
…etion)

Drops the -32001 kNotImplemented stub that thread.suspend returned for
LLDB-backed targets and lands a real per-thread suspend.

Why this works without SetAsync(true): SBThread::Suspend() is a flag
flip — the suspend bit gates the NEXT SBProcess::Continue, not the
suspend call itself. The async-mode flip is only required for the
*delivery* of stop events (the listener path), not for marking a thread
as parked. RSP-backed targets keep their existing vCont;t route (#17
phase-2); LLDB-backed targets now flow through the new backend virtual.

- DebuggerBackend grows a `suspend_thread(target_id, tid)` virtual.
  LldbBackend resolves SBThread by id and calls Suspend(); throws
  backend::Error on bad target/tid or no live process. GdbMiBackend
  throws "not implemented" — MI's per-thread semantics differ enough
  to warrant a separate item.
- Dispatcher::handle_thread_suspend forwards to backend_->suspend_thread
  for non-RSP targets. The "not implemented" branch surfaces -32001 so
  agents still see kNotImplemented when the backend genuinely lacks
  support; everything else is -32004 kBackendError.
- New test_backend_suspend_thread.cpp (4 cases) exercises live LLDB-
  launched suspend against the sleeper fixture + error paths.
- test_dispatcher_nonstop.cpp's "-32001 kNotImplemented in phase-1"
  case is replaced with two cases: forwards-to-backend (happy path)
  and unknown-target → -32004. test_dispatcher_vcont_rsp.cpp's "non-
  RSP returns -32001" case is updated to assert the new "no process"
  -32004 against an empty target.
- All 7 stub-backend test files grow a one-line suspend_thread
  override.

Stretch (SetAsync(true) flip + pump_until_stopped wrappers around
continue/step) intentionally deferred — docs/27 §7 already calls out
that cascade as its own commit. Ships minimum scope clean: 70/70 ctest
green, +5 unit cases, no other behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rgets

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ments (#21 reviewer pass)

Reviewer-pass on PR #17 (v1.6 #21 LLDB suspend_thread landing). Three
findings, all addressed:

1. CRITICAL: dispatcher mapped backend errors to -32001 vs -32004 by
   substring-matching `e.what()` for "not implemented". Any backend
   that threw a descriptive error containing those words for a
   legitimate runtime failure would have been silently promoted to
   kNotImplemented. Fix: typed `backend::NotImplementedError` subclass
   of `backend::Error`; dispatcher catches the subclass first
   (→ -32001), generic Error second (→ -32004). GdbMiBackend's
   suspend_thread throws the new type. Catch order matters and is
   pinned in a comment — the compiler does not warn on reversed
   order.

2. Stale comment blocks describing thread.suspend as "still returns
   -32001 kNotImplemented for LLDB-backed targets" — that's the
   pre-#21 contract. Replaced both: the describe.endpoints text and
   the function-level comment in handle_thread_suspend. The function
   comment now enumerates the three dispatch branches in order (RSP
   channel → vCont;t, LLDB-backed → backend.suspend_thread,
   NotImplemented-throwing backend → -32001).

3. MINOR: stale `[legacy]` Catch2 tag on a test that was updated to
   assert the new non-RSP forwarding path. Renamed to `[non-rsp]`.

New tests (test_dispatcher_nonstop.cpp) lock in the typed-error
contract:
- `NotImplementedError → -32001 by exception type` — message
  deliberately does NOT contain "not implemented" to prove the
  dispatcher uses the exception type, not its message.
- `generic Error with 'not implemented' substring stays -32004` —
  inverse case; this is the exact regression the pre-fix code was
  vulnerable to.

Also updated the gdbmi `todo()` helper (currently `[[maybe_unused]]`)
to throw NotImplementedError, so the contract is uniform for future
stubs.

70/70 ctest green. +2 unit cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zachgenius zachgenius force-pushed the feat/v1.6-lldb-suspend-thread branch from 22aa0f6 to 9e89061 Compare May 12, 2026 11:30
@zachgenius zachgenius merged commit 5479dcc into master May 12, 2026
4 checks passed
@zachgenius zachgenius deleted the feat/v1.6-lldb-suspend-thread branch May 12, 2026 11:36
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