[lldb-dap] Change the launch sequence (#138219) (reland)

This PR changes how we treat the launch sequence in lldb-dap.

 - Send the initialized event after we finish handling the initialize
   request, rather than after we finish attaching or launching.
 - Delay handling the launch and attach request until we have handled
   the configurationDone request. The latter is now largely a NO-OP and
   only exists to signal lldb-dap that it can handle the launch and
   attach requests.
 - Delay handling the initial threads requests until we have handled
   the launch or attach request.
 - Make all attaching and launching synchronous, including when we have
   attach or launch commands. This removes the need to synchronize
   between the request and event thread.

Background:
https://discourse.llvm.org/t/reliability-of-the-lldb-dap-tests/86125
This commit is contained in:
Jonas Devlieghere 2025-05-06 15:58:44 -07:00
parent 7313c3b1f1
commit aeeb9a3c09
No known key found for this signature in database
GPG Key ID: 49CC0BD90FDEED4D
31 changed files with 330 additions and 242 deletions

View File

@ -132,7 +132,6 @@ class DebugCommunication(object):
self.exit_status = None
self.initialize_body = None
self.thread_stop_reasons = {}
self.breakpoint_events = []
self.progress_events = []
self.reverse_requests = []
self.module_events = []
@ -244,13 +243,6 @@ class DebugCommunication(object):
self._process_stopped()
tid = body["threadId"]
self.thread_stop_reasons[tid] = body
elif event == "breakpoint":
# Breakpoint events come in when a breakpoint has locations
# added or removed. Keep track of them so we can look for them
# in tests.
self.breakpoint_events.append(packet)
# no need to add 'breakpoint' event packets to our packets list
return keepGoing
elif event.startswith("progress"):
# Progress events come in as 'progressStart', 'progressUpdate',
# and 'progressEnd' events. Keep these around in case test
@ -412,6 +404,15 @@ class DebugCommunication(object):
self.threads = []
return stopped_events
def wait_for_breakpoint_events(self, timeout=None):
breakpoint_events = []
while True:
event = self.wait_for_event("breakpoint", timeout=timeout)
if not event:
break
breakpoint_events.append(event)
return breakpoint_events
def wait_for_exited(self):
event_dict = self.wait_for_event("exited")
if event_dict is None:
@ -591,6 +592,7 @@ class DebugCommunication(object):
attachCommands=None,
terminateCommands=None,
coreFile=None,
stopOnAttach=True,
postRunCommands=None,
sourceMap=None,
gdbRemotePort=None,
@ -620,6 +622,8 @@ class DebugCommunication(object):
args_dict["attachCommands"] = attachCommands
if coreFile:
args_dict["coreFile"] = coreFile
if stopOnAttach:
args_dict["stopOnEntry"] = stopOnAttach
if postRunCommands:
args_dict["postRunCommands"] = postRunCommands
if sourceMap:
@ -632,7 +636,7 @@ class DebugCommunication(object):
response = self.send_recv(command_dict)
if response["success"]:
self.wait_for_events(["process", "initialized"])
self.wait_for_event("process")
return response
def request_breakpointLocations(
@ -666,10 +670,6 @@ class DebugCommunication(object):
response = self.send_recv(command_dict)
if response:
self.configuration_done_sent = True
# Client requests the baseline of currently existing threads after
# a successful launch or attach.
# Kick off the threads request that follows
self.request_threads()
return response
def _process_stopped(self):
@ -887,7 +887,7 @@ class DebugCommunication(object):
response = self.send_recv(command_dict)
if response["success"]:
self.wait_for_events(["process", "initialized"])
self.wait_for_event("process")
return response
def request_next(self, threadId, granularity="statement"):
@ -1325,6 +1325,26 @@ def attach_options_specified(options):
def run_vscode(dbg, args, options):
dbg.request_initialize(options.sourceInitFile)
if options.sourceBreakpoints:
source_to_lines = {}
for file_line in options.sourceBreakpoints:
(path, line) = file_line.split(":")
if len(path) == 0 or len(line) == 0:
print('error: invalid source with line "%s"' % (file_line))
else:
if path in source_to_lines:
source_to_lines[path].append(int(line))
else:
source_to_lines[path] = [int(line)]
for source in source_to_lines:
dbg.request_setBreakpoints(source, source_to_lines[source])
if options.funcBreakpoints:
dbg.request_setFunctionBreakpoints(options.funcBreakpoints)
dbg.request_configurationDone()
if attach_options_specified(options):
response = dbg.request_attach(
program=options.program,
@ -1353,23 +1373,6 @@ def run_vscode(dbg, args, options):
)
if response["success"]:
if options.sourceBreakpoints:
source_to_lines = {}
for file_line in options.sourceBreakpoints:
(path, line) = file_line.split(":")
if len(path) == 0 or len(line) == 0:
print('error: invalid source with line "%s"' % (file_line))
else:
if path in source_to_lines:
source_to_lines[path].append(int(line))
else:
source_to_lines[path] = [int(line)]
for source in source_to_lines:
dbg.request_setBreakpoints(source, source_to_lines[source])
if options.funcBreakpoints:
dbg.request_setFunctionBreakpoints(options.funcBreakpoints)
dbg.request_configurationDone()
dbg.wait_for_stopped()
else:
if "message" in response:

View File

@ -340,6 +340,7 @@ class DAPTestCaseBase(TestBase):
exitCommands=None,
attachCommands=None,
coreFile=None,
stopOnAttach=True,
disconnectAutomatically=True,
terminateCommands=None,
postRunCommands=None,
@ -348,6 +349,8 @@ class DAPTestCaseBase(TestBase):
expectFailure=False,
gdbRemotePort=None,
gdbRemoteHostname=None,
sourceBreakpoints=None,
functionBreakpoints=None,
):
"""Build the default Makefile target, create the DAP debug adapter,
and attach to the process.
@ -364,6 +367,28 @@ class DAPTestCaseBase(TestBase):
self.addTearDownHook(cleanup)
# Initialize and launch the program
self.dap_server.request_initialize(sourceInitFile)
self.dap_server.wait_for_event("initialized")
# Set source breakpoints as part of the launch sequence.
if sourceBreakpoints:
for source_path, lines in sourceBreakpoints:
response = self.dap_server.request_setBreakpoints(source_path, lines)
self.assertTrue(
response["success"],
"setBreakpoints failed (%s)" % (response),
)
# Set function breakpoints as part of the launch sequence.
if functionBreakpoints:
response = self.dap_server.request_setFunctionBreakpoints(
functionBreakpoints
)
self.assertTrue(
response["success"],
"setFunctionBreakpoint failed (%s)" % (response),
)
self.dap_server.request_configurationDone()
response = self.dap_server.request_attach(
program=program,
pid=pid,
@ -376,6 +401,7 @@ class DAPTestCaseBase(TestBase):
attachCommands=attachCommands,
terminateCommands=terminateCommands,
coreFile=coreFile,
stopOnAttach=stopOnAttach,
postRunCommands=postRunCommands,
sourceMap=sourceMap,
gdbRemotePort=gdbRemotePort,
@ -419,6 +445,8 @@ class DAPTestCaseBase(TestBase):
commandEscapePrefix=None,
customFrameFormat=None,
customThreadFormat=None,
sourceBreakpoints=None,
functionBreakpoints=None,
):
"""Sending launch request to dap"""
@ -434,6 +462,29 @@ class DAPTestCaseBase(TestBase):
# Initialize and launch the program
self.dap_server.request_initialize(sourceInitFile)
self.dap_server.wait_for_event("initialized")
# Set source breakpoints as part of the launch sequence.
if sourceBreakpoints:
for source_path, lines in sourceBreakpoints:
response = self.dap_server.request_setBreakpoints(source_path, lines)
self.assertTrue(
response["success"],
"setBreakpoints failed (%s)" % (response),
)
# Set function breakpoints as part of the launch sequence.
if functionBreakpoints:
response = self.dap_server.request_setFunctionBreakpoints(
functionBreakpoints
)
self.assertTrue(
response["success"],
"setFunctionBreakpoint failed (%s)" % (response),
)
self.dap_server.request_configurationDone()
response = self.dap_server.request_launch(
program,
args=args,
@ -504,6 +555,8 @@ class DAPTestCaseBase(TestBase):
customThreadFormat=None,
launchCommands=None,
expectFailure=False,
sourceBreakpoints=None,
functionBreakpoints=None,
):
"""Build the default Makefile target, create the DAP debug adapter,
and launch the process.
@ -540,6 +593,8 @@ class DAPTestCaseBase(TestBase):
customThreadFormat=customThreadFormat,
launchCommands=launchCommands,
expectFailure=expectFailure,
sourceBreakpoints=sourceBreakpoints,
functionBreakpoints=functionBreakpoints,
)
def getBuiltinDebugServerTool(self):

View File

@ -27,6 +27,8 @@ def spawn_and_wait(program, delay):
@skip
class TestDAP_attach(lldbdap_testcase.DAPTestCaseBase):
def set_and_hit_breakpoint(self, continueToExit=True):
self.dap_server.wait_for_stopped()
source = "main.c"
breakpoint1_line = line_number(source, "// breakpoint 1")
lines = [breakpoint1_line]

View File

@ -18,17 +18,17 @@ import sys
import socket
@skip
class TestDAP_attachByPortNum(lldbdap_testcase.DAPTestCaseBase):
default_timeout = 20
def set_and_hit_breakpoint(self, continueToExit=True):
self.dap_server.wait_for_stopped()
source = "main.c"
main_source_path = os.path.join(os.getcwd(), source)
breakpoint1_line = line_number(main_source_path, "// breakpoint 1")
breakpoint1_line = line_number(source, "// breakpoint 1")
lines = [breakpoint1_line]
# Set breakpoint in the thread function so we can step the threads
breakpoint_ids = self.set_source_breakpoints(main_source_path, lines)
breakpoint_ids = self.set_source_breakpoints(source, lines)
self.assertEqual(
len(breakpoint_ids), len(lines), "expect correct number of breakpoints"
)

View File

@ -81,52 +81,27 @@ class TestDAP_breakpointEvents(lldbdap_testcase.DAPTestCaseBase):
breakpoint["verified"], "expect foo breakpoint to not be verified"
)
# Get the stop at the entry point
self.continue_to_next_stop()
# Make sure we're stopped.
self.dap_server.wait_for_stopped()
# We are now stopped at the entry point to the program. Shared
# libraries are not loaded yet (at least on macOS they aren't) and only
# the breakpoint in the main executable should be resolved.
self.assertEqual(len(self.dap_server.breakpoint_events), 1)
event = self.dap_server.breakpoint_events[0]
body = event["body"]
self.assertEqual(
body["reason"], "changed", "breakpoint event should say changed"
)
breakpoint = body["breakpoint"]
self.assertEqual(breakpoint["id"], main_bp_id)
self.assertTrue(breakpoint["verified"], "main breakpoint should be resolved")
# Clear the list of breakpoint events so we don't see this one again.
self.dap_server.breakpoint_events.clear()
# Flush the breakpoint events.
self.dap_server.wait_for_breakpoint_events(timeout=5)
# Continue to the breakpoint
self.continue_to_breakpoints(dap_breakpoint_ids)
# When the process launches, we first expect to see both the main and
# foo breakpoint as unresolved.
for event in self.dap_server.breakpoint_events[:2]:
body = event["body"]
self.assertEqual(
body["reason"], "changed", "breakpoint event should say changed"
)
breakpoint = body["breakpoint"]
self.assertIn(str(breakpoint["id"]), dap_breakpoint_ids)
self.assertFalse(breakpoint["verified"], "breakpoint should be unresolved")
verified_breakpoint_ids = []
unverified_breakpoint_ids = []
for breakpoint_event in self.dap_server.wait_for_breakpoint_events(timeout=5):
breakpoint = breakpoint_event["body"]["breakpoint"]
id = breakpoint["id"]
if breakpoint["verified"]:
verified_breakpoint_ids.append(id)
else:
unverified_breakpoint_ids.append(id)
# Then, once the dynamic loader has given us a load address, they
# should show up as resolved again.
for event in self.dap_server.breakpoint_events[3:]:
body = event["body"]
self.assertEqual(
body["reason"], "changed", "breakpoint event should say changed"
)
breakpoint = body["breakpoint"]
self.assertIn(str(breakpoint["id"]), dap_breakpoint_ids)
self.assertTrue(breakpoint["verified"], "breakpoint should be resolved")
self.assertNotIn(
"source",
breakpoint,
"breakpoint event should not return a source object",
)
self.assertIn("line", breakpoint, "breakpoint event should have line")
self.assertIn(main_bp_id, unverified_breakpoint_ids)
self.assertIn(foo_bp_id, unverified_breakpoint_ids)
self.assertIn(main_bp_id, verified_breakpoint_ids)
self.assertIn(foo_bp_id, verified_breakpoint_ids)

View File

@ -2,7 +2,6 @@
Test lldb-dap completions request
"""
import lldbdap_testcase
import dap_server
from lldbsuite.test import lldbutil
@ -32,6 +31,7 @@ variable_var_completion = {
variable_var1_completion = {"text": "var1", "label": "var1 -- int &"}
variable_var2_completion = {"text": "var2", "label": "var2 -- int &"}
# Older version of libcxx produce slightly different typename strings for
# templates like vector.
@skipIf(compiler="clang", compiler_version=["<", "16.0"])
@ -43,16 +43,22 @@ class TestDAP_completions(lldbdap_testcase.DAPTestCaseBase):
for not_expected_item in not_expected_list:
self.assertNotIn(not_expected_item, actual_list)
def setup_debugee(self):
def setup_debugee(self, stopOnEntry=False):
program = self.getBuildArtifact("a.out")
self.build_and_launch(program)
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.build_and_launch(
program,
stopOnEntry=stopOnEntry,
sourceBreakpoints=[
(
source,
[
line_number(source, "// breakpoint 1"),
line_number(source, "// breakpoint 2"),
],
),
],
)
def test_command_completions(self):
"""
@ -235,7 +241,7 @@ class TestDAP_completions(lldbdap_testcase.DAPTestCaseBase):
"""
Tests completion requests in "repl-mode=auto"
"""
self.setup_debugee()
self.setup_debugee(stopOnEntry=True)
res = self.dap_server.request_evaluate(
"`lldb-dap repl-mode auto", context="repl"

View File

@ -19,6 +19,7 @@ def get_subprocess(root_process, process_name):
self.assertTrue(False, "No subprocess with name %s found" % process_name)
class TestDAP_console(lldbdap_testcase.DAPTestCaseBase):
def check_lldb_command(
self, lldb_command, contains_string, assert_msg, command_escape_prefix="`"
@ -52,7 +53,7 @@ class TestDAP_console(lldbdap_testcase.DAPTestCaseBase):
character.
"""
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")
lines = [breakpoint1_line]
@ -81,7 +82,7 @@ class TestDAP_console(lldbdap_testcase.DAPTestCaseBase):
def test_custom_escape_prefix(self):
program = self.getBuildArtifact("a.out")
self.build_and_launch(program, commandEscapePrefix="::")
self.build_and_launch(program, stopOnEntry=True, commandEscapePrefix="::")
source = "main.cpp"
breakpoint1_line = line_number(source, "// breakpoint 1")
breakpoint_ids = self.set_source_breakpoints(source, [breakpoint1_line])
@ -96,7 +97,7 @@ class TestDAP_console(lldbdap_testcase.DAPTestCaseBase):
def test_empty_escape_prefix(self):
program = self.getBuildArtifact("a.out")
self.build_and_launch(program, commandEscapePrefix="")
self.build_and_launch(program, stopOnEntry=True, commandEscapePrefix="")
source = "main.cpp"
breakpoint1_line = line_number(source, "// breakpoint 1")
breakpoint_ids = self.set_source_breakpoints(source, [breakpoint1_line])
@ -113,7 +114,7 @@ class TestDAP_console(lldbdap_testcase.DAPTestCaseBase):
def test_exit_status_message_sigterm(self):
source = "main.cpp"
program = self.getBuildArtifact("a.out")
self.build_and_launch(program, commandEscapePrefix="")
self.build_and_launch(program, stopOnEntry=True, commandEscapePrefix="")
breakpoint1_line = line_number(source, "// breakpoint 1")
breakpoint_ids = self.set_source_breakpoints(source, [breakpoint1_line])
self.continue_to_breakpoints(breakpoint_ids)
@ -167,7 +168,7 @@ class TestDAP_console(lldbdap_testcase.DAPTestCaseBase):
def test_diagnositcs(self):
program = self.getBuildArtifact("a.out")
self.build_and_launch(program)
self.build_and_launch(program, stopOnEntry=True)
core = self.getBuildArtifact("minidump.core")
self.yaml2obj("minidump.yaml", core)

View File

@ -16,7 +16,9 @@ class TestDAP_redirection_to_console(lldbdap_testcase.DAPTestCaseBase):
"""
program = self.getBuildArtifact("a.out")
self.build_and_launch(
program, lldbDAPEnv={"LLDB_DAP_TEST_STDOUT_STDERR_REDIRECTION": ""}
program,
stopOnEntry=True,
lldbDAPEnv={"LLDB_DAP_TEST_STDOUT_STDERR_REDIRECTION": ""},
)
source = "main.cpp"

View File

@ -31,7 +31,7 @@ class TestDAP_launch(lldbdap_testcase.DAPTestCaseBase):
created.
"""
program = self.getBuildArtifact("a.out")
self.build_and_launch(program, disconnectAutomatically=False)
self.build_and_launch(program, stopOnEntry=True, disconnectAutomatically=False)
# We set a breakpoint right before the side effect file is created
self.set_source_breakpoints(
@ -39,7 +39,11 @@ class TestDAP_launch(lldbdap_testcase.DAPTestCaseBase):
)
self.continue_to_next_stop()
# verify we haven't produced the side effect file yet
self.assertFalse(os.path.exists(program + ".side_effect"))
self.dap_server.request_disconnect()
# verify we didn't produce the side effect file
time.sleep(1)
self.assertFalse(os.path.exists(program + ".side_effect"))

View File

@ -10,6 +10,7 @@ from lldbsuite.test import lldbutil
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
# DAP tests are flakey, see https://github.com/llvm/llvm-project/issues/137660.
@skip
class TestDAP_evaluate(lldbdap_testcase.DAPTestCaseBase):
@ -42,7 +43,9 @@ class TestDAP_evaluate(lldbdap_testcase.DAPTestCaseBase):
self.context = context
program = self.getBuildArtifact("a.out")
self.build_and_launch(
program, enableAutoVariableSummaries=enableAutoVariableSummaries
program,
enableAutoVariableSummaries=enableAutoVariableSummaries,
stopOnEntry=True,
)
source = "main.cpp"
self.set_source_breakpoints(

View File

@ -2,7 +2,6 @@
Test exception behavior in DAP with signal.
"""
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
import lldbdap_testcase
@ -17,7 +16,7 @@ class TestDAP_exception(lldbdap_testcase.DAPTestCaseBase):
"""
program = self.getBuildArtifact("a.out")
self.build_and_launch(program)
self.dap_server.request_continue()
self.assertTrue(self.verify_stop_exception_info("signal SIGABRT"))
exceptionInfo = self.get_exceptionInfo()
self.assertEqual(exceptionInfo["breakMode"], "always")

View File

@ -15,6 +15,7 @@ import re
# Despite the test program printing correctly. See
# https://github.com/llvm/llvm-project/issues/137599.
class TestDAP_launch(lldbdap_testcase.DAPTestCaseBase):
@skipIfWindows
def test_default(self):
@ -88,8 +89,8 @@ class TestDAP_launch(lldbdap_testcase.DAPTestCaseBase):
"""
program = self.getBuildArtifact("a.out")
self.build_and_launch(program, stopOnEntry=True)
self.set_function_breakpoints(["main"])
stopped_events = self.continue_to_next_stop()
stopped_events = self.dap_server.wait_for_stopped()
for stopped_event in stopped_events:
if "body" in stopped_event:
body = stopped_event["body"]
@ -357,6 +358,7 @@ class TestDAP_launch(lldbdap_testcase.DAPTestCaseBase):
terminateCommands = ["expr 4+2"]
self.build_and_launch(
program,
stopOnEntry=True,
initCommands=initCommands,
preRunCommands=preRunCommands,
postRunCommands=postRunCommands,
@ -530,6 +532,7 @@ class TestDAP_launch(lldbdap_testcase.DAPTestCaseBase):
terminateCommands = ["expr 4+2"]
self.launch(
program=program,
stopOnEntry=True,
terminateCommands=terminateCommands,
disconnectAutomatically=False,
)

View File

@ -50,7 +50,7 @@ class TestDAP_progress(lldbdap_testcase.DAPTestCaseBase):
@skipIfWindows
def test(self):
program = self.getBuildArtifact("a.out")
self.build_and_launch(program)
self.build_and_launch(program, stopOnEntry=True)
progress_emitter = os.path.join(os.getcwd(), "Progress_emitter.py")
self.dap_server.request_evaluate(
f"`command script import {progress_emitter}", context="repl"

View File

@ -20,7 +20,7 @@ class TestDAP_repl_mode_detection(lldbdap_testcase.DAPTestCaseBase):
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")

View File

@ -22,7 +22,6 @@ class TestDAP_restart(lldbdap_testcase.DAPTestCaseBase):
[bp_A, bp_B] = self.set_source_breakpoints("main.c", [line_A, line_B])
# Verify we hit A, then B.
self.dap_server.request_configurationDone()
self.verify_breakpoint_hit([bp_A])
self.dap_server.request_continue()
self.verify_breakpoint_hit([bp_B])

View File

@ -74,7 +74,6 @@ class TestDAP_restart_runInTerminal(lldbdap_testcase.DAPTestCaseBase):
program = self.getBuildArtifact("a.out")
self.build_and_launch(program, runInTerminal=True, stopOnEntry=True)
[bp_main] = self.set_function_breakpoints(["main"])
self.dap_server.request_configurationDone()
# When using stopOnEntry, configurationDone doesn't result in a running
# process, we should immediately get a stopped event instead.

View File

@ -16,12 +16,14 @@ class TestDAP_sendEvent(lldbdap_testcase.DAPTestCaseBase):
"""
program = self.getBuildArtifact("a.out")
source = "main.c"
breakpoint_line = line_number(source, "// breakpoint")
custom_event_body = {
"key": 321,
"arr": [True],
}
self.build_and_launch(
program,
sourceBreakpoints=[(source, [breakpoint_line])],
stopCommands=[
"lldb-dap send-event my-custom-event-no-body",
"lldb-dap send-event my-custom-event '{}'".format(
@ -30,11 +32,6 @@ class TestDAP_sendEvent(lldbdap_testcase.DAPTestCaseBase):
],
)
breakpoint_line = line_number(source, "// breakpoint")
self.set_source_breakpoints(source, [breakpoint_line])
self.continue_to_next_stop()
custom_event = self.dap_server.wait_for_event(
filter=["my-custom-event-no-body"]
)

View File

@ -61,7 +61,7 @@ class TestDAP_stackTrace(lldbdap_testcase.DAPTestCaseBase):
Tests the 'stackTrace' packet and all its variants.
"""
program = self.getBuildArtifact("a.out")
self.build_and_launch(program)
self.build_and_launch(program, stopOnEntry=True)
source = "main.c"
self.source_path = os.path.join(os.getcwd(), source)
self.recurse_end = line_number(source, "recurse end")

View File

@ -37,7 +37,7 @@ class TestDAP_stackTraceMissingSourcePath(lldbdap_testcase.DAPTestCaseBase):
breakpoint_line = line_number(other_source_file, "// Break here")
program = self.getBuildArtifact("a.out")
self.build_and_launch(program, commandEscapePrefix="")
self.build_and_launch(program, stopOnEntry=True, commandEscapePrefix="")
breakpoint_ids = self.set_source_breakpoints(
other_source_file, [breakpoint_line]

View File

@ -2,7 +2,6 @@
Test lldb-dap start-debugging reverse requests.
"""
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
import lldbdap_testcase
@ -16,7 +15,7 @@ class TestDAP_startDebugging(lldbdap_testcase.DAPTestCaseBase):
"""
program = self.getBuildArtifact("a.out")
source = "main.c"
self.build_and_launch(program)
self.build_and_launch(program, stopOnEntry=True)
breakpoint_line = line_number(source, "// breakpoint")

View File

@ -19,7 +19,7 @@ class TestDAP_stop_hooks(lldbdap_testcase.DAPTestCaseBase):
self.build_and_launch(program, stopOnEntry=True, preRunCommands=preRunCommands)
# The first stop is on entry.
self.continue_to_next_stop()
self.dap_server.wait_for_stopped()
breakpoint_ids = self.set_function_breakpoints(["main"])
# This request hangs if the race happens, because, in that case, the

View File

@ -13,13 +13,13 @@ class TestDAP_variables_children(lldbdap_testcase.DAPTestCaseBase):
program = self.getBuildArtifact("a.out")
self.build_and_launch(
program,
stopOnEntry=True,
preRunCommands=[
"command script import '%s'" % self.getSourcePath("formatter.py")
],
)
source = "main.cpp"
breakpoint1_line = line_number(source, "// break here")
lines = [breakpoint1_line]
breakpoint_ids = self.set_source_breakpoints(
source, [line_number(source, "// break here")]
@ -47,7 +47,7 @@ class TestDAP_variables_children(lldbdap_testcase.DAPTestCaseBase):
Test the stepping out of a function with return value show the children correctly
"""
program = self.getBuildArtifact("a.out")
self.build_and_launch(program)
self.build_and_launch(program, stopOnEntry=True)
function_name = "test_return_variable_with_children"
breakpoint_ids = self.set_function_breakpoints([function_name])

View File

@ -84,8 +84,8 @@ DAP::DAP(Log *log, const ReplMode default_repl_mode,
: log(log), transport(transport), broadcaster("lldb-dap"),
exception_breakpoints(), focus_tid(LLDB_INVALID_THREAD_ID),
stop_at_entry(false), is_attach(false),
restarting_process_id(LLDB_INVALID_PROCESS_ID),
configuration_done_sent(false), waiting_for_run_in_terminal(false),
restarting_process_id(LLDB_INVALID_PROCESS_ID), configuration_done(false),
waiting_for_run_in_terminal(false),
progress_event_reporter(
[&](const ProgressEvent &event) { SendJSON(event.ToJSON()); }),
reverse_request_seq(0), repl_mode(default_repl_mode) {
@ -893,10 +893,19 @@ llvm::Error DAP::Loop() {
return errWrapper;
}
// The launch sequence is special and we need to carefully handle
// packets in the right order. Until we've handled configurationDone,
bool add_to_pending_queue = false;
if (const protocol::Request *req =
std::get_if<protocol::Request>(&*next);
req && req->command == "disconnect") {
disconnecting = true;
std::get_if<protocol::Request>(&*next)) {
llvm::StringRef command = req->command;
if (command == "disconnect")
disconnecting = true;
if (!configuration_done)
add_to_pending_queue =
command != "initialize" && command != "configurationDone" &&
command != "disconnect" && !command.ends_with("Breakpoints");
}
const std::optional<CancelArguments> cancel_args =
@ -924,7 +933,8 @@ llvm::Error DAP::Loop() {
{
std::lock_guard<std::mutex> guard(m_queue_mutex);
m_queue.push_back(std::move(*next));
auto &queue = add_to_pending_queue ? m_pending_queue : m_queue;
queue.push_back(std::move(*next));
}
m_queue_cv.notify_one();
}
@ -938,16 +948,19 @@ llvm::Error DAP::Loop() {
StopEventHandlers();
});
while (!disconnecting || !m_queue.empty()) {
while (true) {
std::unique_lock<std::mutex> lock(m_queue_mutex);
m_queue_cv.wait(lock, [&] { return disconnecting || !m_queue.empty(); });
if (m_queue.empty())
if (disconnecting && m_queue.empty())
break;
Message next = m_queue.front();
m_queue.pop_front();
// Unlock while we're processing the event.
lock.unlock();
if (!HandleObject(next))
return llvm::createStringError(llvm::inconvertibleErrorCode(),
"unhandled packet");
@ -1219,6 +1232,16 @@ void DAP::SetConfiguration(const protocol::Configuration &config,
SetThreadFormat(*configuration.customThreadFormat);
}
void DAP::SetConfigurationDone() {
{
std::lock_guard<std::mutex> guard(m_queue_mutex);
std::copy(m_pending_queue.begin(), m_pending_queue.end(),
std::front_inserter(m_queue));
configuration_done = true;
}
m_queue_cv.notify_all();
}
void DAP::SetFrameFormat(llvm::StringRef format) {
if (format.empty())
return;

View File

@ -188,7 +188,7 @@ struct DAP {
// shutting down the entire adapter. When we're restarting, we keep the id of
// the old process here so we can detect this case and keep running.
lldb::pid_t restarting_process_id;
bool configuration_done_sent;
bool configuration_done;
llvm::StringMap<std::unique_ptr<BaseRequestHandler>> request_handlers;
bool waiting_for_run_in_terminal;
ProgressEventReporter progress_event_reporter;
@ -251,6 +251,8 @@ struct DAP {
/// Configures the debug adapter for launching/attaching.
void SetConfiguration(const protocol::Configuration &confing, bool is_attach);
void SetConfigurationDone();
/// Configure source maps based on the current `DAPConfiguration`.
void ConfigureSourceMaps();
@ -417,8 +419,10 @@ struct DAP {
lldb::SBMutex GetAPIMutex() const { return target.GetAPIMutex(); }
private:
std::mutex m_queue_mutex;
/// Queue for all incoming messages.
std::deque<protocol::Message> m_queue;
std::deque<protocol::Message> m_pending_queue;
std::mutex m_queue_mutex;
std::condition_variable m_queue_cv;
std::mutex m_cancelled_requests_mutex;

View File

@ -222,7 +222,7 @@ void SendContinuedEvent(DAP &dap) {
// 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_sent || dap.focus_tid == LLDB_INVALID_THREAD_ID) {
if (!dap.configuration_done || dap.focus_tid == LLDB_INVALID_THREAD_ID) {
return;
}

View File

@ -133,61 +133,70 @@ void AttachRequestHandler::operator()(const llvm::json::Object &request) const {
dap.SendOutput(OutputType::Console,
llvm::StringRef(attach_msg, attach_msg_len));
}
if (attachCommands.empty()) {
// No "attachCommands", just attach normally.
// Disable async events so the attach will be successful when we return from
// the launch call and the launch will happen synchronously
{
// Perform the launch in synchronous mode so that we don't have to worry
// about process state changes during the launch.
ScopeSyncMode scope_sync_mode(dap.debugger);
if (attachCommands.empty()) {
// No "attachCommands", just attach normally.
if (core_file.empty()) {
if ((pid != LLDB_INVALID_PROCESS_ID) &&
(gdb_remote_port != invalid_port)) {
// If both pid and port numbers are specified.
error.SetErrorString("The user can't specify both pid and port");
} else if (gdb_remote_port != invalid_port) {
// If port is specified and pid is not.
lldb::SBListener listener = dap.debugger.GetListener();
if (core_file.empty()) {
if ((pid != LLDB_INVALID_PROCESS_ID) &&
(gdb_remote_port != invalid_port)) {
// If both pid and port numbers are specified.
error.SetErrorString("The user can't specify both pid and port");
} else if (gdb_remote_port != invalid_port) {
// If port is specified and pid is not.
lldb::SBListener listener = dap.debugger.GetListener();
// If the user hasn't provided the hostname property, default localhost
// being used.
std::string connect_url =
llvm::formatv("connect://{0}:", gdb_remote_hostname);
connect_url += std::to_string(gdb_remote_port);
dap.target.ConnectRemote(listener, connect_url.c_str(), "gdb-remote",
error);
// If the user hasn't provided the hostname property, default
// localhost being used.
std::string connect_url =
llvm::formatv("connect://{0}:", gdb_remote_hostname);
connect_url += std::to_string(gdb_remote_port);
dap.target.ConnectRemote(listener, connect_url.c_str(), "gdb-remote",
error);
} else {
// Attach by pid or process name.
lldb::SBAttachInfo attach_info;
if (pid != LLDB_INVALID_PROCESS_ID)
attach_info.SetProcessID(pid);
else if (dap.configuration.program.has_value())
attach_info.SetExecutable(dap.configuration.program->data());
attach_info.SetWaitForLaunch(wait_for, false /*async*/);
dap.target.Attach(attach_info, error);
}
} else {
// Attach by pid or process name.
lldb::SBAttachInfo attach_info;
if (pid != LLDB_INVALID_PROCESS_ID)
attach_info.SetProcessID(pid);
else if (dap.configuration.program.has_value())
attach_info.SetExecutable(dap.configuration.program->data());
attach_info.SetWaitForLaunch(wait_for, false /*async*/);
dap.target.Attach(attach_info, error);
dap.target.LoadCore(core_file.data(), error);
}
} else {
dap.target.LoadCore(core_file.data(), error);
// We have "attachCommands" that are a set of commands that are expected
// to execute the commands after which a process should be created. If
// there is no valid process after running these commands, we have failed.
if (llvm::Error err = dap.RunAttachCommands(attachCommands)) {
response["success"] = false;
EmplaceSafeString(response, "message", llvm::toString(std::move(err)));
dap.SendJSON(llvm::json::Value(std::move(response)));
return;
}
// The custom commands might have created a new target so we should use
// the selected target after these commands are run.
dap.target = dap.debugger.GetSelectedTarget();
}
} else {
// We have "attachCommands" that are a set of commands that are expected
// to execute the commands after which a process should be created. If there
// is no valid process after running these commands, we have failed.
if (llvm::Error err = dap.RunAttachCommands(attachCommands)) {
response["success"] = false;
EmplaceSafeString(response, "message", llvm::toString(std::move(err)));
dap.SendJSON(llvm::json::Value(std::move(response)));
return;
}
// The custom commands might have created a new target so we should use the
// selected target after these commands are run.
dap.target = dap.debugger.GetSelectedTarget();
// Make sure the process is attached and stopped before proceeding as the
// the launch commands are not run using the synchronous mode.
error = dap.WaitForProcessToStop(std::chrono::seconds(timeout_seconds));
}
// Make sure the process is attached and stopped.
error = dap.WaitForProcessToStop(std::chrono::seconds(timeout_seconds));
// Clients can request a baseline of currently existing threads after
// we acknowledge the configurationDone request.
// Client requests the baseline of currently existing threads after
// a successful or attach by sending a 'threads' request
// right after receiving the configurationDone response.
// Obtain the list of threads before we resume the process
dap.initial_thread_list =
GetThreads(dap.target.GetProcess(), dap.thread_format);
if (error.Success() && core_file.empty()) {
auto attached_pid = dap.target.GetProcess().GetProcessID();
if (attached_pid == LLDB_INVALID_PROCESS_ID) {
@ -206,9 +215,17 @@ void AttachRequestHandler::operator()(const llvm::json::Object &request) const {
}
dap.SendJSON(llvm::json::Value(std::move(response)));
// FIXME: Move this into PostRun.
if (error.Success()) {
SendProcessEvent(dap, Attach);
dap.SendJSON(CreateEventObject("initialized"));
if (dap.target.GetProcess().IsValid()) {
SendProcessEvent(dap, Attach);
if (dap.stop_at_entry)
SendThreadStoppedEvent(dap);
else
dap.target.GetProcess().Continue();
}
}
}

View File

@ -47,21 +47,11 @@ namespace lldb_dap {
void ConfigurationDoneRequestHandler::operator()(
const llvm::json::Object &request) const {
dap.SetConfigurationDone();
llvm::json::Object response;
FillResponse(request, response);
dap.SendJSON(llvm::json::Value(std::move(response)));
dap.configuration_done_sent = true;
if (dap.stop_at_entry)
SendThreadStoppedEvent(dap);
else {
// Client requests the baseline of currently existing threads after
// a successful launch or attach by sending a 'threads' request
// right after receiving the configurationDone response.
// Obtain the list of threads before we resume the process
dap.initial_thread_list =
GetThreads(dap.target.GetProcess(), dap.thread_format);
dap.target.GetProcess().Continue();
}
}
} // namespace lldb_dap

View File

@ -140,43 +140,28 @@ static void EventThreadFunction(DAP &dap) {
lldb::SBProcess process = lldb::SBProcess::GetProcessFromEvent(event);
if (event_mask & lldb::SBProcess::eBroadcastBitStateChanged) {
auto state = lldb::SBProcess::GetStateFromEvent(event);
DAP_LOG(dap.log, "State = {0}", state);
switch (state) {
case lldb::eStateConnected:
case lldb::eStateDetached:
case lldb::eStateInvalid:
// Not a state event
break;
case lldb::eStateUnloaded:
break;
case lldb::eStateConnected:
break;
case lldb::eStateAttaching:
break;
case lldb::eStateLaunching:
break;
case lldb::eStateStepping:
break;
case lldb::eStateCrashed:
break;
case lldb::eStateDetached:
break;
case lldb::eStateSuspended:
break;
case lldb::eStateLaunching:
case lldb::eStateStopped:
// We launch and attach in synchronous mode then the first stop
// event will not be delivered. If we use "launchCommands" during a
// launch or "attachCommands" during an attach we might some process
// stop events which we do not want to send an event for. We will
// manually send a stopped event in request_configurationDone(...)
// so don't send any before then.
if (dap.configuration_done_sent) {
// Only report a stopped event if the process was not
// automatically restarted.
if (!lldb::SBProcess::GetRestartedFromEvent(event)) {
SendStdOutStdErr(dap, process);
SendThreadStoppedEvent(dap);
}
case lldb::eStateSuspended:
// Only report a stopped event if the process was not
// automatically restarted.
if (!lldb::SBProcess::GetRestartedFromEvent(event)) {
SendStdOutStdErr(dap, process);
SendThreadStoppedEvent(dap);
}
break;
case lldb::eStateRunning:
case lldb::eStateStepping:
dap.WillContinue();
SendContinuedEvent(dap);
break;
@ -284,6 +269,7 @@ llvm::Expected<InitializeResponseBody> InitializeRequestHandler::Run(
// Do not source init files until in/out/err are configured.
dap.debugger = lldb::SBDebugger::Create(false);
dap.debugger.SetInputFile(dap.in);
dap.target = dap.debugger.GetDummyTarget();
llvm::Expected<int> out_fd = dap.out.GetWriteFileDescriptor();
if (!out_fd)
@ -338,4 +324,8 @@ llvm::Expected<InitializeResponseBody> InitializeRequestHandler::Run(
return dap.GetCapabilities();
}
void InitializeRequestHandler::PostRun() const {
dap.SendJSON(CreateEventObject("initialized"));
}
} // namespace lldb_dap

View File

@ -71,9 +71,12 @@ void LaunchRequestHandler::PostRun() const {
if (dap.target.GetProcess().IsValid()) {
// Attach happens when launching with runInTerminal.
SendProcessEvent(dap, dap.is_attach ? Attach : Launch);
}
dap.SendJSON(CreateEventObject("initialized"));
if (dap.stop_at_entry)
SendThreadStoppedEvent(dap);
else
dap.target.GetProcess().Continue();
}
}
} // namespace lldb_dap

View File

@ -8,6 +8,7 @@
#include "Handler/RequestHandler.h"
#include "DAP.h"
#include "EventHelper.h"
#include "Handler/ResponseHandler.h"
#include "JSONUtils.h"
#include "LLDBUtils.h"
@ -162,7 +163,7 @@ RunInTerminal(DAP &dap, const protocol::LaunchRequestArguments &arguments) {
dap.target.GetProcess().Continue();
// Now that the actual target is just starting (i.e. exec was just invoked),
// we return the debugger to its async state.
// we return the debugger to its sync state.
scope_sync_mode.reset();
// If sending the notification failed, the launcher should be dead by now and
@ -238,35 +239,47 @@ llvm::Error BaseRequestHandler::LaunchProcess(
launch_info.SetLaunchFlags(flags | lldb::eLaunchFlagDebug |
lldb::eLaunchFlagStopAtEntry);
if (arguments.runInTerminal) {
if (llvm::Error err = RunInTerminal(dap, arguments))
return err;
} else if (launchCommands.empty()) {
lldb::SBError error;
// Disable async events so the launch will be successful when we return from
// the launch call and the launch will happen synchronously
{
// Perform the launch in synchronous mode so that we don't have to worry
// about process state changes during the launch.
ScopeSyncMode scope_sync_mode(dap.debugger);
dap.target.Launch(launch_info, error);
if (error.Fail())
return llvm::make_error<DAPError>(error.GetCString());
} else {
// Set the launch info so that run commands can access the configured
// launch details.
dap.target.SetLaunchInfo(launch_info);
if (llvm::Error err = dap.RunLaunchCommands(launchCommands))
return err;
// The custom commands might have created a new target so we should use the
// selected target after these commands are run.
dap.target = dap.debugger.GetSelectedTarget();
// Make sure the process is launched and stopped at the entry point before
// proceeding as the launch commands are not run using the synchronous
// mode.
lldb::SBError error = dap.WaitForProcessToStop(arguments.timeout);
if (error.Fail())
return llvm::make_error<DAPError>(error.GetCString());
if (arguments.runInTerminal) {
if (llvm::Error err = RunInTerminal(dap, arguments))
return err;
} else if (launchCommands.empty()) {
lldb::SBError error;
dap.target.Launch(launch_info, error);
if (error.Fail())
return llvm::make_error<DAPError>(error.GetCString());
} else {
// Set the launch info so that run commands can access the configured
// launch details.
dap.target.SetLaunchInfo(launch_info);
if (llvm::Error err = dap.RunLaunchCommands(launchCommands))
return err;
// The custom commands might have created a new target so we should use
// the selected target after these commands are run.
dap.target = dap.debugger.GetSelectedTarget();
}
}
// Make sure the process is launched and stopped at the entry point before
// proceeding.
lldb::SBError error = dap.WaitForProcessToStop(arguments.timeout);
if (error.Fail())
return llvm::make_error<DAPError>(error.GetCString());
// Clients can request a baseline of currently existing threads after
// we acknowledge the configurationDone request.
// Client requests the baseline of currently existing threads after
// a successful or attach by sending a 'threads' request
// right after receiving the configurationDone response.
// Obtain the list of threads before we resume the process
dap.initial_thread_list =
GetThreads(dap.target.GetProcess(), dap.thread_format);
return llvm::Error::success();
}

View File

@ -282,6 +282,7 @@ public:
static llvm::StringLiteral GetCommand() { return "initialize"; }
llvm::Expected<protocol::InitializeResponseBody>
Run(const protocol::InitializeRequestArguments &args) const override;
void PostRun() const override;
};
class LaunchRequestHandler