Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
eb22c2f
Integration w/ LLDB-DAP using shared event thread
qxy11 Aug 14, 2025
412cf86
Add targetIdx to attach config
qxy11 Aug 14, 2025
dfefa98
Move debugger construction to Launch/AttachRequest
qxy11 Aug 14, 2025
bf3fb54
Add comment, move static variable
qxy11 Aug 14, 2025
2a3b3b0
Undo temp fix for LLDBServerPluginAMDGPU
qxy11 Aug 14, 2025
58c7548
Add lldb-dap unit tests for gpu
qxy11 Aug 14, 2025
770558b
Format
qxy11 Aug 14, 2025
32f791d
Fix the hip file compilation
qxy11 Aug 14, 2025
c41920b
Rename instance variables w/ lldb convention
qxy11 Aug 14, 2025
e9dc33b
Stronger mapping from targetIdx to debugger
qxy11 Aug 14, 2025
2b5436a
Use weak reference to event handler in DAPSessionManager
qxy11 Aug 15, 2025
028eddd
Move DAPSessionManager into its own file and std::once in GetInstance()
qxy11 Aug 15, 2025
9cc4af8
Address thread safety issue w/ RAII ManagedEventThread
qxy11 Aug 18, 2025
afd3fe1
Add session name to GPUActions to make it configurable per plugin, an…
qxy11 Sep 11, 2025
aae9921
Pass configured session names to reverse request DAP instance, rename…
qxy11 Sep 11, 2025
69d841c
Assign unique target IDs to reverse attach to
qxy11 Sep 11, 2025
f18bd67
Move and fix tests for unique target ids
qxy11 Sep 11, 2025
1868fe2
Add static convenience method for FindDAP + lint
qxy11 Sep 11, 2025
ffb3a53
Unique target ids + dap_session_name -> session_name
qxy11 Sep 19, 2025
01d14b1
Fix attach request handling
qxy11 Sep 19, 2025
53b2074
Merge origin/llvm-server-plugins into dap-dual-connection
qxy11 Sep 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lldb/include/lldb/API/SBTarget.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class LLDB_API SBTarget {
eBroadcastBitWatchpointChanged = (1 << 3),
eBroadcastBitSymbolsLoaded = (1 << 4),
eBroadcastBitSymbolsChanged = (1 << 5),
eBroadcastBitNewTargetSpawned = (1 << 6),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think NewTargetCreated would be a better name

};

// Constructors
Expand Down
1 change: 1 addition & 0 deletions lldb/include/lldb/Target/Target.h
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@ class Target : public std::enable_shared_from_this<Target>,
eBroadcastBitWatchpointChanged = (1 << 3),
eBroadcastBitSymbolsLoaded = (1 << 4),
eBroadcastBitSymbolsChanged = (1 << 5),
eBroadcastBitNewTargetSpawned = (1 << 6),
};

// These two functions fill out the Broadcaster interface:
Expand Down
84 changes: 84 additions & 0 deletions lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,28 @@ def _handle_recv_packet(self, packet: Optional[ProtocolMessage]) -> bool:
elif packet_type == "response":
if packet["command"] == "disconnect":
keepGoing = False
elif packet_type == "request":
# This is a reverse request automatically spawned from LLDB (eg. for GPU targets)
command = packet.get("command", "unknown")
self.reverse_requests.append(packet)
if command == "startDebugging":
self._handle_startDebugging_request(packet)
else:
desc = f"unhandled automatic reverse request of type {command}"
raise ValueError(desc)

self._enqueue_recv_packet(packet)
return keepGoing

def _handle_startDebugging_request(self, packet):
response = {
"type": "response",
"request_seq": packet.get("seq", 0),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think you should have a default seq, as you should always get one. packet["seq"] should be fine in this case

"success": True,
"command": "startDebugging",
"body": {}
}
self.send_packet(response, set_sequence=True)

def _process_continued(self, all_threads_continued: bool):
self.frame_scopes = {}
Expand Down Expand Up @@ -670,6 +690,7 @@ def request_attach(
sourceMap: Optional[Union[list[tuple[str, str]], dict[str, str]]] = None,
gdbRemotePort: Optional[int] = None,
gdbRemoteHostname: Optional[str] = None,
targetIdx: Optional[int] = None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't use target index for this. It's not guaranteed to be unique per target for an entire session. For example, see this sequence:

  • you create two targets. They will have index 0 and 1
  • you delete target 1
  • then you create a new target. It will have index 1 again

This might result in race conditions.

I think that should use create an actual unique id for each target as part of its creation, then you can use it to select the target you want as part of the reverse request.

):
args_dict = {}
if pid is not None:
Expand Down Expand Up @@ -703,6 +724,8 @@ def request_attach(
args_dict["gdb-remote-port"] = gdbRemotePort
if gdbRemoteHostname is not None:
args_dict["gdb-remote-hostname"] = gdbRemoteHostname
if targetIdx is not None:
args_dict["targetIdx"] = targetIdx
command_dict = {"command": "attach", "type": "request", "arguments": args_dict}
return self.send_recv(command_dict)

Expand Down Expand Up @@ -1333,6 +1356,8 @@ def __init__(
):
self.process = None
self.connection = None
self.child_dap_sessions: list["DebugAdapterServer"] = [] # Track child sessions for cleanup

if executable is not None:
process, connection = DebugAdapterServer.launch(
executable=executable, connection=connection, env=env, log_file=log_file
Expand Down Expand Up @@ -1414,6 +1439,65 @@ def get_pid(self) -> int:
if self.process:
return self.process.pid
return -1

def get_child_sessions(self) -> list["DebugAdapterServer"]:
return self.child_dap_sessions

def _handle_startDebugging_request(self, packet):
"""Launch a new DebugAdapterServer with attach config parameters from the packet"""
try:
# Extract arguments from the packet
arguments = packet.get('arguments', {})
request_type = arguments.get('request', 'attach') # 'attach' or 'launch'
configuration = arguments.get('configuration', {})

# Create a new DAP session that launches its own lldb-dap process
child_dap = DebugAdapterServer(
connection=self.connection,
log_file=self.log_file
)

# Track the child session for proper cleanup
self.child_dap_sessions.append(child_dap)

# Initialize the child DAP session
child_dap.request_initialize()

# Configure the child session based on the request type and configuration
if request_type == 'attach':
# Extract attach-specific parameters
attach_commands = configuration.get('attachCommands', [])
target_idx = configuration.get('targetIdx', None)

# Send attach request to the child DAP
child_dap.request_attach(
attachCommands=attach_commands,
targetIdx=target_idx,
)
else:
raise ValueError(f"Unsupported startDebugging request type: {request_type}")

# Send success response
response = {
"type": "response",
"request_seq": packet.get("seq", 0),
"success": True,
"command": "startDebugging",
"body": {}
}

except Exception as e:
# Send error response
response = {
"type": "response",
"request_seq": packet.get("seq", 0),
"success": False,
"command": "startDebugging",
"message": f"Failed to start debugging: {str(e)}",
"body": {}
}

self.send_packet(response, set_sequence=True)

def terminate(self):
try:
Expand Down
167 changes: 119 additions & 48 deletions lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,119 @@ def create_debug_adapter(
env=lldbDAPEnv,
)

def _get_dap_server(self, child_session_index: Optional[int] = None) -> dap_server.DebugAdapterServer:
"""Get a specific DAP server instance.

Args:
child_session_index: Index of child session, or None for main session

Returns:
The requested DAP server instance
"""
if child_session_index is None:
return self.dap_server
else:
child_sessions = self.dap_server.get_child_sessions()
if child_session_index >= len(child_sessions):
raise IndexError(f"Child session index {child_session_index} out of range. Found {len(child_sessions)} child sessions.")
return child_sessions[child_session_index]

def _set_source_breakpoints_impl(self, dap_server_instance, source_path, lines, data=None, wait_for_resolve=True):
"""Implementation for setting source breakpoints on any DAP server"""
response = dap_server_instance.request_setBreakpoints(Source(source_path), lines, data)
if response is None or not response["success"]:
return []
breakpoints = response["body"]["breakpoints"]
breakpoint_ids = []
for breakpoint in breakpoints:
breakpoint_ids.append("%i" % (breakpoint["id"]))
if wait_for_resolve:
self._wait_for_breakpoints_to_resolve_impl(dap_server_instance, breakpoint_ids)
return breakpoint_ids

def _wait_for_breakpoints_to_resolve_impl(self, dap_server_instance, breakpoint_ids, timeout=None):
"""Implementation for waiting for breakpoints to resolve on any DAP server"""
if timeout is None:
timeout = self.DEFAULT_TIMEOUT
unresolved_breakpoints = dap_server_instance.wait_for_breakpoints_to_be_verified(breakpoint_ids, timeout)
self.assertEqual(
len(unresolved_breakpoints),
0,
f"Expected to resolve all breakpoints. Unresolved breakpoint ids: {unresolved_breakpoints}",
)

def _verify_breakpoint_hit_impl(self, dap_server_instance, breakpoint_ids, timeout=None):
"""Implementation for verifying breakpoint hit on any DAP server"""
if timeout is None:
timeout = self.DEFAULT_TIMEOUT
stopped_events = dap_server_instance.wait_for_stopped(timeout)
for stopped_event in stopped_events:
if "body" in stopped_event:
body = stopped_event["body"]
if "reason" not in body:
continue
if (
body["reason"] != "breakpoint"
and body["reason"] != "instruction breakpoint"
):
continue
if "description" not in body:
continue
# Descriptions for breakpoints will be in the form
# "breakpoint 1.1", so look for any description that matches
# ("breakpoint 1.") in the description field as verification
# that one of the breakpoint locations was hit. DAP doesn't
# allow breakpoints to have multiple locations, but LLDB does.
# So when looking at the description we just want to make sure
# the right breakpoint matches and not worry about the actual
# location.
description = body["description"]
for breakpoint_id in breakpoint_ids:
match_desc = f"breakpoint {breakpoint_id}."
if match_desc in description:
return
self.assertTrue(False, f"breakpoint not hit, stopped_events={stopped_events}")

def _do_continue_impl(self, dap_server_instance):
"""Implementation for continuing execution on any DAP server"""
resp = dap_server_instance.request_continue()
self.assertTrue(resp["success"], f"continue request failed: {resp}")

# Multi-session methods for operating on specific sessions without switching context
def set_source_breakpoints_on(self, child_session_index: Optional[int], source_path, lines, data=None, wait_for_resolve=True):
"""Set source breakpoints on a specific DAP session without switching the active session."""
return self._set_source_breakpoints_impl(
self._get_dap_server(child_session_index), source_path, lines, data, wait_for_resolve
)

def verify_breakpoint_hit_on(self, child_session_index: Optional[int], breakpoint_ids: list[str], timeout=DEFAULT_TIMEOUT):
"""Verify breakpoint hit on a specific DAP session without switching the active session."""
return self._verify_breakpoint_hit_impl(
self._get_dap_server(child_session_index), breakpoint_ids, timeout
)

def do_continue_on(self, child_session_index: Optional[int]):
"""Continue execution on a specific DAP session without switching the active session."""
return self._do_continue_impl(self._get_dap_server(child_session_index))

def start_server(self, connection):
"""
Start an lldb-dap server process listening on the specified connection.
"""
log_file_path = self.getBuildArtifact("dap.txt")
(process, connection) = dap_server.DebugAdapterServer.launch(
executable=self.lldbDAPExec,
connection=connection,
log_file=log_file_path
)

def cleanup():
process.terminate()

self.addTearDownHook(cleanup)

return (process, connection)

def build_and_create_debug_adapter(
self,
lldbDAPEnv: Optional[dict[str, str]] = None,
Expand All @@ -59,18 +172,9 @@ def set_source_breakpoints(
Each object in data is 1:1 mapping with the entry in lines.
It contains optional location/hitCondition/logMessage parameters.
"""
response = self.dap_server.request_setBreakpoints(
Source(source_path), lines, data
return self._set_source_breakpoints_impl(
self.dap_server, source_path, lines, data, wait_for_resolve
)
if response is None or not response["success"]:
return []
breakpoints = response["body"]["breakpoints"]
breakpoint_ids = []
for breakpoint in breakpoints:
breakpoint_ids.append("%i" % (breakpoint["id"]))
if wait_for_resolve:
self.wait_for_breakpoints_to_resolve(breakpoint_ids)
return breakpoint_ids

def set_source_breakpoints_assembly(
self, source_reference, lines, data=None, wait_for_resolve=True
Expand Down Expand Up @@ -113,13 +217,8 @@ def set_function_breakpoints(
def wait_for_breakpoints_to_resolve(
self, breakpoint_ids: list[str], timeout: Optional[float] = DEFAULT_TIMEOUT
):
unresolved_breakpoints = self.dap_server.wait_for_breakpoints_to_be_verified(
breakpoint_ids, timeout
)
self.assertEqual(
len(unresolved_breakpoints),
0,
f"Expected to resolve all breakpoints. Unresolved breakpoint ids: {unresolved_breakpoints}",
return self._wait_for_breakpoints_to_resolve_impl(
self.dap_server, breakpoint_ids, timeout
)

def waitUntil(self, condition_callback):
Expand All @@ -145,33 +244,7 @@ def verify_breakpoint_hit(self, breakpoint_ids, timeout=DEFAULT_TIMEOUT):
"breakpoint_ids" should be a list of breakpoint ID strings
(["1", "2"]). The return value from self.set_source_breakpoints()
or self.set_function_breakpoints() can be passed to this function"""
stopped_events = self.dap_server.wait_for_stopped(timeout)
for stopped_event in stopped_events:
if "body" in stopped_event:
body = stopped_event["body"]
if "reason" not in body:
continue
if (
body["reason"] != "breakpoint"
and body["reason"] != "instruction breakpoint"
):
continue
if "description" not in body:
continue
# Descriptions for breakpoints will be in the form
# "breakpoint 1.1", so look for any description that matches
# ("breakpoint 1.") in the description field as verification
# that one of the breakpoint locations was hit. DAP doesn't
# allow breakpoints to have multiple locations, but LLDB does.
# So when looking at the description we just want to make sure
# the right breakpoint matches and not worry about the actual
# location.
description = body["description"]
for breakpoint_id in breakpoint_ids:
match_desc = f"breakpoint {breakpoint_id}."
if match_desc in description:
return
self.assertTrue(False, f"breakpoint not hit, stopped_events={stopped_events}")
self._verify_breakpoint_hit_impl(self.dap_server, breakpoint_ids, timeout)

def verify_all_breakpoints_hit(self, breakpoint_ids, timeout=DEFAULT_TIMEOUT):
"""Wait for the process we are debugging to stop, and verify we hit
Expand Down Expand Up @@ -384,8 +457,7 @@ def stepOut(self, threadId=None, waitForStop=True, timeout=DEFAULT_TIMEOUT):
return None

def do_continue(self): # `continue` is a keyword.
resp = self.dap_server.request_continue()
self.assertTrue(resp["success"], f"continue request failed: {resp}")
self._do_continue_impl(self.dap_server)

def continue_to_next_stop(self, timeout=DEFAULT_TIMEOUT):
self.do_continue()
Expand Down Expand Up @@ -478,7 +550,6 @@ def launch(
**kwargs,
):
"""Sending launch request to dap"""

# Make sure we disconnect and terminate the DAP debug adapter,
# if we throw an exception during the test case
def cleanup():
Expand Down
4 changes: 4 additions & 0 deletions lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,10 @@ Status ProcessGDBRemote::HandleConnectionRequest(const GPUActions &gpu_action) {
process_sp->GetTarget().shared_from_this());
LLDB_LOG(log, "ProcessGDBRemote::HandleConnectionRequest(): successfully "
"created process!!!");
auto event_sp =
std::make_shared<Event>(Target::eBroadcastBitNewTargetSpawned,
new Target::TargetEventData(gpu_target_sp));
GetTarget().BroadcastEvent(event_sp);
return Status();
}

Expand Down
1 change: 1 addition & 0 deletions lldb/source/Target/Target.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ Target::Target(Debugger &debugger, const ArchSpec &target_arch,
SetEventName(eBroadcastBitModulesUnloaded, "modules-unloaded");
SetEventName(eBroadcastBitWatchpointChanged, "watchpoint-changed");
SetEventName(eBroadcastBitSymbolsLoaded, "symbols-loaded");
SetEventName(eBroadcastBitNewTargetSpawned, "new-target-spawned");

CheckInWithManager();

Expand Down
3 changes: 3 additions & 0 deletions lldb/test/API/tools/lldb-dap/gpu/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
HIP_SOURCES := hello_world.hip

include Makefile.rules
Loading