mirror of
https://github.com/HKUDS/CLI-Anything.git
synced 2026-10-30 18:12:56 +08:00
fix: keep lldb dap responsive while running
This commit is contained in:
@@ -117,6 +117,22 @@ Variables support expandable child references for structs/classes/arrays, and
|
||||
`setVariable` can update stopped-frame locals or child values when LLDB allows
|
||||
the assignment.
|
||||
|
||||
For long-running GUI targets, DAP `continue` responds before the blocking LLDB
|
||||
`SBProcess.Continue()` call completes, then waits on a background thread for the
|
||||
next stop. DAP `pause` uses `SBProcess.SendAsyncInterrupt()` so the adapter stays
|
||||
responsive while the debuggee is running. If `setBreakpoints` or
|
||||
`setFunctionBreakpoints` arrives during an active continue, the adapter first
|
||||
requests an async interrupt, waits for the continue thread to observe a stopped
|
||||
state, and only then mutates LLDB breakpoints. If the process does not stop in
|
||||
time, the request fails clearly instead of hanging the DAP loop.
|
||||
|
||||
`launch` and `attach` accept the non-standard boolean argument
|
||||
`autoContinueInternalBreakpoints`. When enabled, the adapter auto-continues
|
||||
known internal JIT/startup breakpoint stops such as NVIDIA
|
||||
`__jit_debug_register_code` / `jit-debug-register` and Windows
|
||||
`Exception 0x80000003` at ``ntdll.dll`DbgBreakPoint``, while writing a DAP
|
||||
`output` event explaining the auto-continue.
|
||||
|
||||
The persistent session daemon now speaks a localhost JSON socket protocol and
|
||||
stores its session token in an owner-scoped state file. `memory find` scans in
|
||||
64 KiB chunks and caps each request at 1 MiB.
|
||||
|
||||
@@ -199,6 +199,13 @@ class LLDBSession:
|
||||
raise RuntimeError(f"Interrupt failed: {error}")
|
||||
return self._process_info()
|
||||
|
||||
def interrupt_async(self) -> Dict[str, Any]:
|
||||
self._require_process()
|
||||
error = self.process.SendAsyncInterrupt()
|
||||
if error is not None and not error.Success():
|
||||
raise RuntimeError(f"Async interrupt failed: {error}")
|
||||
return {"status": "interrupt_requested"}
|
||||
|
||||
def backtrace(self, limit: int = 50) -> Dict[str, Any]:
|
||||
thread = self._current_thread()
|
||||
frames = []
|
||||
|
||||
@@ -8,6 +8,7 @@ import json
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Any, BinaryIO, Callable
|
||||
|
||||
@@ -80,6 +81,13 @@ class LLDBDebugAdapter:
|
||||
self._variable_refs: dict[int, dict[str, Any]] = {}
|
||||
self._next_ref = 1
|
||||
self._log_file = Path(log_file).expanduser() if log_file else None
|
||||
self._protocol_lock = threading.Lock()
|
||||
self._lldb_api_lock = threading.RLock()
|
||||
self._continue_state = threading.Condition()
|
||||
self._continue_active = False
|
||||
self._auto_continue_internal_breakpoints = False
|
||||
self._pause_requested = False
|
||||
self._mutation_stop_timeout = 10.0
|
||||
|
||||
def run(self, instream: BinaryIO | None = None, outstream: BinaryIO | None = None) -> int:
|
||||
instream = instream or sys.stdin.buffer
|
||||
@@ -157,6 +165,7 @@ class LLDBDebugAdapter:
|
||||
"stop_at_entry": bool(args.get("stopOnEntry", False)),
|
||||
"suppress_stdio": True,
|
||||
}
|
||||
self._auto_continue_internal_breakpoints = bool(args.get("autoContinueInternalBreakpoints", False))
|
||||
self._pending_attach = None
|
||||
return {}, None
|
||||
|
||||
@@ -174,6 +183,7 @@ class LLDBDebugAdapter:
|
||||
"name": str(name) if name else None,
|
||||
"wait_for": bool(args.get("waitFor", False)),
|
||||
}
|
||||
self._auto_continue_internal_breakpoints = bool(args.get("autoContinueInternalBreakpoints", False))
|
||||
self._pending_launch = None
|
||||
return {}, None
|
||||
|
||||
@@ -220,51 +230,55 @@ class LLDBDebugAdapter:
|
||||
if not path:
|
||||
raise RuntimeError("setBreakpoints requires source.path")
|
||||
|
||||
self._ensure_stopped_for_target_mutation("setBreakpoints")
|
||||
session = self._ensure_session()
|
||||
source_key = str(Path(path))
|
||||
for bp_id in self._source_breakpoints.get(source_key, []):
|
||||
try:
|
||||
session.breakpoint_delete(bp_id)
|
||||
except Exception:
|
||||
pass
|
||||
with self._lldb_api_lock:
|
||||
for bp_id in self._source_breakpoints.get(source_key, []):
|
||||
try:
|
||||
session.breakpoint_delete(bp_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
dap_breakpoints = []
|
||||
created_ids = []
|
||||
for item in args.get("breakpoints") or []:
|
||||
line = int(item.get("line"))
|
||||
payload = session.breakpoint_set(
|
||||
file=source_key,
|
||||
line=line,
|
||||
condition=item.get("condition"),
|
||||
allow_pending=True,
|
||||
)
|
||||
created_ids.append(payload["id"])
|
||||
dap_breakpoints.append(self._to_dap_breakpoint(payload, source_key, requested_line=line))
|
||||
dap_breakpoints = []
|
||||
created_ids = []
|
||||
for item in args.get("breakpoints") or []:
|
||||
line = int(item.get("line"))
|
||||
payload = session.breakpoint_set(
|
||||
file=source_key,
|
||||
line=line,
|
||||
condition=item.get("condition"),
|
||||
allow_pending=True,
|
||||
)
|
||||
created_ids.append(payload["id"])
|
||||
dap_breakpoints.append(self._to_dap_breakpoint(payload, source_key, requested_line=line))
|
||||
|
||||
self._source_breakpoints[source_key] = created_ids
|
||||
return {"breakpoints": dap_breakpoints}, None
|
||||
|
||||
def _handle_setFunctionBreakpoints(self, args: dict[str, Any]):
|
||||
self._ensure_stopped_for_target_mutation("setFunctionBreakpoints")
|
||||
session = self._ensure_session()
|
||||
for bp_id in self._function_breakpoints:
|
||||
try:
|
||||
session.breakpoint_delete(bp_id)
|
||||
except Exception:
|
||||
pass
|
||||
with self._lldb_api_lock:
|
||||
for bp_id in self._function_breakpoints:
|
||||
try:
|
||||
session.breakpoint_delete(bp_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self._function_breakpoints = []
|
||||
result = []
|
||||
for item in args.get("breakpoints") or []:
|
||||
name = item.get("name")
|
||||
if not name:
|
||||
continue
|
||||
payload = session.breakpoint_set(
|
||||
function=str(name),
|
||||
condition=item.get("condition"),
|
||||
allow_pending=True,
|
||||
)
|
||||
self._function_breakpoints.append(payload["id"])
|
||||
result.append(self._to_dap_breakpoint(payload))
|
||||
self._function_breakpoints = []
|
||||
result = []
|
||||
for item in args.get("breakpoints") or []:
|
||||
name = item.get("name")
|
||||
if not name:
|
||||
continue
|
||||
payload = session.breakpoint_set(
|
||||
function=str(name),
|
||||
condition=item.get("condition"),
|
||||
allow_pending=True,
|
||||
)
|
||||
self._function_breakpoints.append(payload["id"])
|
||||
result.append(self._to_dap_breakpoint(payload))
|
||||
return {"breakpoints": result}, None
|
||||
|
||||
def _handle_threads(self, _args: dict[str, Any]):
|
||||
@@ -357,16 +371,20 @@ class LLDBDebugAdapter:
|
||||
def post_send():
|
||||
self._reset_refs_for_resume()
|
||||
self._send_continued_event()
|
||||
self._ensure_session().continue_exec()
|
||||
self._emit_breakpoint_updates()
|
||||
self._emit_execution_event(default_reason="breakpoint")
|
||||
self._start_continue_thread(
|
||||
name="cli-anything-lldb-dap-continue",
|
||||
default_reason="breakpoint",
|
||||
)
|
||||
|
||||
return {"allThreadsContinued": True}, post_send
|
||||
|
||||
def _handle_pause(self, _args: dict[str, Any]):
|
||||
def post_send():
|
||||
self._ensure_session().interrupt()
|
||||
self._emit_execution_event(default_reason="pause")
|
||||
self._pause_requested = True
|
||||
self._request_async_interrupt()
|
||||
if not self._is_continue_active():
|
||||
with self._lldb_api_lock:
|
||||
self._emit_execution_event(default_reason="pause")
|
||||
|
||||
return {}, post_send
|
||||
|
||||
@@ -448,11 +466,72 @@ class LLDBDebugAdapter:
|
||||
def post_send():
|
||||
self._reset_refs_for_resume()
|
||||
self._send_continued_event()
|
||||
step_fn()
|
||||
self._emit_execution_event(default_reason="step")
|
||||
with self._lldb_api_lock:
|
||||
step_fn()
|
||||
self._emit_execution_event(default_reason="step")
|
||||
|
||||
return post_send
|
||||
|
||||
def _start_continue_thread(self, *, name: str, default_reason: str):
|
||||
with self._continue_state:
|
||||
if self._continue_active:
|
||||
self._log("continue requested while a continue operation is already active")
|
||||
return
|
||||
self._continue_active = True
|
||||
threading.Thread(
|
||||
target=self._continue_until_stop,
|
||||
kwargs={"default_reason": default_reason},
|
||||
name=name,
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
def _continue_until_stop(self, *, default_reason: str):
|
||||
try:
|
||||
self._ensure_session().continue_exec()
|
||||
except Exception as exc:
|
||||
self._log(f"continue failed: {exc}")
|
||||
self._send_event("output", {"category": "stderr", "output": f"continue failed: {exc}\n"})
|
||||
self._send_event("terminated")
|
||||
return
|
||||
finally:
|
||||
self._mark_continue_inactive()
|
||||
|
||||
with self._lldb_api_lock:
|
||||
self._emit_breakpoint_updates()
|
||||
self._emit_execution_event(default_reason=default_reason)
|
||||
|
||||
def _mark_continue_inactive(self):
|
||||
with self._continue_state:
|
||||
self._continue_active = False
|
||||
self._continue_state.notify_all()
|
||||
|
||||
def _is_continue_active(self) -> bool:
|
||||
with self._continue_state:
|
||||
return self._continue_active
|
||||
|
||||
def _ensure_stopped_for_target_mutation(self, operation: str):
|
||||
if not self._is_continue_active():
|
||||
return
|
||||
self._log(f"{operation}: interrupting running debuggee before target mutation")
|
||||
self._request_async_interrupt()
|
||||
with self._continue_state:
|
||||
stopped = self._continue_state.wait_for(
|
||||
lambda: not self._continue_active,
|
||||
timeout=self._mutation_stop_timeout,
|
||||
)
|
||||
if not stopped:
|
||||
raise RuntimeError(
|
||||
f"Timed out waiting for debuggee to stop before {operation}. "
|
||||
"Send a pause request and retry after the stopped event."
|
||||
)
|
||||
|
||||
def _request_async_interrupt(self):
|
||||
session = self._ensure_session()
|
||||
interrupt = getattr(session, "interrupt_async", None)
|
||||
if interrupt is not None:
|
||||
return interrupt()
|
||||
return session.interrupt()
|
||||
|
||||
def _emit_execution_event(self, default_reason: str | None = None):
|
||||
info = self._ensure_session().process_info()
|
||||
state = info.get("state")
|
||||
@@ -471,6 +550,9 @@ class LLDBDebugAdapter:
|
||||
reason = "entry" if default_reason == "entry" else (stop.get("reason") or default_reason or "pause")
|
||||
if reason in {"signal", "crashed"}:
|
||||
reason = "exception"
|
||||
if self._pause_requested:
|
||||
self._pause_requested = False
|
||||
reason = "pause"
|
||||
body = {
|
||||
"reason": reason,
|
||||
"threadId": info.get("selected_thread_id"),
|
||||
@@ -482,8 +564,38 @@ class LLDBDebugAdapter:
|
||||
if stop.get("description"):
|
||||
body["description"] = stop["description"]
|
||||
body["text"] = stop["description"]
|
||||
if self._should_auto_continue_internal_stop(body):
|
||||
self._send_event(
|
||||
"output",
|
||||
{
|
||||
"category": "console",
|
||||
"output": f"auto-continued internal breakpoint: {self._summarize_stop(body)}\n",
|
||||
},
|
||||
)
|
||||
self._send_continued_event(info.get("selected_thread_id"))
|
||||
self._start_continue_thread(
|
||||
name="cli-anything-lldb-dap-auto-continue",
|
||||
default_reason=default_reason or "breakpoint",
|
||||
)
|
||||
return
|
||||
self._send_event("stopped", body)
|
||||
|
||||
def _should_auto_continue_internal_stop(self, body: dict[str, Any]) -> bool:
|
||||
if not self._auto_continue_internal_breakpoints:
|
||||
return False
|
||||
if body.get("reason") == "pause":
|
||||
return False
|
||||
text = str(body.get("description") or body.get("text") or "")
|
||||
if "jit-debug-register" in text or "__jit_debug_register_code" in text:
|
||||
return True
|
||||
if "Exception 0x80000003" in text and "ntdll.dll`DbgBreakPoint" in text:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _summarize_stop(self, body: dict[str, Any]) -> str:
|
||||
text = str(body.get("description") or body.get("text") or body.get("reason") or "unknown")
|
||||
return text.splitlines()[0] if text else "unknown"
|
||||
|
||||
def _send_continued_event(self, thread_id: int | None = None):
|
||||
body: dict[str, Any] = {"allThreadsContinued": True}
|
||||
if thread_id is not None:
|
||||
@@ -616,24 +728,26 @@ class LLDBDebugAdapter:
|
||||
success: bool = True,
|
||||
message: str | None = None,
|
||||
):
|
||||
payload: dict[str, Any] = {
|
||||
"seq": self._next_seq(),
|
||||
"type": "response",
|
||||
"request_seq": request_seq,
|
||||
"success": success,
|
||||
"command": command,
|
||||
}
|
||||
if body is not None:
|
||||
payload["body"] = body
|
||||
if message:
|
||||
payload["message"] = message
|
||||
self._write(payload)
|
||||
with self._protocol_lock:
|
||||
payload: dict[str, Any] = {
|
||||
"seq": self._next_seq(),
|
||||
"type": "response",
|
||||
"request_seq": request_seq,
|
||||
"success": success,
|
||||
"command": command,
|
||||
}
|
||||
if body is not None:
|
||||
payload["body"] = body
|
||||
if message:
|
||||
payload["message"] = message
|
||||
self._write(payload)
|
||||
|
||||
def _send_event(self, event: str, body: dict[str, Any] | None = None):
|
||||
payload: dict[str, Any] = {"seq": self._next_seq(), "type": "event", "event": event}
|
||||
if body is not None:
|
||||
payload["body"] = body
|
||||
self._write(payload)
|
||||
with self._protocol_lock:
|
||||
payload: dict[str, Any] = {"seq": self._next_seq(), "type": "event", "event": event}
|
||||
if body is not None:
|
||||
payload["body"] = body
|
||||
self._write(payload)
|
||||
|
||||
def _next_seq(self) -> int:
|
||||
seq = self._seq
|
||||
|
||||
@@ -73,6 +73,19 @@ DAP variables can expose child references for structs/classes/arrays. Use
|
||||
`setVariable` only while stopped; LLDB may reject writes to optimized-out or
|
||||
read-only values.
|
||||
|
||||
For long-running GUI debuggees, DAP `continue` is non-blocking from the client's
|
||||
point of view: the adapter sends the response and `continued` event first, then
|
||||
waits for LLDB on a background thread. DAP `pause` uses LLDB async interrupt.
|
||||
If an agent needs to change breakpoints while the debuggee is running, the
|
||||
adapter interrupts first and waits for a stopped state before mutating LLDB
|
||||
breakpoints; if the target does not stop in time, retry after an explicit
|
||||
`pause`/`stopped` cycle.
|
||||
|
||||
For GUI apps that stop on debugger-internal startup or shader-JIT breakpoints,
|
||||
`launch` and `attach` accept the non-standard boolean argument
|
||||
`autoContinueInternalBreakpoints`. Enable it only when those internal stops are
|
||||
noise for the task; the adapter emits an `output` event before auto-continuing.
|
||||
|
||||
## Command Groups
|
||||
|
||||
### target
|
||||
@@ -133,6 +146,8 @@ cli-anything-lldb --json core load --path /path/to/core
|
||||
- Use REPL when a human-like interactive shell is more convenient, not because persistence requires it.
|
||||
- Unresolved CLI breakpoints fail by default; pass `--allow-pending` only when a future module/symbol load is expected.
|
||||
- DAP unresolved breakpoints use protocol semantics: `verified: false` until resolved.
|
||||
- DAP `continue` is non-blocking for long-running GUI processes, and DAP `pause` uses async interrupt.
|
||||
- DAP breakpoint changes during an active continue first interrupt and wait for a stopped state before mutating LLDB.
|
||||
- `memory find` uses a chunked scan capped at 1 MiB per call.
|
||||
- Call `target create` before process or core commands.
|
||||
- Expect structured errors: `{"error": "...", "type": "..."}`
|
||||
|
||||
@@ -42,7 +42,10 @@
|
||||
- Validate frame/variable references are cleared on resume
|
||||
- Validate EOF cleanup destroys the LLDB session
|
||||
- Validate running-state execution events emit `continued`, not a false `stopped`
|
||||
- Validate DAP `pause` calls the interrupt path and emits a pause stop event
|
||||
- Validate DAP `pause` calls the async interrupt path and emits a pause stop event
|
||||
- Validate DAP breakpoint mutation during an active continue requests async interrupt and waits for stopped state
|
||||
- Validate DAP breakpoint mutation reports a clear timeout when the target does not stop
|
||||
- Validate DAP auto-continues known internal JIT/startup breakpoint stops when explicitly enabled
|
||||
- Validate DAP transcript response/event ordering for initialize, launch, breakpoint setup, and configuration completion
|
||||
- Validate DAP `modules` and `exceptionInfo` response shapes
|
||||
- Validate DAP `readMemory` base64 encoding and expandable variable references
|
||||
@@ -157,9 +160,9 @@ python -m pytest cli_anything/lldb/tests -q
|
||||
|
||||
### Result summary
|
||||
|
||||
- `test_core.py`: 40 passed
|
||||
- `test_core.py`: 44 passed
|
||||
- `test_full_e2e.py`: 7 passed, 2 warnings from LLDB SWIG bindings
|
||||
- combined default run: 47 passed, 2 warnings from LLDB SWIG bindings
|
||||
- combined default run: 51 passed, 2 warnings from LLDB SWIG bindings
|
||||
- skip situation: 0 skipped in the current local run; older runs could skip the optional core-load negative-path scenario when `LLDB_TEST_CORE` was unset, but the fixture now creates a local placeholder core path for that negative-path test
|
||||
|
||||
### Notes
|
||||
@@ -179,3 +182,4 @@ python -m pytest cli_anything/lldb/tests -q
|
||||
- Added a real DAP source-line breakpoint E2E scenario
|
||||
- Added DAP `loadedSources` and `readMemory` coverage while keeping the harness at version 1.0.0
|
||||
- Added DAP variable child expansion, `setVariable`, `modules`, `exceptionInfo`, and transcript ordering coverage while keeping the harness at version 1.0.0
|
||||
- Added non-blocking DAP continue, async pause, guarded running-state breakpoint mutation, and internal breakpoint auto-continue coverage
|
||||
|
||||
@@ -12,6 +12,7 @@ import io
|
||||
import subprocess
|
||||
import sys
|
||||
import stat
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -199,7 +200,96 @@ class TestDAPProtocol:
|
||||
assert response["success"] is True
|
||||
assert event["event"] == "stopped"
|
||||
assert event["body"]["reason"] == "pause"
|
||||
fake_session.interrupt.assert_called_once()
|
||||
fake_session.interrupt_async.assert_called_once()
|
||||
|
||||
def test_set_breakpoints_interrupts_active_continue_before_mutation(self):
|
||||
from cli_anything.lldb.dap import LLDBDebugAdapter
|
||||
|
||||
fake_session = MagicMock()
|
||||
fake_session.breakpoint_set.return_value = {
|
||||
"id": 7,
|
||||
"resolved": True,
|
||||
"locations": 1,
|
||||
"location_details": [{"file": "C:/tmp/main.c", "line": 12}],
|
||||
}
|
||||
adapter = LLDBDebugAdapter(session_factory=lambda: fake_session)
|
||||
adapter._ensure_session()
|
||||
adapter._mutation_stop_timeout = 1.0
|
||||
with adapter._continue_state:
|
||||
adapter._continue_active = True
|
||||
|
||||
release = threading.Timer(0.01, adapter._mark_continue_inactive)
|
||||
release.start()
|
||||
try:
|
||||
body, post_send = adapter._handle_setBreakpoints(
|
||||
{
|
||||
"source": {"path": "C:/tmp/main.c"},
|
||||
"breakpoints": [{"line": 12}],
|
||||
}
|
||||
)
|
||||
finally:
|
||||
release.join()
|
||||
|
||||
assert post_send is None
|
||||
assert body["breakpoints"][0]["verified"] is True
|
||||
fake_session.interrupt_async.assert_called_once()
|
||||
fake_session.breakpoint_set.assert_called_once()
|
||||
|
||||
def test_set_breakpoints_reports_timeout_if_running_target_will_not_stop(self):
|
||||
from cli_anything.lldb.dap import LLDBDebugAdapter
|
||||
|
||||
fake_session = MagicMock()
|
||||
adapter = LLDBDebugAdapter(session_factory=lambda: fake_session)
|
||||
adapter._ensure_session()
|
||||
adapter._mutation_stop_timeout = 0.01
|
||||
with adapter._continue_state:
|
||||
adapter._continue_active = True
|
||||
|
||||
try:
|
||||
with pytest.raises(RuntimeError, match="Timed out waiting for debuggee to stop"):
|
||||
adapter._handle_setBreakpoints(
|
||||
{
|
||||
"source": {"path": "C:/tmp/main.c"},
|
||||
"breakpoints": [{"line": 12}],
|
||||
}
|
||||
)
|
||||
finally:
|
||||
adapter._mark_continue_inactive()
|
||||
|
||||
fake_session.interrupt_async.assert_called_once()
|
||||
fake_session.breakpoint_set.assert_not_called()
|
||||
|
||||
def test_auto_continue_internal_breakpoint_emits_output_and_resumes(self):
|
||||
from cli_anything.lldb.dap import LLDBDebugAdapter, read_message
|
||||
|
||||
fake_session = MagicMock()
|
||||
fake_session.process_info.return_value = {
|
||||
"state": "stopped",
|
||||
"selected_thread_id": 99,
|
||||
"stop": {
|
||||
"reason": "breakpoint",
|
||||
"description": "frame #0: nvgpucomp64.dll`__jit_debug_register_code",
|
||||
"hit_breakpoint_ids": [],
|
||||
},
|
||||
"exit_status": 0,
|
||||
}
|
||||
out = io.BytesIO()
|
||||
adapter = LLDBDebugAdapter(session_factory=lambda: fake_session)
|
||||
adapter._out = out
|
||||
adapter._auto_continue_internal_breakpoints = True
|
||||
adapter._start_continue_thread = MagicMock()
|
||||
|
||||
adapter._emit_execution_event(default_reason="breakpoint")
|
||||
out.seek(0)
|
||||
output_event = read_message(out)
|
||||
continued_event = read_message(out)
|
||||
stopped_event = read_message(out)
|
||||
|
||||
assert output_event["event"] == "output"
|
||||
assert "auto-continued internal breakpoint" in output_event["body"]["output"]
|
||||
assert continued_event["event"] == "continued"
|
||||
assert stopped_event is None
|
||||
adapter._start_continue_thread.assert_called_once()
|
||||
|
||||
def test_stack_trace_reports_total_frames(self):
|
||||
from cli_anything.lldb.dap import LLDBDebugAdapter, read_message
|
||||
@@ -573,6 +663,20 @@ class TestSessionLifecycle:
|
||||
process.Stop.assert_called_once()
|
||||
assert payload["pid"] == 123
|
||||
|
||||
def test_interrupt_async_requests_async_interrupt(self):
|
||||
from cli_anything.lldb.core.session import LLDBSession
|
||||
|
||||
session = self._make_session()
|
||||
process = MagicMock()
|
||||
process.IsValid.return_value = True
|
||||
process.SendAsyncInterrupt.return_value = MagicMock(Success=lambda: True)
|
||||
session.process = process
|
||||
|
||||
payload = LLDBSession.interrupt_async(session)
|
||||
|
||||
process.SendAsyncInterrupt.assert_called_once()
|
||||
assert payload == {"status": "interrupt_requested"}
|
||||
|
||||
def test_session_status_reports_target_and_process(self):
|
||||
from cli_anything.lldb.core.session import LLDBSession
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ _ALLOWED_SESSION_METHODS = {
|
||||
"step_out",
|
||||
"continue_exec",
|
||||
"interrupt",
|
||||
"interrupt_async",
|
||||
"backtrace",
|
||||
"locals",
|
||||
"local_values",
|
||||
|
||||
@@ -73,6 +73,19 @@ DAP variables can expose child references for structs/classes/arrays. Use
|
||||
`setVariable` only while stopped; LLDB may reject writes to optimized-out or
|
||||
read-only values.
|
||||
|
||||
For long-running GUI debuggees, DAP `continue` is non-blocking from the client's
|
||||
point of view: the adapter sends the response and `continued` event first, then
|
||||
waits for LLDB on a background thread. DAP `pause` uses LLDB async interrupt.
|
||||
If an agent needs to change breakpoints while the debuggee is running, the
|
||||
adapter interrupts first and waits for a stopped state before mutating LLDB
|
||||
breakpoints; if the target does not stop in time, retry after an explicit
|
||||
`pause`/`stopped` cycle.
|
||||
|
||||
For GUI apps that stop on debugger-internal startup or shader-JIT breakpoints,
|
||||
`launch` and `attach` accept the non-standard boolean argument
|
||||
`autoContinueInternalBreakpoints`. Enable it only when those internal stops are
|
||||
noise for the task; the adapter emits an `output` event before auto-continuing.
|
||||
|
||||
## Command Groups
|
||||
|
||||
### target
|
||||
@@ -133,6 +146,8 @@ cli-anything-lldb --json core load --path /path/to/core
|
||||
- Use REPL when a human-like interactive shell is more convenient, not because persistence requires it.
|
||||
- Unresolved CLI breakpoints fail by default; pass `--allow-pending` only when a future module/symbol load is expected.
|
||||
- DAP unresolved breakpoints use protocol semantics: `verified: false` until resolved.
|
||||
- DAP `continue` is non-blocking for long-running GUI processes, and DAP `pause` uses async interrupt.
|
||||
- DAP breakpoint changes during an active continue first interrupt and wait for a stopped state before mutating LLDB.
|
||||
- `memory find` uses a chunked scan capped at 1 MiB per call.
|
||||
- Call `target create` before process or core commands.
|
||||
- Expect structured errors: `{"error": "...", "type": "..."}`
|
||||
|
||||
Reference in New Issue
Block a user