[Lldb-commits] [lldb] [lldb-dap] IO Handling refactor to support raw input mode. (PR #180858)
John Harrison via lldb-commits
lldb-commits at lists.llvm.org
Thu Feb 12 09:58:52 PST 2026
https://github.com/ashgti updated https://github.com/llvm/llvm-project/pull/180858
>From 2000bbe4c9ec8cc19e776273dbe4459106f4f505 Mon Sep 17 00:00:00 2001
From: John Harrison <harjohn at google.com>
Date: Thu, 29 Jan 2026 16:22:19 -0800
Subject: [PATCH 1/5] [lldb-dap] IO Handling refactor to support raw input
mode.
Updating lldb-dap to use a pty (when supported) for the debugger input/output handles. This enables us to support raw-input mode commands like `script` or `breakpoint command add`.
As a fallback on platforms that don't support a pty we use a pipe, which also works but has some usability issues. With a pipe, we do NOT see the prompt and some commands do not show us any helpful information, e.g. `breakpoint command add` does NOT tell you to use `DONE` to end the command. The command does work, just not as helpful as with a pty.
With a pty we see the prompt in the console output and it often shows up in the command output as well.
---
lldb/packages/Python/lldbsuite/test/dotest.py | 11 +-
.../test/tools/lldb-dap/dap_server.py | 40 ++-
.../test/tools/lldb-dap/lldbdap_testcase.py | 4 +-
.../Interpreter/embedded_interpreter.py | 3 +
.../tools/lldb-dap/cancel/TestDAP_cancel.py | 41 +--
.../API/tools/lldb-dap/cancel/busy_loop.py | 17 ++
.../completions/TestDAP_completions.py | 3 +-
.../tools/lldb-dap/console/TestDAP_console.py | 4 +
.../lldb-dap/evaluate/TestDAP_evaluate.py | 10 +-
.../lldb-dap/launch/TestDAP_launch_version.py | 7 +-
.../lldb-dap/progress/TestDAP_Progress.py | 5 +-
.../repl-mode/TestDAP_repl_mode_detection.py | 13 +-
.../lldb-dap/send-event/TestDAP_sendEvent.py | 3 +-
.../lldb-dap/variables/TestDAP_variables.py | 8 +-
lldb/tools/lldb-dap/CMakeLists.txt | 1 +
lldb/tools/lldb-dap/DAP.cpp | 247 ++++++++++++++----
lldb/tools/lldb-dap/DAP.h | 47 +++-
lldb/tools/lldb-dap/EvaluateContext.cpp | 127 +++++++++
lldb/tools/lldb-dap/EvaluateContext.h | 48 ++++
lldb/tools/lldb-dap/EventHelper.cpp | 52 ++--
.../Handler/CompletionsRequestHandler.cpp | 1 +
.../ConfigurationDoneRequestHandler.cpp | 3 +
.../Handler/EvaluateRequestHandler.cpp | 106 +++-----
.../Handler/VariablesRequestHandler.cpp | 62 +++--
lldb/tools/lldb-dap/ProgressEvent.cpp | 2 +-
.../lldb-dap/Protocol/ProtocolRequests.cpp | 4 +-
.../lldb-dap/Protocol/ProtocolRequests.h | 4 +-
lldb/tools/lldb-dap/Variables.cpp | 21 ++
lldb/tools/lldb-dap/Variables.h | 4 +
lldb/unittests/DAP/Handler/DisconnectTest.cpp | 11 +-
lldb/unittests/DAP/TestBase.cpp | 23 +-
lldb/unittests/DAP/TestBase.h | 1 -
32 files changed, 679 insertions(+), 254 deletions(-)
create mode 100644 lldb/test/API/tools/lldb-dap/cancel/busy_loop.py
create mode 100644 lldb/tools/lldb-dap/EvaluateContext.cpp
create mode 100644 lldb/tools/lldb-dap/EvaluateContext.h
diff --git a/lldb/packages/Python/lldbsuite/test/dotest.py b/lldb/packages/Python/lldbsuite/test/dotest.py
index 533be0a065e3a..f357db9d58f14 100644
--- a/lldb/packages/Python/lldbsuite/test/dotest.py
+++ b/lldb/packages/Python/lldbsuite/test/dotest.py
@@ -569,11 +569,12 @@ def setupSysPath():
lldbDir = os.path.dirname(lldbtest_config.lldbExec)
- lldbDAPExec = os.path.join(
- lldbDir, "lldb-dap.exe" if sys.platform == "win32" else "lldb-dap"
- )
- if is_exe(lldbDAPExec):
- os.environ["LLDBDAP_EXEC"] = lldbDAPExec
+ if not os.environ.get("LLDBDAP_EXEC", None):
+ lldbDAPExec = os.path.join(
+ lldbDir, "lldb-dap.exe" if sys.platform == "win32" else "lldb-dap"
+ )
+ if is_exe(lldbDAPExec):
+ os.environ["LLDBDAP_EXEC"] = lldbDAPExec
configuration.yaml2macho_core = shutil.which("yaml2macho-core", path=lldbDir)
diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
index 840c087ceec4d..f3efc7b23214b 100644
--- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
+++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
@@ -299,7 +299,7 @@ def __init__(
self.threads: Optional[dict] = None
self.stopped_thread: Optional[dict] = None
self.thread_stacks: Optional[Dict[int, List[dict]]]
- self.thread_stop_reasons: Dict[str, Any] = {}
+ self.thread_stop_reasons: Dict[int, Any] = {}
self.focused_tid: Optional[int] = None
self.frame_scopes: Dict[str, Any] = {}
# keyed by breakpoint id
@@ -486,9 +486,10 @@ def _process_recv_packets(self) -> None:
with self._recv_condition:
for packet in self._recv_packets:
if packet and ("seq" not in packet or packet["seq"] == 0):
- raise ValueError(
- f"received a malformed packet, expected 'seq != 0' for {packet!r}"
- )
+ pass
+ # raise ValueError(
+ # f"received a malformed packet, expected 'seq != 0' for {packet!r}"
+ # )
if packet:
self._log.messages.append((Log.Dir.RECV, packet))
# Handle events that may modify any stateful properties of
@@ -540,10 +541,13 @@ def _handle_event(self, packet: Event) -> None:
# reasons since the 'threads' command doesn't return
# that information.
self._process_stopped()
- tid = body["threadId"]
- self.thread_stop_reasons[tid] = body
- if "preserveFocusHint" not in body or not body["preserveFocusHint"]:
- self.focused_tid = tid
+ if "allThreadsStopped" in body and body["allThreadsStopped"]:
+ self.thread_stop_reasons = {}
+ if "threadId" in body:
+ tid = body["threadId"]
+ self.thread_stop_reasons[tid] = body
+ if "preserveFocusHint" not in body or not body["preserveFocusHint"]:
+ self.focused_tid = tid
elif event.startswith("progress"):
# Progress events come in as 'progressStart', 'progressUpdate',
# and 'progressEnd' events. Keep these around in case test
@@ -663,11 +667,13 @@ def predicate(p: ProtocolMessage):
return cast(Response, self._recv_packet(predicate=predicate))
- def wait_for_event(self, filter: List[str]) -> Event:
+ def wait_for_event(
+ self, filter: List[str], pred: Callable[[Event], bool] = lambda _: True
+ ) -> Event:
"""Wait for the first event that matches the filter."""
def predicate(p: ProtocolMessage):
- return p["type"] == "event" and p["event"] in filter
+ return p["type"] == "event" and p["event"] in filter and pred(p)
return cast(Event, self._recv_packet(predicate=predicate))
@@ -706,6 +712,18 @@ def ensure_initialized(self) -> None:
), "configuration done has already been sent, 'initialized' should have already occurred"
self.wait_for_initialized()
+ def collect_progress(self, title: str) -> List[Event]:
+ start_event = self.wait_for_event(
+ ["progressStart"], lambda event: event["body"]["title"] == title
+ )
+ progress_id = start_event["body"]["progressId"]
+ self.wait_for_event(
+ ["progressEnd"], lambda event: event["body"]["progressId"] == progress_id
+ )
+ return [
+ e for e in self.progress_events if e["body"]["progressId"] == progress_id
+ ]
+
def wait_for_stopped(self) -> List[Event]:
"""Wait for the next 'stopped' event to occur, coalescing all stopped events within a given quiet period."""
return self.collect_events(["stopped", "exited"])
@@ -1162,6 +1180,8 @@ def request_evaluate(
args_dict["frameId"] = stackFrame["id"]
if context:
+ if context == "repl" and not self.configuration_done_sent:
+ raise ValueError("configurtionDone must be called to activate the repl")
args_dict["context"] = context
if is_hex is not None:
args_dict["format"] = {"hex": is_hex}
diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py
index f3c16bd849a48..daa3823cac77e 100644
--- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py
+++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py
@@ -241,11 +241,13 @@ def verify_stop_exception_info(
def verify_stop_on_entry(self) -> None:
"""Waits for the process to be stopped and then verifies at least one
thread has the stop reason 'entry'."""
+ if not self.dap_server.configuration_done_sent:
+ self.verify_configuration_done()
self.dap_server.wait_for_stopped()
self.assertIn(
"entry",
(t["reason"] for t in self.dap_server.thread_stop_reasons.values()),
- "Expected at least one thread to report stop reason 'entry' in {self.dap_server.thread_stop_reasons}",
+ f"Expected at least one thread to report stop reason 'entry' in {self.dap_server.thread_stop_reasons}",
)
def verify_commands(self, flavor: str, output: str, commands: List[str]):
diff --git a/lldb/source/Interpreter/embedded_interpreter.py b/lldb/source/Interpreter/embedded_interpreter.py
index 12c47bd712816..b8d0cfeccd94e 100644
--- a/lldb/source/Interpreter/embedded_interpreter.py
+++ b/lldb/source/Interpreter/embedded_interpreter.py
@@ -49,6 +49,8 @@ def readfunc(prompt):
def readfunc_stdio(prompt):
+ # Flush stdout and stderr to ensure consistency.
+ sys.stderr.flush()
sys.stdout.write(prompt)
sys.stdout.flush()
line = sys.stdin.readline()
@@ -77,6 +79,7 @@ def run_python_interpreter(local_dict):
if e.code:
print("Script exited with code %s" % e.code)
+
def run_one_line(local_dict, input_string):
global g_run_one_line_str
try:
diff --git a/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py b/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py
index 14789a6694686..8429941ad574f 100644
--- a/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py
+++ b/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py
@@ -2,10 +2,8 @@
Test lldb-dap cancel request
"""
+import os
import time
-
-from lldbsuite.test.decorators import *
-from lldbsuite.test.lldbtest import *
import lldbdap_testcase
@@ -13,25 +11,21 @@ class TestDAP_cancel(lldbdap_testcase.DAPTestCaseBase):
def send_async_req(self, command: str, arguments: dict = {}) -> int:
return self.dap_server.send_packet(
{
+ "seq": 0,
"type": "request",
"command": command,
"arguments": arguments,
}
)
- def async_blocking_request(self, duration: float) -> int:
+ def async_blocking_request(self, count: int) -> int:
"""
- Sends an evaluate request that will sleep for the specified duration to
+ Sends an evaluate request that will sleep for the specified count to
block the request handling thread.
"""
return self.send_async_req(
command="evaluate",
- arguments={
- "expression": '`script import time; print("starting sleep", file=lldb.debugger.GetOutputFileHandle()); time.sleep({})'.format(
- duration
- ),
- "context": "repl",
- },
+ arguments={"expression": f"`busy-loop {count}", "context": "repl"},
)
def async_cancel(self, requestId: int) -> int:
@@ -42,14 +36,20 @@ def test_pending_request(self):
Tests cancelling a pending request.
"""
program = self.getBuildArtifact("a.out")
- self.build_and_launch(program)
+ busy_loop = os.path.join(os.path.dirname(__file__), "busy_loop.py")
+ self.build_and_launch(
+ program,
+ initCommands=[f"command script import {busy_loop}"],
+ stopOnEntry=True,
+ )
+ self.verify_stop_on_entry()
# Use a relatively short timeout since this is only to ensure the
# following request is queued.
- blocking_seq = self.async_blocking_request(duration=self.DEFAULT_TIMEOUT / 10)
+ blocking_seq = self.async_blocking_request(count=1)
# Use a longer timeout to ensure we catch if the request was interrupted
# properly.
- pending_seq = self.async_blocking_request(duration=self.DEFAULT_TIMEOUT / 2)
+ pending_seq = self.async_blocking_request(count=10)
cancel_seq = self.async_cancel(requestId=pending_seq)
blocking_resp = self.dap_server.receive_response(blocking_seq)
@@ -74,11 +74,18 @@ def test_inflight_request(self):
Tests cancelling an inflight request.
"""
program = self.getBuildArtifact("a.out")
- self.build_and_launch(program)
+ busy_loop = os.path.join(os.path.dirname(__file__), "busy_loop.py")
+ self.build_and_launch(
+ program,
+ initCommands=[f"command script import {busy_loop}"],
+ stopOnEntry=True,
+ )
+ self.verify_configuration_done()
+ self.verify_stop_on_entry()
- blocking_seq = self.async_blocking_request(duration=self.DEFAULT_TIMEOUT / 2)
+ blocking_seq = self.async_blocking_request(count=10)
# Wait for the sleep to start to cancel the inflight request.
- self.collect_console(pattern="starting sleep")
+ time.sleep(0.5)
cancel_seq = self.async_cancel(requestId=blocking_seq)
blocking_resp = self.dap_server.receive_response(blocking_seq)
diff --git a/lldb/test/API/tools/lldb-dap/cancel/busy_loop.py b/lldb/test/API/tools/lldb-dap/cancel/busy_loop.py
new file mode 100644
index 0000000000000..21108d7e32e45
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/cancel/busy_loop.py
@@ -0,0 +1,17 @@
+import time
+import lldb
+
+
+ at lldb.command(command_name="busy-loop")
+def busy_loop(debugger, command, exe_ctx, result, internal_dict):
+ """Test helper as a busy loop."""
+ if not command:
+ command = "10"
+ count = int(command)
+ print("Starting loop...", count)
+ for i in range(count):
+ if debugger.InterruptRequested():
+ print("interrupt requested, stopping loop", i)
+ break
+ print("No interrupted requested, sleeping", i)
+ time.sleep(1)
diff --git a/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py b/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py
index dd5fdcc4bbad4..db7d04829f046 100644
--- a/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py
+++ b/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py
@@ -377,14 +377,13 @@ def test_auto_completions(self):
Tests completion requests in "repl-mode=auto"
"""
self.setup_debuggee()
+ self.continue_to_next_stop()
res = self.dap_server.request_evaluate(
"`lldb-dap repl-mode auto", context="repl"
)
self.assertTrue(res["success"])
- self.continue_to_next_stop()
-
# Stopped at breakpoint 1
# 'var' variable is in scope, completions should not show any warning.
self.dap_server.get_completions("var ")
diff --git a/lldb/test/API/tools/lldb-dap/console/TestDAP_console.py b/lldb/test/API/tools/lldb-dap/console/TestDAP_console.py
index ceddaeb50cd3b..cd0a8e715de42 100644
--- a/lldb/test/API/tools/lldb-dap/console/TestDAP_console.py
+++ b/lldb/test/API/tools/lldb-dap/console/TestDAP_console.py
@@ -164,8 +164,12 @@ def test_exit_status_message_ok(self):
)
def test_diagnositcs(self):
+ source = "main.cpp"
program = self.getBuildArtifact("a.out")
self.build_and_launch(program)
+ breakpoint1_line = line_number(source, "// breakpoint 1")
+ breakpoint_ids = self.set_source_breakpoints(source, [breakpoint1_line])
+ self.continue_to_breakpoints(breakpoint_ids)
core = self.getBuildArtifact("minidump.core")
self.yaml2obj("minidump.yaml", core)
diff --git a/lldb/test/API/tools/lldb-dap/evaluate/TestDAP_evaluate.py b/lldb/test/API/tools/lldb-dap/evaluate/TestDAP_evaluate.py
index 98ed0b9df228a..e2e940c21ad0e 100644
--- a/lldb/test/API/tools/lldb-dap/evaluate/TestDAP_evaluate.py
+++ b/lldb/test/API/tools/lldb-dap/evaluate/TestDAP_evaluate.py
@@ -138,11 +138,12 @@ def run_test_evaluate_expressions(
self.assertEvaluate("var1", "20", want_type="int")
# Empty expression should equate to the previous expression.
if context == "repl":
- self.assertEvaluate("", "20")
+ self.assertEvaluate("", r"\(lldb\) ", want_memref=False)
else:
self.assertEvaluateFailure("")
self.assertEvaluate("var2", "21", want_type="int")
if context == "repl":
+ self.assertEvaluate("e var2", "21", want_memref=False)
self.assertEvaluate("", "21", want_type="int")
self.assertEvaluate("", "21", want_type="int")
self.assertEvaluate("static_int", "0x0000002a", want_type="int", is_hex=True)
@@ -210,15 +211,14 @@ def run_test_evaluate_expressions(
"struct3", "0x.*0", want_varref=True, want_type="my_struct *"
)
- if context == "repl" or context is None:
- # In repl or unknown context expressions may be interpreted as lldb
- # commands since no variables have the same name as the command.
+ if context == "repl":
+ # In repl context expressions may be interpreted as lldb commands
+ # since no variables have the same name as the command.
self.assertEvaluate("list", r".*", want_memref=False)
# Changing the frame index should not make a difference
self.assertEvaluate(
"version", r".*lldb.+", want_memref=False, frame_index=1
)
-
else:
self.assertEvaluateFailure("list") # local variable of a_function
diff --git a/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch_version.py b/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch_version.py
index a598db41595a5..b57cff45a6ec6 100644
--- a/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch_version.py
+++ b/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch_version.py
@@ -30,7 +30,12 @@ def test(self):
version_eval_response = self.dap_server.request_evaluate(
"`version", context="repl"
)
- version_eval_output = version_eval_response["body"]["result"]
+ version_eval_output = (
+ version_eval_response["body"]["result"]
+ .removeprefix("(lldb) ")
+ .removesuffix("(lldb) ")
+ )
+ # Strip the prompt which may show up in the repl output.
version_string = self.dap_server.get_capability("$__lldb_version")
self.assertEqual(
diff --git a/lldb/test/API/tools/lldb-dap/progress/TestDAP_Progress.py b/lldb/test/API/tools/lldb-dap/progress/TestDAP_Progress.py
index 3f57dfb66024d..7a1b6608c1e43 100755
--- a/lldb/test/API/tools/lldb-dap/progress/TestDAP_Progress.py
+++ b/lldb/test/API/tools/lldb-dap/progress/TestDAP_Progress.py
@@ -21,12 +21,10 @@ def verify_progress_events(
expected_not_in_message=None,
only_verify_first_update=False,
):
- self.dap_server.wait_for_event(["progressEnd"])
- self.assertTrue(len(self.dap_server.progress_events) > 0)
start_found = False
update_found = False
end_found = False
- for event in self.dap_server.progress_events:
+ for event in self.dap_server.collect_progress(expected_title):
event_type = event["event"]
if "progressStart" in event_type:
title = event["body"]["title"]
@@ -55,6 +53,7 @@ def verify_progress_events(
def test(self):
program = self.getBuildArtifact("a.out")
self.build_and_launch(program, stopOnEntry=True)
+ self.verify_stop_on_entry()
progress_emitter = os.path.join(os.getcwd(), "Progress_emitter.py")
self.dap_server.request_evaluate(
f"`command script import {progress_emitter}", context="repl"
diff --git a/lldb/test/API/tools/lldb-dap/repl-mode/TestDAP_repl_mode_detection.py b/lldb/test/API/tools/lldb-dap/repl-mode/TestDAP_repl_mode_detection.py
index c6f59949d668e..d2d99b6457b77 100644
--- a/lldb/test/API/tools/lldb-dap/repl-mode/TestDAP_repl_mode_detection.py
+++ b/lldb/test/API/tools/lldb-dap/repl-mode/TestDAP_repl_mode_detection.py
@@ -3,8 +3,6 @@
"""
import lldbdap_testcase
-import dap_server
-from lldbsuite.test import lldbutil
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
@@ -20,20 +18,23 @@ def assertEvaluate(self, expression, regex):
def test_completions(self):
program = self.getBuildArtifact("a.out")
- self.build_and_launch(program)
+ self.build_and_launch(program, stopOnEntry=True)
source = "main.cpp"
breakpoint1_line = line_number(source, "// breakpoint 1")
breakpoint2_line = line_number(source, "// breakpoint 2")
self.set_source_breakpoints(source, [breakpoint1_line, breakpoint2_line])
+ self.verify_stop_on_entry()
# The result of the commands should return the empty string.
- self.assertEvaluate("`command regex user_command s/^$/platform/", r"^$")
- self.assertEvaluate("`command alias alias_command platform", r"^$")
+ self.assertEvaluate(
+ "`command regex user_command s/^$/platform/", r"^\(lldb\) $"
+ )
+ self.assertEvaluate("`command alias alias_command platform", r"^\(lldb\) $")
self.assertEvaluate(
"`command alias alias_command_with_arg platform select --sysroot %1 remote-linux",
- r"^$",
+ r"^\(lldb\) $",
)
self.continue_to_next_stop()
diff --git a/lldb/test/API/tools/lldb-dap/send-event/TestDAP_sendEvent.py b/lldb/test/API/tools/lldb-dap/send-event/TestDAP_sendEvent.py
index a01845669666f..ae211b36f7c0d 100644
--- a/lldb/test/API/tools/lldb-dap/send-event/TestDAP_sendEvent.py
+++ b/lldb/test/API/tools/lldb-dap/send-event/TestDAP_sendEvent.py
@@ -61,7 +61,8 @@ def test_send_internal_event(self):
resp = self.dap_server.request_evaluate(
"`lldb-dap send-event stopped", context="repl"
)
+ self.assertFalse(resp["success"], f"Expected error response in {resp}")
self.assertRegex(
- resp["body"]["result"],
+ resp["body"]["error"]["format"],
r"Invalid use of lldb-dap send-event, event \"stopped\" should be handled by lldb-dap internally.",
)
diff --git a/lldb/test/API/tools/lldb-dap/variables/TestDAP_variables.py b/lldb/test/API/tools/lldb-dap/variables/TestDAP_variables.py
index 1dbb0143e7a55..7e563d1ea402d 100644
--- a/lldb/test/API/tools/lldb-dap/variables/TestDAP_variables.py
+++ b/lldb/test/API/tools/lldb-dap/variables/TestDAP_variables.py
@@ -139,8 +139,7 @@ def darwin_dwarf_missing_obj(self, initCommands):
},
},
}
- varref_dict = {}
- self.verify_variables(verify_locals, locals, varref_dict)
+ self.verify_variables(verify_locals, locals)
def do_test_scopes_variables_setVariable_evaluate(
self, enableAutoVariableSummaries: bool
@@ -580,8 +579,9 @@ def do_test_scopes_and_evaluate_expansion(self, enableAutoVariableSummaries: boo
var_ref = expr_varref_dict[expandable_expression["name"]]
response = self.dap_server.request_variables(var_ref)
- self.verify_variables(
- expandable_expression["children"], response["body"]["variables"]
+ self.assertFalse(
+ response["success"],
+ f"variable reference should be out of scope in {response}",
)
# Test that frame scopes have corresponding presentation hints.
diff --git a/lldb/tools/lldb-dap/CMakeLists.txt b/lldb/tools/lldb-dap/CMakeLists.txt
index 8566c663fcc06..47ab3669442f7 100644
--- a/lldb/tools/lldb-dap/CMakeLists.txt
+++ b/lldb/tools/lldb-dap/CMakeLists.txt
@@ -11,6 +11,7 @@ add_lldb_library(lldbDAP
DAPError.cpp
DAPLog.cpp
DAPSessionManager.cpp
+ EvaluateContext.cpp
EventHelper.cpp
ExceptionBreakpoint.cpp
FifoFiles.cpp
diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp
index 5ef384706aab0..4dd61d6cf6320 100644
--- a/lldb/tools/lldb-dap/DAP.cpp
+++ b/lldb/tools/lldb-dap/DAP.cpp
@@ -17,22 +17,21 @@
#include "LLDBUtils.h"
#include "OutputRedirector.h"
#include "Protocol/ProtocolBase.h"
-#include "Protocol/ProtocolEvents.h"
#include "Protocol/ProtocolRequests.h"
#include "Protocol/ProtocolTypes.h"
#include "ProtocolUtils.h"
-#include "Transport.h"
-#include "lldb/API/SBBreakpoint.h"
#include "lldb/API/SBCommandInterpreter.h"
+#include "lldb/API/SBCommandInterpreterRunOptions.h"
#include "lldb/API/SBEvent.h"
#include "lldb/API/SBLanguageRuntime.h"
#include "lldb/API/SBListener.h"
#include "lldb/API/SBMutex.h"
#include "lldb/API/SBProcess.h"
-#include "lldb/API/SBStream.h"
-#include "lldb/Host/JSONTransport.h"
+#include "lldb/Host/File.h"
#include "lldb/Host/MainLoop.h"
#include "lldb/Host/MainLoopBase.h"
+#include "lldb/Host/Pipe.h"
+#include "lldb/Host/PseudoTerminal.h"
#include "lldb/Utility/Status.h"
#include "lldb/lldb-defines.h"
#include "lldb/lldb-enumerations.h"
@@ -56,12 +55,13 @@
#include <cstdint>
#include <cstdio>
#include <functional>
-#include <future>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
+#include <sys/fcntl.h>
#include <thread>
+#include <tuple>
#include <utility>
#include <variant>
@@ -78,14 +78,6 @@ using namespace lldb_dap;
using namespace lldb_dap::protocol;
using namespace lldb_private;
-namespace {
-#ifdef _WIN32
-const char DEV_NULL[] = "nul";
-#else
-const char DEV_NULL[] = "/dev/null";
-#endif
-} // namespace
-
namespace lldb_dap {
static std::string GetStringFromStructuredData(lldb::SBStructuredData &data,
@@ -224,17 +216,92 @@ ExceptionBreakpoint *DAP::GetExceptionBreakpoint(const lldb::break_id_t bp_id) {
return nullptr;
}
+llvm::Expected<std::pair<IOWrapper, IOWrapper>>
+IOWrapper::CreatePseudoTerminal() {
+ // For testing purposes return an error to cause us to fallback to a pair of
+ // pipes.
+ if (getenv("LLDB_DAP_FORCE_REPL_PIPE"))
+ return llvm::createStringError("forced repl pipe");
+
+ lldb_private::PseudoTerminal pty;
+ if (auto err = pty.OpenFirstAvailablePrimary(O_RDWR | O_NOCTTY | O_CLOEXEC))
+ return err;
+ if (auto err = pty.OpenSecondary(O_RDWR | O_NOCTTY | O_CLOEXEC))
+ return err;
+
+ lldb_private::Terminal term(pty.GetPrimaryFileDescriptor());
+ if (llvm::Error err = term.SetCanonical(false))
+ return err;
+ if (llvm::Error err = term.SetRaw())
+ return err;
+
+ int primary_fd = pty.ReleasePrimaryFileDescriptor();
+ int replica_fd = pty.ReleaseSecondaryFileDescriptor();
+
+ return std::make_pair(
+ IOWrapper(
+ /*read=*/std::make_shared<lldb_private::NativeFile>(
+ primary_fd, lldb_private::File::eOpenOptionReadOnly, false),
+ /*write=*/std::make_shared<lldb_private::NativeFile>(
+ primary_fd, lldb_private::File::eOpenOptionWriteOnly, true)),
+ IOWrapper(
+ /*read=*/std::make_shared<lldb_private::NativeFile>(
+ replica_fd, lldb_private::File::eOpenOptionReadOnly, false),
+ /*write=*/std::make_shared<lldb_private::NativeFile>(
+ replica_fd, lldb_private::File::eOpenOptionWriteOnly, true)));
+}
+
+// Hookup a pair of pipes such that reading from one writes to the other.
+llvm::Expected<std::pair<IOWrapper, IOWrapper>> IOWrapper::CreatePipePair() {
+ lldb_private::Pipe primary_pipe, replica_pipe;
+ if (auto err = primary_pipe.CreateNew().takeError())
+ return err;
+ if (auto err = replica_pipe.CreateNew().takeError())
+ return err;
+
+ return std::make_pair(
+ IOWrapper(/*read=*/
+ std::make_shared<lldb_private::NativeFile>(
+ primary_pipe.ReleaseReadFileDescriptor(),
+ lldb_private::File::eOpenOptionReadOnly, false),
+ /*write=*/std::make_shared<lldb_private::NativeFile>(
+ replica_pipe.ReleaseWriteFileDescriptor(),
+ lldb_private::File::eOpenOptionWriteOnly, true)),
+ IOWrapper(/*read=*/
+ std::make_shared<lldb_private::NativeFile>(
+ replica_pipe.ReleaseReadFileDescriptor(),
+ lldb_private::File::eOpenOptionReadOnly, false),
+ /*write=*/std::make_shared<lldb_private::NativeFile>(
+ primary_pipe.ReleaseWriteFileDescriptor(),
+ lldb_private::File::eOpenOptionWriteOnly, true)));
+}
+
llvm::Error DAP::ConfigureIO(std::FILE *overrideOut, std::FILE *overrideErr) {
- in = lldb::SBFile(std::fopen(DEV_NULL, "r"), /*transfer_ownership=*/true);
+ DAP_LOG(log, "Initializing IO");
+
+ llvm::Expected<std::pair<IOWrapper, IOWrapper>> io =
+ IOWrapper::CreatePseudoTerminal();
+ if (!io) {
+ DAP_LOG_ERROR(
+ log, io.takeError(),
+ "Falling back to a pipe for debugger IO, creating a pty failed: {0}");
+ io = IOWrapper::CreatePipePair();
+ if (!io)
+ return io.takeError();
+ }
+
+ std::tie(primary, replica) = *io;
- if (auto Error = out.RedirectTo(overrideOut, [this](llvm::StringRef output) {
- SendOutput(OutputType::Console, output);
- }))
+ if (auto Error = stdout_redirect.RedirectTo(
+ overrideOut, [this](llvm::StringRef output) {
+ SendOutput(OutputType::Console, output);
+ }))
return Error;
- if (auto Error = err.RedirectTo(overrideErr, [this](llvm::StringRef output) {
- SendOutput(OutputType::Console, output);
- }))
+ if (auto Error = stderr_redirect.RedirectTo(
+ overrideErr, [this](llvm::StringRef output) {
+ SendOutput(OutputType::Console, output);
+ }))
return Error;
return llvm::Error::success();
@@ -596,6 +663,9 @@ lldb::SBFrame DAP::GetLLDBFrame(const llvm::json::Object &arguments) {
ReplMode DAP::DetectReplMode(lldb::SBFrame &frame, std::string &expression,
bool partial_expression) {
+ if (!debugger.GetCommandInterpreter().IsActive() || expression.empty())
+ return ReplMode::Command;
+
// Check for the escape hatch prefix.
if (llvm::StringRef expr_ref = expression;
expr_ref.consume_front(configuration.commandEscapePrefix)) {
@@ -606,6 +676,9 @@ ReplMode DAP::DetectReplMode(lldb::SBFrame &frame, std::string &expression,
if (repl_mode != ReplMode::Auto)
return repl_mode;
+ if (expression.empty() && (repl_mode != ReplMode::Variable))
+ return ReplMode::Command;
+
// We cannot check if expression is a variable without a frame.
if (!frame)
return ReplMode::Command;
@@ -947,6 +1020,7 @@ llvm::Error DAP::Disconnect(bool terminateDebuggee) {
}
SendTerminatedEvent();
+ debugger.Clear();
TerminateLoop();
return ToError(error);
}
@@ -959,7 +1033,7 @@ bool DAP::IsCancelled(const protocol::Request &req) {
void DAP::ClearCancelRequest(const CancelArguments &args) {
std::lock_guard<std::mutex> guard(m_cancelled_requests_mutex);
if (args.requestId)
- m_cancelled_requests.erase(*args.requestId);
+ m_cancelled_requests.erase(args.requestId);
}
template <typename T>
@@ -990,7 +1064,7 @@ void DAP::Received(const protocol::Request &request) {
{
std::lock_guard<std::mutex> guard(m_cancelled_requests_mutex);
if (cancel_args->requestId)
- m_cancelled_requests.insert(*cancel_args->requestId);
+ m_cancelled_requests.insert(cancel_args->requestId);
}
// If a cancel is requested for the active request, make a best
@@ -1000,6 +1074,22 @@ void DAP::Received(const protocol::Request &request) {
DAP_LOG(log, "interrupting inflight request (command={0} seq={1})",
m_active_request->command, m_active_request->seq);
debugger.RequestInterrupt();
+ debugger.DispatchInputInterrupt();
+ debugger.GetCommandInterpreter().InterruptCommand();
+ }
+
+ // Making a best effort attempt at interrupting in-progress events.
+ if (!cancel_args->progressId.empty()) {
+ DAP_LOG(log, "cancel in progress event, interrupt debugger");
+ // FIXME: Which of these are actually required, they all have different
+ // behavior.
+ debugger.RequestInterrupt();
+ debugger.DispatchInputInterrupt();
+ debugger.GetCommandInterpreter().InterruptCommand();
+
+ std::lock_guard<std::mutex> guard(m_eval_mutex);
+ if (m_evaluate_context)
+ m_evaluate_context->Interrupt();
}
}
@@ -1055,6 +1145,40 @@ void DAP::TransportHandler() {
return;
}
+ Status status;
+ m_out_handle = m_loop.RegisterReadObject(
+ primary.read,
+ [this](auto &loop) {
+ std::array<char, OutputBufferSize> buf = {0};
+ size_t num_bytes = buf.size();
+ if (auto err = primary.read->Read(buf.data(), num_bytes).takeError()) {
+ DAP_LOG_ERROR(log, std::move(err), "read failed {0}");
+ return;
+ }
+
+ // EOF detected
+ if (num_bytes == 0) {
+ m_out_handle.reset();
+ return;
+ }
+
+ std::lock_guard<std::mutex> guard(m_eval_mutex);
+
+ if (m_evaluate_context &&
+ m_evaluate_context->HandleOutput({buf.data(), num_bytes}))
+ return;
+
+ SendOutput(OutputType::Console, {buf.data(), num_bytes});
+ },
+ status);
+
+ if (llvm::Error err = status.takeError()) {
+ DAP_LOG_ERROR(log, std::move(err), "registering pty failed: {0}");
+ std::lock_guard<std::mutex> guard(m_queue_mutex);
+ m_error_occurred = true;
+ return;
+ }
+
if (Status status = m_loop.Run(); status.Fail()) {
DAP_LOG_ERROR(log, status.takeError(), "MainLoop run failed: {0}");
std::lock_guard<std::mutex> guard(m_queue_mutex);
@@ -1063,6 +1187,17 @@ void DAP::TransportHandler() {
}
}
+void DAP::ActivateRepl() {
+ lldb::SBCommandInterpreterRunOptions options;
+ options.SetSpawnThread(true);
+ options.SetAutoHandleEvents(false);
+ options.SetEchoCommands(false);
+ options.SetEchoCommentCommands(false);
+ options.SetAllowRepeats(true);
+ options.SetAddToHistory(true);
+ debugger.RunCommandInterpreter(options);
+}
+
llvm::Error DAP::Loop() {
{
// Reset disconnect flag once we start the loop.
@@ -1074,8 +1209,8 @@ llvm::Error DAP::Loop() {
llvm::scope_exit cleanup([this]() {
// FIXME: Merge these into the MainLoop handler.
- out.Stop();
- err.Stop();
+ stdout_redirect.Stop();
+ stderr_redirect.Stop();
StopEventHandlers();
// Destroy the debugger when the session ends. This will trigger the
@@ -1246,6 +1381,7 @@ protocol::Capabilities DAP::GetCapabilities() {
protocol::eAdapterFeatureLogPoints,
protocol::eAdapterFeatureSteppingGranularity,
protocol::eAdapterFeatureValueFormattingOptions,
+ protocol::eAdapterFeatureSingleThreadExecutionRequests,
};
// Capabilities associated with specific requests.
@@ -1343,18 +1479,44 @@ llvm::Error DAP::InitializeDebugger() {
debugger = lldb::SBDebugger::Create(/*argument_name=*/false);
// Configure input/output/error file descriptors.
- debugger.SetInputFile(in);
+ debugger.SetInputFile(replica.read);
+ debugger.SetOutputFile(replica.write);
+ debugger.SetErrorFile(replica.write);
+
target = debugger.GetDummyTarget();
- llvm::Expected<int> out_fd = out.GetWriteFileDescriptor();
- if (!out_fd)
- return out_fd.takeError();
- debugger.SetOutputFile(lldb::SBFile(*out_fd, "w", false));
+ auto cmd = debugger.GetCommandInterpreter().AddMultiwordCommand(
+ "lldb-dap", "Commands for managing lldb-dap.");
+
+ if (clientFeatures.contains(eClientFeatureStartDebuggingRequest)) {
+ cmd.AddCommand(
+ "start-debugging", new StartDebuggingCommand(*this),
+ "Sends a startDebugging request from the debug adapter to the client "
+ "to start a child debug session of the same type as the caller.");
+ }
+
+ cmd.AddCommand(
+ "repl-mode", new ReplModeCommand(*this),
+ "Get or set the repl behavior of lldb-dap evaluation requests.");
+ cmd.AddCommand("send-event", new SendEventCommand(*this),
+ "Sends an DAP event to the client.");
+
+ StartEventThreads();
+
+ // Set the print callback to allow us to intercept command return objects from
+ // the repl to provide a more detailed view of the output.
+ debugger.GetCommandInterpreter().SetPrintCallback(
+ [](lldb::SBCommandReturnObject &result, void *baton) {
+ DAP *dap = static_cast<DAP *>(baton);
+ std::lock_guard<std::mutex> guard(dap->m_eval_mutex);
- llvm::Expected<int> err_fd = err.GetWriteFileDescriptor();
- if (!err_fd)
- return err_fd.takeError();
- debugger.SetErrorFile(lldb::SBFile(*err_fd, "w", false));
+ if (dap->m_evaluate_context &&
+ dap->m_evaluate_context->HandleReturnObject(result))
+ return lldb::eCommandReturnObjectPrintCallbackHandled;
+
+ return lldb::eCommandReturnObjectPrintCallbackSkipped;
+ },
+ this);
// The sourceInitFile option is not part of the DAP specification. It is an
// extension used by the test suite to prevent sourcing `.lldbinit` and
@@ -1374,23 +1536,6 @@ llvm::Error DAP::InitializeDebugger() {
if (llvm::Error err = RunPreInitCommands())
return err;
- auto cmd = debugger.GetCommandInterpreter().AddMultiwordCommand(
- "lldb-dap", "Commands for managing lldb-dap.");
-
- if (clientFeatures.contains(eClientFeatureStartDebuggingRequest)) {
- cmd.AddCommand(
- "start-debugging", new StartDebuggingCommand(*this),
- "Sends a startDebugging request from the debug adapter to the client "
- "to start a child debug session of the same type as the caller.");
- }
-
- cmd.AddCommand(
- "repl-mode", new ReplModeCommand(*this),
- "Get or set the repl behavior of lldb-dap evaluation requests.");
- cmd.AddCommand("send-event", new SendEventCommand(*this),
- "Sends an DAP event to the client.");
-
- StartEventThreads();
return llvm::Error::success();
}
diff --git a/lldb/tools/lldb-dap/DAP.h b/lldb/tools/lldb-dap/DAP.h
index 657105c8dd6b9..153f2826c8045 100644
--- a/lldb/tools/lldb-dap/DAP.h
+++ b/lldb/tools/lldb-dap/DAP.h
@@ -11,6 +11,7 @@
#include "DAPForward.h"
#include "DAPSessionManager.h"
+#include "EvaluateContext.h"
#include "ExceptionBreakpoint.h"
#include "FunctionBreakpoint.h"
#include "InstructionBreakpoint.h"
@@ -23,16 +24,15 @@
#include "Transport.h"
#include "Variables.h"
#include "lldb/API/SBBroadcaster.h"
-#include "lldb/API/SBCommandInterpreter.h"
#include "lldb/API/SBDebugger.h"
#include "lldb/API/SBError.h"
-#include "lldb/API/SBFile.h"
#include "lldb/API/SBFormat.h"
#include "lldb/API/SBFrame.h"
#include "lldb/API/SBMutex.h"
#include "lldb/API/SBTarget.h"
#include "lldb/API/SBThread.h"
#include "lldb/Host/MainLoop.h"
+#include "lldb/lldb-forward.h"
#include "lldb/lldb-types.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/DenseSet.h"
@@ -81,6 +81,20 @@ enum class ReplMode { Variable = 0, Command, Auto };
using DAPTransport = lldb_private::transport::JSONTransport<ProtocolDescriptor>;
+/// Wraps a set of file handles for reading and writing to a pair of handlers.
+/// Typically a pty on supported platforms or a pair of pipes as a fallback.
+struct IOWrapper {
+ IOWrapper() = default;
+ IOWrapper(lldb::FileSP read, lldb::FileSP write)
+ : read(read), write(write) {};
+
+ static llvm::Expected<std::pair<IOWrapper, IOWrapper>> CreatePseudoTerminal();
+ static llvm::Expected<std::pair<IOWrapper, IOWrapper>> CreatePipePair();
+
+ lldb::FileSP read;
+ lldb::FileSP write;
+};
+
struct DAP final : public DAPTransport::MessageHandler {
friend class DAPSessionManager;
@@ -89,9 +103,16 @@ struct DAP final : public DAPTransport::MessageHandler {
Log &log;
DAPTransport &transport;
- lldb::SBFile in;
- OutputRedirector out;
- OutputRedirector err;
+
+ /// pty handles used for communicating with the debugger input / output
+ /// IOHandler.
+ /// @{
+ IOWrapper primary;
+ IOWrapper replica;
+ /// @}
+
+ OutputRedirector stdout_redirect;
+ OutputRedirector stderr_redirect;
/// Configuration specified by the launch or attach commands.
protocol::Configuration configuration;
@@ -148,11 +169,7 @@ struct DAP final : public DAPTransport::MessageHandler {
lldb::SBFormat thread_format;
llvm::unique_function<void()> on_configuration_done;
- /// This is used to allow request_evaluate to handle empty expressions
- /// (ie the user pressed 'return' and expects the previous expression to
- /// repeat). If the previous expression was a command, it will be empty.
- /// Else it will contain the last valid variable expression.
- std::string last_valid_variable_expression;
+ void ActivateRepl();
/// The set of features supported by the connected client.
llvm::DenseSet<ClientFeature> clientFeatures;
@@ -422,6 +439,11 @@ struct DAP final : public DAPTransport::MessageHandler {
void StartEventThread();
void StartProgressEventThread();
+ void SetEvaluateContext(EvaluateContext *context) {
+ std::lock_guard<std::mutex> guard(m_eval_mutex);
+ m_evaluate_context = context;
+ }
+
/// DAP debugger initialization functions.
/// @{
@@ -492,6 +514,11 @@ struct DAP final : public DAPTransport::MessageHandler {
const llvm::StringRef m_client_name;
+ std::mutex m_eval_mutex;
+ /// An optional context that is used while evaluating an expressions.
+ EvaluateContext *m_evaluate_context = nullptr;
+ lldb_private::MainLoop::ReadHandleUP m_out_handle;
+
/// List of addresses mapped by sourceReference.
std::vector<lldb::addr_t> m_source_references;
std::mutex m_source_references_mutex;
diff --git a/lldb/tools/lldb-dap/EvaluateContext.cpp b/lldb/tools/lldb-dap/EvaluateContext.cpp
new file mode 100644
index 0000000000000..3570a08bc800a
--- /dev/null
+++ b/lldb/tools/lldb-dap/EvaluateContext.cpp
@@ -0,0 +1,127 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "EvaluateContext.h"
+#include "DAP.h"
+#include "DAPLog.h"
+#include "lldb/API/SBCommandInterpreter.h"
+#include "lldb/API/SBCommandReturnObject.h"
+#include "lldb/API/SBDebugger.h"
+#include "lldb/API/SBMutex.h"
+#include "lldb/API/SBProgress.h"
+#include "lldb/Host/File.h"
+#include "lldb/Utility/Status.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Error.h"
+#include <chrono>
+#include <memory>
+#include <string>
+
+using namespace llvm;
+using namespace lldb_dap;
+using lldb_private::Status;
+
+EvaluateContext::EvaluateContext(DAP &dap, StringRef expr)
+ : m_dap(dap), m_expr(expr),
+ m_wants_return_object(dap.debugger.GetCommandInterpreter().IsActive()) {}
+
+void EvaluateContext::Interrupt() {
+ DAP_LOG(m_dap.log, "EvaluateContext::Interrupt");
+ Done(/*immediate=*/true);
+}
+
+bool EvaluateContext::WantsRawInput() {
+ return !m_dap.debugger.GetCommandInterpreter().IsActive();
+}
+
+void EvaluateContext::Done(bool immediate) {
+ DAP_LOG(m_dap.log, "EvaluateContext::Done");
+ if (immediate)
+ m_loop.AddPendingCallback([](auto &loop) { loop.RequestTermination(); });
+ else
+ m_loop.AddCallback([](auto &loop) { loop.RequestTermination(); },
+ std::chrono::milliseconds(50));
+}
+
+bool EvaluateContext::HandleOutput(StringRef o) {
+ DAP_LOG(m_dap.log, "EvaluateContext::HandleOutput(o={0})", o);
+ // Skip the echo of the input
+ if (o.trim() == m_expr.trim() && !m_echo_detected)
+ m_echo_detected = true;
+ else
+ m_output += o;
+
+ if (m_wants_return_object) {
+ if (m_return_object_reported)
+ Done(/*immediate=*/m_output.ends_with(m_dap.debugger.GetPrompt()));
+ } else {
+ Done(/*immediate=*/m_output.ends_with(m_dap.debugger.GetPrompt()));
+ }
+
+ return true;
+}
+
+bool EvaluateContext::HandleReturnObject(lldb::SBCommandReturnObject &result) {
+ DAP_LOG(m_dap.log, "EvaluateContext::HandleReturnObject");
+ m_success = result.Succeeded();
+
+ m_return_object_reported = true;
+
+ if (result.GetStatus() == lldb::eReturnStatusSuccessFinishResult)
+ m_variables = result.GetValues(lldb::eDynamicDontRunTarget);
+ if (result.GetOutputSize())
+ m_output += StringRef{result.GetOutput(), result.GetOutputSize()};
+ if (result.GetErrorSize())
+ m_output += StringRef{result.GetError(), result.GetErrorSize()};
+
+ if (WantsRawInput())
+ Done(/*immediate=*/false);
+ else
+ Done(/*immediate=*/!m_output.empty());
+
+ return true;
+}
+
+Expected<std::pair<std::string, lldb::SBValueList>>
+EvaluateContext::Run(DAP &dap, StringRef expr) {
+ DAP_LOG(dap.log, "EvaluateContext::Run");
+ EvaluateContext context(dap, expr);
+ dap.SetEvaluateContext(&context);
+
+ lldb::SBProgress progress(/*title=*/"Evaluating expression",
+ expr.str().data(), dap.debugger);
+
+ lldb::SBMutex api_mutex = dap.GetAPIMutex();
+
+ // While in raw input mode, don't wait for output in case the IOHandler never
+ // writes.
+ if (context.WantsRawInput() && !dap.primary.write->GetIsInteractive())
+ context.Done(false);
+
+ // Unlock to allow the background thread to handle reading/processing.
+ api_mutex.unlock();
+
+ size_t size = expr.size();
+ if (Error err = dap.primary.write->Write(expr.data(), size).takeError())
+ return err;
+
+ Status status = context.m_loop.Run();
+
+ api_mutex.lock();
+ dap.SetEvaluateContext(nullptr);
+
+ if (auto error = status.takeError())
+ return error;
+
+ if (!context.m_success)
+ return createStringError(context.m_output.empty()
+ ? "Error evaluating expression"
+ : context.m_output.str());
+
+ return std::make_pair(context.m_output.str().str(), context.m_variables);
+}
diff --git a/lldb/tools/lldb-dap/EvaluateContext.h b/lldb/tools/lldb-dap/EvaluateContext.h
new file mode 100644
index 0000000000000..c4679c696ca4f
--- /dev/null
+++ b/lldb/tools/lldb-dap/EvaluateContext.h
@@ -0,0 +1,48 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_TOOLS_LLDB_DAP_EVALUATE_CONTEXT_H
+#define LLDB_TOOLS_LLDB_DAP_EVALUATE_CONTEXT_H
+
+#include "DAPForward.h"
+#include "lldb/API/SBValueList.h"
+#include "lldb/Host/MainLoop.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Error.h"
+
+namespace lldb_dap {
+class EvaluateContext {
+public:
+ bool HandleOutput(llvm::StringRef);
+ bool HandleReturnObject(lldb::SBCommandReturnObject &);
+ static llvm::Expected<std::pair<std::string, lldb::SBValueList>>
+ Run(DAP &dap, llvm::StringRef expr);
+
+ void Interrupt();
+
+private:
+ bool WantsRawInput();
+ void Done(bool immediate);
+ EvaluateContext(DAP &dap, llvm::StringRef expr);
+
+ DAP &m_dap;
+ llvm::StringRef m_expr;
+ llvm::SmallString<32> m_output;
+ lldb::SBValueList m_variables;
+ bool m_echo_detected = false;
+ bool m_success = true;
+ bool m_return_object_reported = false;
+ // If the CommandInterpreter is not active, then we're in raw input mode and
+ // will not receive a result from the print object helper.
+ bool m_wants_return_object = false;
+ lldb_private::MainLoop m_loop;
+};
+
+} // namespace lldb_dap
+
+#endif
diff --git a/lldb/tools/lldb-dap/EventHelper.cpp b/lldb/tools/lldb-dap/EventHelper.cpp
index dbf4823408b11..6831c2202c50a 100644
--- a/lldb/tools/lldb-dap/EventHelper.cpp
+++ b/lldb/tools/lldb-dap/EventHelper.cpp
@@ -27,6 +27,7 @@
#include "lldb/API/SBPlatform.h"
#include "lldb/API/SBStream.h"
#include "lldb/API/SBThread.h"
+#include "lldb/API/SBThreadCollection.h"
#include "lldb/lldb-defines.h"
#include "lldb/lldb-types.h"
#include "llvm/Support/Error.h"
@@ -181,9 +182,10 @@ void SendProcessEvent(DAP &dap, LaunchMethod launch_method) {
static void SendStoppedEvent(DAP &dap, lldb::SBThread &thread, bool on_entry,
bool all_threads_stopped, bool preserve_focus) {
protocol::StoppedEventBody body;
+ body.reason = protocol::eStoppedReasonPause;
if (on_entry) {
body.reason = protocol::eStoppedReasonEntry;
- } else {
+ } else if (thread.IsValid()) {
switch (thread.GetStopReason()) {
case lldb::eStopReasonTrace:
case lldb::eStopReasonPlanComplete:
@@ -235,13 +237,14 @@ static void SendStoppedEvent(DAP &dap, lldb::SBThread &thread, bool on_entry,
case lldb::eStopReasonThreadExiting:
case lldb::eStopReasonInvalid:
case lldb::eStopReasonNone:
- return;
+ break;
}
+
+ lldb::SBStream description;
+ thread.GetStopDescription(description);
+ body.description = {description.GetData(), description.GetSize()};
}
lldb::tid_t tid = thread.GetThreadID();
- lldb::SBStream description;
- thread.GetStopDescription(description);
- body.description = {description.GetData(), description.GetSize()};
body.threadId = tid;
body.allThreadsStopped = all_threads_stopped;
body.preserveFocusHint = preserve_focus;
@@ -266,7 +269,8 @@ llvm::Error SendThreadStoppedEvent(DAP &dap, bool on_entry) {
llvm::DenseSet<lldb::tid_t> old_thread_ids;
old_thread_ids.swap(dap.thread_ids);
- lldb::tid_t focused_tid = LLDB_INVALID_THREAD_ID;
+ lldb::SBThread focused_thread;
+ std::vector<lldb::SBThread> stopped_threads;
for (auto thread : process) {
// Collect all known thread ids for sending thread events.
dap.thread_ids.insert(thread.GetThreadID());
@@ -274,22 +278,28 @@ llvm::Error SendThreadStoppedEvent(DAP &dap, bool on_entry) {
if (!ThreadHasStopReason(thread))
continue;
- // When we stop, report allThreadsStopped for the *first* stopped thread to
- // ensure the list of stopped threads is up to date.
- bool first_stop = focused_tid == LLDB_INVALID_THREAD_ID;
- SendStoppedEvent(dap, thread, on_entry, /*all_threads_stopped=*/first_stop,
- /*preserve_focus=*/!first_stop);
-
- // Default focus to the first stopped thread.
- if (focused_tid == LLDB_INVALID_THREAD_ID)
- focused_tid = thread.GetThreadID();
+ // Focus on the first stopped thread if the selected thread is not stopped.
+ if (!focused_thread.IsValid())
+ focused_thread = thread;
+ else
+ stopped_threads.push_back(thread);
}
- if (focused_tid == LLDB_INVALID_THREAD_ID)
+ if (!focused_thread)
+ focused_thread = process.GetSelectedThread();
+
+ if (!focused_thread)
return make_error<DAPError>("no stopped threads");
+ // Notify the focused thread first and tell the client all threads have
+ // stopped.
+ SendStoppedEvent(dap, focused_thread, on_entry, true, false);
+ // Send stopped events for remaining stopped threads.
+ for (auto thread : stopped_threads)
+ SendStoppedEvent(dap, thread, on_entry, false, true);
+
// Update focused thread.
- dap.focus_tid = focused_tid;
+ dap.focus_tid = focused_thread.GetThreadID();
for (const auto &tid : old_thread_ids)
if (!dap.thread_ids.contains(tid))
@@ -314,15 +324,11 @@ void SendStdOutStdErr(DAP &dap, lldb::SBProcess &process) {
// Send a "continued" event to indicate the process is in the running state.
void SendContinuedEvent(DAP &dap) {
lldb::SBProcess process = dap.target.GetProcess();
- if (!process.IsValid()) {
- return;
- }
-
// If the focus thread is not set then we haven't reported any thread status
// to the client, so nothing to report.
- if (!dap.configuration_done || dap.focus_tid == LLDB_INVALID_THREAD_ID) {
+ if (!process.IsValid() || !dap.configuration_done ||
+ dap.focus_tid == LLDB_INVALID_THREAD_ID)
return;
- }
llvm::json::Object event(CreateEventObject("continued"));
llvm::json::Object body;
diff --git a/lldb/tools/lldb-dap/Handler/CompletionsRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/CompletionsRequestHandler.cpp
index 423043e6c26dd..4988ccb616b48 100644
--- a/lldb/tools/lldb-dap/Handler/CompletionsRequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/CompletionsRequestHandler.cpp
@@ -11,6 +11,7 @@
#include "Protocol/ProtocolRequests.h"
#include "Protocol/ProtocolTypes.h"
#include "RequestHandler.h"
+#include "lldb/API/SBCommandInterpreter.h"
#include "lldb/API/SBStringList.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/Support/ConvertUTF.h"
diff --git a/lldb/tools/lldb-dap/Handler/ConfigurationDoneRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/ConfigurationDoneRequestHandler.cpp
index 4b84a047cd148..8e589ed68c65c 100644
--- a/lldb/tools/lldb-dap/Handler/ConfigurationDoneRequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/ConfigurationDoneRequestHandler.cpp
@@ -7,6 +7,7 @@
//===----------------------------------------------------------------------===//
#include "DAP.h"
+#include "DAPLog.h"
#include "EventHelper.h"
#include "LLDBUtils.h"
#include "Protocol/ProtocolRequests.h"
@@ -74,6 +75,8 @@ void ConfigurationDoneRequestHandler::PostRun() const {
dap.on_configuration_done();
// Clear the callback to ensure any captured resources are released.
dap.on_configuration_done = nullptr;
+
+ dap.ActivateRepl();
}
} // namespace lldb_dap
diff --git a/lldb/tools/lldb-dap/Handler/EvaluateRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/EvaluateRequestHandler.cpp
index 0eb5c57732350..5f2ac55f6af0a 100644
--- a/lldb/tools/lldb-dap/Handler/EvaluateRequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/EvaluateRequestHandler.cpp
@@ -14,9 +14,11 @@
#include "Protocol/ProtocolRequests.h"
#include "Protocol/ProtocolTypes.h"
#include "RequestHandler.h"
+#include "lldb/API/SBValue.h"
#include "lldb/lldb-enumerations.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Error.h"
+#include <cstddef>
using namespace llvm;
using namespace lldb_dap;
@@ -24,25 +26,6 @@ using namespace lldb_dap::protocol;
namespace lldb_dap {
-static bool RunExpressionAsLLDBCommand(DAP &dap, lldb::SBFrame &frame,
- std::string &expression,
- EvaluateContext context) {
- if (context != eEvaluateContextRepl && context != eEvaluateContextUnknown)
- return false;
-
- // Since we don't know this context do not try to repeat the last command;
- if (context == eEvaluateContextUnknown && expression.empty())
- return false;
-
- const bool repeat_last_command =
- expression.empty() && dap.last_valid_variable_expression.empty();
- if (repeat_last_command)
- return true;
-
- const ReplMode repl_mode = dap.DetectReplMode(frame, expression, false);
- return repl_mode == ReplMode::Command;
-}
-
static lldb::SBValue EvaluateVariableExpression(lldb::SBTarget &target,
lldb::SBFrame &frame,
const std::string &expression,
@@ -73,65 +56,60 @@ static lldb::SBValue EvaluateVariableExpression(lldb::SBTarget &target,
/// The expression has access to any variables and arguments that are in scope.
Expected<EvaluateResponseBody>
EvaluateRequestHandler::Run(const EvaluateArguments &arguments) const {
-
EvaluateResponseBody body;
lldb::SBFrame frame = dap.GetLLDBFrame(arguments.frameId);
- std::string expression = llvm::StringRef(arguments.expression).trim().str();
- const EvaluateContext evaluate_context = arguments.context;
- const bool is_repl_context = evaluate_context == eEvaluateContextRepl;
-
- if (RunExpressionAsLLDBCommand(dap, frame, expression, evaluate_context)) {
- // Since the current expression is not for a variable, clear the
- // last_valid_variable_expression field.
- dap.last_valid_variable_expression.clear();
+ std::string expression = arguments.expression;
+ const bool is_repl_context = arguments.context == eEvaluateContextRepl;
+ const bool run_as_expression = arguments.context != eEvaluateContextHover;
+ lldb::SBValue value;
+
+ if (arguments.context == protocol::eEvaluateContextRepl &&
+ dap.DetectReplMode(frame, expression, false) == ReplMode::Command) {
+ const lldb::StateType process_state = dap.target.GetProcess().GetState();
+ if (!lldb::SBDebugger::StateIsStoppedState(process_state))
+ return llvm::make_error<DAPError>(
+ "Cannot evaluate expressions while the process is running. Pause "
+ "the process and try again.",
+ /**error_code=*/llvm::inconvertibleErrorCode(),
+ /**show_user=*/false);
+
// If we're evaluating a command relative to the current frame, set the
// focus_tid to the current frame for any thread related events.
- if (frame.IsValid()) {
+ if (frame.IsValid())
dap.focus_tid = frame.GetThread().GetThreadID();
- }
- bool required_command_failed = false;
- body.result = RunLLDBCommands(
- dap.debugger, llvm::StringRef(), {expression}, required_command_failed,
- /*parse_command_directives=*/false, /*echo_commands=*/false);
- return body;
- }
+ Expected<std::pair<std::string, lldb::SBValueList>> result =
+ EvaluateContext::Run(dap, expression + "\n");
+ if (!result)
+ return result.takeError();
- const lldb::StateType process_state = dap.target.GetProcess().GetState();
- if (!lldb::SBDebugger::StateIsStoppedState(process_state))
- return llvm::make_error<DAPError>(
- "Cannot evaluate expressions while the process is running. Pause "
- "the process and try again.",
- /**error_code=*/llvm::inconvertibleErrorCode(),
- /**show_user=*/false);
+ lldb::SBValueList values;
+ std::tie(body.result, values) = *result;
- // If the user expression is empty, evaluate the last valid variable
- // expression.
- if (expression.empty() && is_repl_context)
- expression = dap.last_valid_variable_expression;
+ if (values.GetSize() == 1) {
+ value = values.GetValueAtIndex(0);
+ body.type = value.GetDisplayTypeName();
+ } else if (values.GetSize()) {
+ body.variablesReference = dap.variables.Insert(result->second);
+ }
+ } else {
+ value = EvaluateVariableExpression(dap.target, frame, expression,
+ run_as_expression);
- const bool run_as_expression = evaluate_context != eEvaluateContextHover;
- lldb::SBValue value = EvaluateVariableExpression(
- dap.target, frame, expression, run_as_expression);
+ if (value.GetError().Fail())
+ return ToError(value.GetError(), /*show_user=*/false);
- if (value.GetError().Fail())
- return ToError(value.GetError(), /*show_user=*/false);
+ if (is_repl_context)
+ value = value.Persist();
- if (is_repl_context) {
- // save the new variable expression
- dap.last_valid_variable_expression = std::move(expression);
+ const bool hex = arguments.format ? arguments.format->hex : false;
+ VariableDescription desc(
+ value, dap.configuration.enableAutoVariableSummaries, hex);
- // Freeze dry the value in case users expand it later in the debug console
- value = value.Persist();
+ body.result = desc.GetResult(arguments.context);
+ body.type = desc.display_type_name;
}
- const bool hex = arguments.format ? arguments.format->hex : false;
- VariableDescription desc(value, dap.configuration.enableAutoVariableSummaries,
- hex);
-
- body.result = desc.GetResult(evaluate_context);
- body.type = desc.display_type_name;
-
if (value.MightHaveChildren() || ValuePointsToCode(value))
body.variablesReference =
dap.variables.InsertVariable(value, /*is_permanent=*/is_repl_context);
diff --git a/lldb/tools/lldb-dap/Handler/VariablesRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/VariablesRequestHandler.cpp
index ee7cb525d9c29..665bd62f4bd97 100644
--- a/lldb/tools/lldb-dap/Handler/VariablesRequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/VariablesRequestHandler.cpp
@@ -80,7 +80,7 @@ VariablesRequestHandler::Run(const VariablesArguments &arguments) const {
var.name = "<error>";
var.type = "const char *";
var.value = var_err;
- variables.emplace_back(var);
+ variables.emplace_back(std::move(var));
}
}
const int64_t end_idx = start_idx + ((count == 0) ? num_children : count);
@@ -136,35 +136,39 @@ VariablesRequestHandler::Run(const VariablesArguments &arguments) const {
// We are expanding a variable that has children, so we will return its
// children.
lldb::SBValue variable = dap.variables.GetVariable(var_ref);
- if (variable.IsValid()) {
- const bool is_permanent =
- dap.variables.IsPermanentVariableReference(var_ref);
- auto addChild = [&](lldb::SBValue child,
- std::optional<llvm::StringRef> custom_name = {}) {
- if (!child.IsValid())
- return;
- const int64_t child_var_ref =
- dap.variables.InsertVariable(child, is_permanent);
- variables.emplace_back(
- CreateVariable(child, child_var_ref, hex,
- dap.configuration.enableAutoVariableSummaries,
- dap.configuration.enableSyntheticChildDebugging,
- /*is_name_duplicated=*/false, custom_name));
- };
- const int64_t num_children = variable.GetNumChildren();
- const int64_t end_idx = start + ((count == 0) ? num_children : count);
- int64_t i = start;
- for (; i < end_idx && i < num_children; ++i)
- addChild(variable.GetChildAtIndex(i));
-
- // If we haven't filled the count quota from the request, we insert a new
- // "[raw]" child that can be used to inspect the raw version of a
- // synthetic member. That eliminates the need for the user to go to the
- // debug console and type `frame var <variable> to get these values.
- if (dap.configuration.enableSyntheticChildDebugging &&
- variable.IsSynthetic() && i == num_children)
- addChild(variable.GetNonSyntheticValue(), "[raw]");
+ if (!variable.IsValid()) {
+ if (!variable.IsInScope())
+ return make_error<DAPError>("value is no longer in scope.");
+ return make_error<DAPError>("invalid variable reference");
}
+
+ const bool is_permanent =
+ dap.variables.IsPermanentVariableReference(var_ref);
+ auto addChild = [&](lldb::SBValue child,
+ std::optional<llvm::StringRef> custom_name = {}) {
+ if (!child.IsValid())
+ return;
+ const int64_t child_var_ref =
+ dap.variables.InsertVariable(child, is_permanent);
+ variables.emplace_back(
+ CreateVariable(child, child_var_ref, hex,
+ dap.configuration.enableAutoVariableSummaries,
+ dap.configuration.enableSyntheticChildDebugging,
+ /*is_name_duplicated=*/false, custom_name));
+ };
+ const int64_t num_children = variable.GetNumChildren();
+ const int64_t end_idx = start + ((count == 0) ? num_children : count);
+ int64_t i = start;
+ for (; i < end_idx && i < num_children; ++i)
+ addChild(variable.GetChildAtIndex(i));
+
+ // If we haven't filled the count quota from the request, we insert a new
+ // "[raw]" child that can be used to inspect the raw version of a
+ // synthetic member. That eliminates the need for the user to go to the
+ // debug console and type `frame var <variable> to get these values.
+ if (dap.configuration.enableSyntheticChildDebugging &&
+ variable.IsSynthetic() && i == num_children)
+ addChild(variable.GetNonSyntheticValue(), "[raw]");
}
return VariablesResponseBody{std::move(variables)};
diff --git a/lldb/tools/lldb-dap/ProgressEvent.cpp b/lldb/tools/lldb-dap/ProgressEvent.cpp
index 5ef03ccb24bb9..33964922e1ad7 100644
--- a/lldb/tools/lldb-dap/ProgressEvent.cpp
+++ b/lldb/tools/lldb-dap/ProgressEvent.cpp
@@ -114,7 +114,7 @@ json::Value ProgressEvent::ToJSON() const {
if (m_event_type == progressStart) {
EmplaceSafeString(body, "title", m_message);
- body.try_emplace("cancellable", false);
+ body.try_emplace("cancellable", true);
}
if (m_event_type == progressUpdate)
diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp b/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp
index a72802a33fc9c..7683b8c26cbe9 100644
--- a/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp
+++ b/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp
@@ -146,8 +146,8 @@ namespace lldb_dap::protocol {
bool fromJSON(const json::Value &Params, CancelArguments &CA, json::Path P) {
json::ObjectMapper O(Params, P);
- return O && O.map("requestId", CA.requestId) &&
- O.map("progressId", CA.progressId);
+ return O && O.mapOptional("requestId", CA.requestId) &&
+ O.mapOptional("progressId", CA.progressId);
}
bool fromJSON(const json::Value &Params, DisconnectArguments &DA,
diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolRequests.h b/lldb/tools/lldb-dap/Protocol/ProtocolRequests.h
index 28c9f48200e0c..5e50322873360 100644
--- a/lldb/tools/lldb-dap/Protocol/ProtocolRequests.h
+++ b/lldb/tools/lldb-dap/Protocol/ProtocolRequests.h
@@ -42,13 +42,13 @@ struct CancelArguments {
/// is cancelled.
///
/// Both a `requestId` and a `progressId` can be specified in one request.
- std::optional<int64_t> requestId;
+ uint64_t requestId = 0;
/// The ID (attribute `progressId`) of the progress to cancel. If missing no
/// progress is cancelled.
///
/// Both a `requestId` and a `progressId` can be specified in one request.
- std::optional<int64_t> progressId;
+ std::string progressId;
};
bool fromJSON(const llvm::json::Value &, CancelArguments &, llvm::json::Path);
diff --git a/lldb/tools/lldb-dap/Variables.cpp b/lldb/tools/lldb-dap/Variables.cpp
index 6d49113243799..e030100b5c3ee 100644
--- a/lldb/tools/lldb-dap/Variables.cpp
+++ b/lldb/tools/lldb-dap/Variables.cpp
@@ -113,6 +113,27 @@ int64_t Variables::InsertVariable(lldb::SBValue variable, bool is_permanent) {
return var_ref;
}
+int64_t Variables::Insert(lldb::SBValueList values) {
+ if (!values.IsValid() || values.GetSize() == 0)
+ return LLDB_DAP_INVALID_VAR_REF;
+
+ if (values.GetSize() == 1) {
+ lldb::SBValue value = values.GetValueAtIndex(0);
+ if (value.MightHaveChildren())
+ return InsertVariable(value, /*is_permanent=*/true);
+ return 0;
+ }
+
+ int64_t var_ref = GetNewVariableReference(true);
+ // Copy the values as persisted results.
+ lldb::SBValueList persisted_values;
+ for (uint32_t i = 0; i < values.GetSize(); i++) {
+ persisted_values.Append(values.GetValueAtIndex(i).Persist());
+ }
+ m_referenced_value_lists.insert({var_ref, persisted_values});
+ return var_ref;
+}
+
lldb::SBValue Variables::FindVariable(uint64_t variablesReference,
llvm::StringRef name) {
lldb::SBValue variable;
diff --git a/lldb/tools/lldb-dap/Variables.h b/lldb/tools/lldb-dap/Variables.h
index 9814cabc7036c..2ceb86f48c3cf 100644
--- a/lldb/tools/lldb-dap/Variables.h
+++ b/lldb/tools/lldb-dap/Variables.h
@@ -90,6 +90,7 @@ struct Variables {
/// Insert a new \p variable.
/// \return variableReference assigned to this expandable variable.
int64_t InsertVariable(lldb::SBValue variable, bool is_permanent);
+ int64_t Insert(lldb::SBValueList values);
std::optional<ScopeData> GetTopLevelScope(int64_t variablesReference);
@@ -123,6 +124,9 @@ struct Variables {
/// These are the variables evaluated from debug console REPL.
llvm::DenseMap<int64_t, lldb::SBValue> m_referencedpermanent_variables;
+ /// ValueLists for evaluated commands.
+ llvm::DenseMap<int64_t, lldb::SBValueList> m_referenced_value_lists;
+
/// Key = dap_frame_id (encodes both thread index ID and frame ID)
/// Value = scopes for the frame (locals, globals, registers)
std::map<uint64_t, FrameScopes> m_frames;
diff --git a/lldb/unittests/DAP/Handler/DisconnectTest.cpp b/lldb/unittests/DAP/Handler/DisconnectTest.cpp
index 212c5698feea8..05e71534ae455 100644
--- a/lldb/unittests/DAP/Handler/DisconnectTest.cpp
+++ b/lldb/unittests/DAP/Handler/DisconnectTest.cpp
@@ -44,17 +44,18 @@ TEST_F(DisconnectRequestHandlerTest, DisconnectTriggersTerminateCommands) {
LoadCore();
- DisconnectRequestHandler handler(*dap);
-
dap->configuration.terminateCommands = {"?script print(1)",
"script print(2)"};
EXPECT_EQ(dap->target.GetProcess().GetState(), lldb::eStateStopped);
+
+ DisconnectRequestHandler handler(*dap);
ASSERT_THAT_ERROR(handler.Run(std::nullopt), Succeeded());
- EXPECT_CALL(client, Received(Output("1\n")));
- EXPECT_CALL(client, Received(Output("2\n"))).Times(2);
- EXPECT_CALL(client, Received(Output("(lldb) script print(2)\n")));
+
EXPECT_CALL(client, Received(Output("Running terminateCommands:\n")));
+ EXPECT_CALL(client, Received(Output("(lldb) script print(2)\n")));
+ EXPECT_CALL(client, Received(Output("2\n")));
EXPECT_CALL(client, Received(IsEvent("terminated", _)));
+
Run();
}
#endif
diff --git a/lldb/unittests/DAP/TestBase.cpp b/lldb/unittests/DAP/TestBase.cpp
index a9231085637c9..af477d3c37538 100644
--- a/lldb/unittests/DAP/TestBase.cpp
+++ b/lldb/unittests/DAP/TestBase.cpp
@@ -20,6 +20,7 @@
#include "llvm/Support/Error.h"
#include "llvm/Testing/Support/Error.h"
#include "gtest/gtest.h"
+#include <chrono>
#include <cstdio>
#include <memory>
@@ -37,7 +38,7 @@ using lldb_private::Pipe;
void TransportBase::SetUp() {
std::tie(to_client, to_server) = TestDAPTransport::createPair();
- log = std::make_unique<Log>(llvm::outs(), log_mutex);
+ log = std::make_unique<Log>(llvm::errs(), log_mutex);
dap = std::make_unique<DAP>(
/*log=*/*log,
/*default_repl_mode=*/ReplMode::Auto,
@@ -56,13 +57,17 @@ void TransportBase::SetUp() {
}
void TransportBase::Run() {
- bool addition_succeeded = loop.AddPendingCallback(
- [](lldb_private::MainLoopBase &loop) { loop.RequestTermination(); });
+ bool addition_succeeded = loop.AddCallback(
+ [](lldb_private::MainLoopBase &loop) {
+ llvm::errs() << "Terminating\n";
+ loop.RequestTermination();
+ },
+ std::chrono::seconds(1));
EXPECT_TRUE(addition_succeeded);
EXPECT_THAT_ERROR(loop.Run().takeError(), llvm::Succeeded());
}
-void DAPTestBase::SetUp() { TransportBase::SetUp(); }
+// void DAPTestBase::SetUp() { TransportBase::SetUp(); }
void DAPTestBase::TearDown() {
if (core)
@@ -107,13 +112,9 @@ void DAPTestBase::CreateDebugger() {
ASSERT_THAT_ERROR(dap->ConfigureIO(dev_null_stream, dev_null_stream),
Succeeded());
- dap->debugger.SetInputFile(dap->in);
- auto out_fd = dap->out.GetWriteFileDescriptor();
- ASSERT_THAT_EXPECTED(out_fd, Succeeded());
- dap->debugger.SetOutputFile(lldb::SBFile(*out_fd, "w", false));
- auto err_fd = dap->out.GetWriteFileDescriptor();
- ASSERT_THAT_EXPECTED(err_fd, Succeeded());
- dap->debugger.SetErrorFile(lldb::SBFile(*err_fd, "w", false));
+ dap->no_lldbinit = true;
+ ASSERT_THAT_ERROR(dap->InitializeDebugger(), Succeeded());
+ ASSERT_THAT_ERROR(dap->ActivateRepl(), Succeeded());
}
void DAPTestBase::LoadCore() {
diff --git a/lldb/unittests/DAP/TestBase.h b/lldb/unittests/DAP/TestBase.h
index f1c7e6b989729..d8166d04feed2 100644
--- a/lldb/unittests/DAP/TestBase.h
+++ b/lldb/unittests/DAP/TestBase.h
@@ -110,7 +110,6 @@ class DAPTestBase : public TransportBase {
static void SetUpTestSuite();
static void TeatUpTestSuite();
- void SetUp() override;
void TearDown() override;
bool GetDebuggerSupportsTarget(llvm::StringRef platform);
>From 183789c85f77e1adde53e56918c372048ccceddc Mon Sep 17 00:00:00 2001
From: John Harrison <harjohn at google.com>
Date: Tue, 10 Feb 2026 15:36:42 -0800
Subject: [PATCH 2/5] Remove testing code.
---
.../Python/lldbsuite/test/tools/lldb-dap/dap_server.py | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
index f3efc7b23214b..acd9b09558eef 100644
--- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
+++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
@@ -486,10 +486,9 @@ def _process_recv_packets(self) -> None:
with self._recv_condition:
for packet in self._recv_packets:
if packet and ("seq" not in packet or packet["seq"] == 0):
- pass
- # raise ValueError(
- # f"received a malformed packet, expected 'seq != 0' for {packet!r}"
- # )
+ raise ValueError(
+ f"received a malformed packet, expected 'seq != 0' for {packet!r}"
+ )
if packet:
self._log.messages.append((Log.Dir.RECV, packet))
# Handle events that may modify any stateful properties of
>From 8fb124da84c1aeaa9425023150a88dc79d2ae5d1 Mon Sep 17 00:00:00 2001
From: John Harrison <harjohn at google.com>
Date: Wed, 11 Feb 2026 15:16:24 -0800
Subject: [PATCH 3/5] Correcting the stopped-events tests.
---
.../packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
index 3eafd6d3c0f3b..aeaf5dad3975b 100644
--- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
+++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
@@ -540,8 +540,6 @@ def _handle_event(self, packet: Event) -> None:
# reasons since the 'threads' command doesn't return
# that information.
self._process_stopped()
- if "allThreadsStopped" in body and body["allThreadsStopped"]:
- self.thread_stop_reasons = {}
if "threadId" in body:
tid = body["threadId"]
self.thread_stop_reasons[tid] = body
>From 0c06bef8480dd464e397f4cf2aa8cc7677439d57 Mon Sep 17 00:00:00 2001
From: John Harrison <harjohn at google.com>
Date: Wed, 11 Feb 2026 16:22:49 -0800
Subject: [PATCH 4/5] Fixing DAPTests.
---
lldb/tools/lldb-dap/DAP.cpp | 24 +++++++++++++-----------
lldb/unittests/DAP/TestBase.cpp | 14 ++------------
2 files changed, 15 insertions(+), 23 deletions(-)
diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp
index 4de0200dabe93..85bb295ff137c 100644
--- a/lldb/tools/lldb-dap/DAP.cpp
+++ b/lldb/tools/lldb-dap/DAP.cpp
@@ -297,17 +297,19 @@ llvm::Error DAP::ConfigureIO(std::FILE *overrideOut, std::FILE *overrideErr) {
std::tie(primary, replica) = *io;
- if (auto Error = stdout_redirect.RedirectTo(
- overrideOut, [this](llvm::StringRef output) {
- SendOutput(OutputType::Console, output);
- }))
- return Error;
-
- if (auto Error = stderr_redirect.RedirectTo(
- overrideErr, [this](llvm::StringRef output) {
- SendOutput(OutputType::Console, output);
- }))
- return Error;
+ if (overrideOut)
+ if (auto Error = stdout_redirect.RedirectTo(
+ overrideOut, [this](llvm::StringRef output) {
+ SendOutput(OutputType::Console, output);
+ }))
+ return Error;
+
+ if (overrideErr)
+ if (auto Error = stderr_redirect.RedirectTo(
+ overrideErr, [this](llvm::StringRef output) {
+ SendOutput(OutputType::Console, output);
+ }))
+ return Error;
return llvm::Error::success();
}
diff --git a/lldb/unittests/DAP/TestBase.cpp b/lldb/unittests/DAP/TestBase.cpp
index c0d6ae7e70aac..61986b74a20b6 100644
--- a/lldb/unittests/DAP/TestBase.cpp
+++ b/lldb/unittests/DAP/TestBase.cpp
@@ -29,9 +29,6 @@ using namespace lldb;
using namespace lldb_dap;
using namespace lldb_dap::protocol;
using namespace lldb_dap_tests;
-using lldb_private::File;
-using lldb_private::FileSpec;
-using lldb_private::FileSystem;
using lldb_private::MainLoop;
using lldb_private::Pipe;
@@ -98,18 +95,11 @@ void DAPTestBase::CreateDebugger() {
ASSERT_TRUE(dap->debugger);
dap->target = dap->debugger.GetDummyTarget();
- Expected<lldb::FileUP> dev_null = FileSystem::Instance().Open(
- FileSpec(FileSystem::DEV_NULL), File::eOpenOptionReadWrite);
- ASSERT_THAT_EXPECTED(dev_null, Succeeded());
- lldb::FileSP dev_null_sp = std::move(*dev_null);
-
- std::FILE *dev_null_stream = dev_null_sp->GetStream();
- ASSERT_THAT_ERROR(dap->ConfigureIO(dev_null_stream, dev_null_stream),
- Succeeded());
+ ASSERT_THAT_ERROR(dap->ConfigureIO(), Succeeded());
dap->no_lldbinit = true;
ASSERT_THAT_ERROR(dap->InitializeDebugger(), Succeeded());
- ASSERT_THAT_ERROR(dap->ActivateRepl(), Succeeded());
+ dap->ActivateRepl();
}
void DAPTestBase::LoadCore() {
>From 8410caf854366bb638bc6810080bd626da7beaae Mon Sep 17 00:00:00 2001
From: John Harrison <ash at greaterthaninfinity.com>
Date: Thu, 12 Feb 2026 09:58:41 -0800
Subject: [PATCH 5/5] Apply suggestions from code review
Co-authored-by: Ebuka Ezike <yerimyah1 at gmail.com>
---
lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py b/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py
index 8429941ad574f..4b3e83e3bb57a 100644
--- a/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py
+++ b/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py
@@ -36,7 +36,7 @@ def test_pending_request(self):
Tests cancelling a pending request.
"""
program = self.getBuildArtifact("a.out")
- busy_loop = os.path.join(os.path.dirname(__file__), "busy_loop.py")
+ busy_loop = self.getSourcePath("busy_loop.py")
self.build_and_launch(
program,
initCommands=[f"command script import {busy_loop}"],
More information about the lldb-commits
mailing list