@@ -3342,18 +3342,27 @@ class DevtoolIdeSdkGccTests(DevtoolIdeSdkTests):
for config in configurations:
# Verify required fields exist
- required_fields = ["name", "type", "request", "program", "cwd", "MIMode",
+ required_fields = ["name", "type", "request", "program", "MIMode",
"miDebuggerPath", "miDebuggerServerAddress"]
for field in required_fields:
self.assertIn(field, config, f"Configuration '{config.get('name', 'Unknown')}' missing required field: {field}")
# Verify common configuration values
self.assertEqual(config["type"], "cppdbg", f"Configuration '{config['name']}' should use cppdbg type")
- self.assertEqual(config["request"], "launch", f"Configuration '{config['name']}' should be launch type")
- self.assertEqual(config["cwd"], "${workspaceFolder}", f"Configuration '{config['name']}' should use workspaceFolder as cwd")
self.assertEqual(config["MIMode"], "gdb", f"Configuration '{config['name']}' should use gdb MIMode")
- self.assertEqual(config.get("externalConsole", False), False, f"Configuration '{config['name']}' should not use external console")
- self.assertEqual(config.get("stopAtEntry", True), True, f"Configuration '{config['name']}' should stop at entry")
+
+ if config["request"] == "launch":
+ self.assertEqual(config["cwd"], "${workspaceFolder}", f"Configuration '{config['name']}' should use workspaceFolder as cwd")
+ self.assertEqual(config.get("externalConsole", False), False, f"Configuration '{config['name']}' should not use external console")
+ self.assertEqual(config.get("stopAtEntry", True), True, f"Configuration '{config['name']}' should stop at entry")
+ elif config["request"] == "attach":
+ # Attaching to a process running on the target requires the
+ # extended-remote protocol. Stopping the session then detaches
+ # from the process instead of killing it.
+ self.assertTrue(config.get("useExtendedRemote"), f"Configuration '{config['name']}' should use useExtendedRemote")
+ self.assertNotIn("cwd", config, f"Configuration '{config['name']}' should not set cwd in attach mode")
+ else:
+ self.fail(f"Configuration '{config['name']}' has unexpected request type: {config['request']}")
# Verify program path is absolute and exists conceptually
program = config["program"]
@@ -104,6 +104,12 @@ class DebuggerCrossConfig:
hex_port = "%04X" % self.debug_server_port
return "grep -q :%s /proc/net/tcp /proc/net/tcp6 2>/dev/null" % hex_port
+ def get_debug_server_ready_marker(self, port):
+ return "%s ready on port %s" % (self.DEBUG_SERVER_NAME, port)
+
+ def get_debug_server_ready_marker_pattern(self):
+ return "^%s$" % self.get_debug_server_ready_marker("[0-9]+")
+
def _target_wait_for_tcp_port_cmd(self, pid_var=None, log_file=None):
"""Shell fragment waiting until the debug server listens on its port.
@@ -120,6 +126,13 @@ class DebuggerCrossConfig:
% (self._target_tcp_port_check_cmd(), self.TARGET_START_RETRIES,
cleanup, self.DEBUG_SERVER_NAME, self.debug_server_port, dump_log))
+ def _target_wait_for_process_exit_cmd(self, pid_var):
+ return (
+ "_w=0; while kill -0 \\$_%s 2>/dev/null; do _w=\\$((_w+1)); "
+ "[ \\$_w -lt 100 ] || { echo %s did not stop >&2; exit 1; }; "
+ "sleep 0.1; done;"
+ % (pid_var, self.DEBUG_SERVER_NAME))
+
def initialize(self):
"""Called after construction to generate any required config files."""
pass
@@ -128,7 +141,7 @@ class DebuggerCrossConfig:
def _target_start_cmd(self, mode):
raise NotImplementedError
- def _target_kill_cmd(self):
+ def _target_stop_cmd(self, mode):
raise NotImplementedError
@@ -182,18 +195,19 @@ class GdbCrossConfig(DebuggerCrossConfig):
"\"/bin/sh -c '/usr/bin/gdbserver --once :1234 /usr/bin/cmake-example'\""
"""
if server_mode == DebuggerServerModes.ONCE:
- gdbserver_cmd_start = "%s --once :%s %s" % (
+ gdbserver_cmd_start = "mkdir -p %s; " % self._gdbserver_tmp_dir(server_mode)
+ gdbserver_cmd_start += "%s --once :%s %s & " % (
self.debugger_cross.debug_server_path, self.debug_server_port, self.binary.binary_path)
- elif server_mode == DebuggerServerModes.ATTACH:
- pid_command = self.binary.pid_command
- if pid_command:
- gdbserver_cmd_start = "%s --attach :%s \\$(%s)" % (
- self.debugger_cross.debug_server_path,
- self.debug_server_port,
- pid_command)
- else:
- raise DevtoolError("Cannot use gdbserver attach mode for binary %s. No PID found." % self.binary.binary_path)
- elif server_mode == DebuggerServerModes.MULTI:
+ gdbserver_cmd_start += "_gdbserver_pid=\\$!; "
+ gdbserver_cmd_start += "echo \\$_gdbserver_pid > %s; " % self._gdbserver_pid_file(server_mode)
+ gdbserver_cmd_start += self._target_wait_for_tcp_port_cmd(
+ "gdbserver_pid") + " "
+ gdbserver_cmd_start += "echo %s; wait \\$_gdbserver_pid" % (
+ self.get_debug_server_ready_marker(self.debug_server_port))
+ elif server_mode in (DebuggerServerModes.ATTACH, DebuggerServerModes.MULTI):
+ # Both modes run a persistent server speaking the extended-remote
+ # protocol. They differ on the client side only: ATTACH attaches to
+ # a process that is already running on the target.
gdbserver_cmd_start = self._target_tcp_port_check_cmd() + " && exit 0; "
gdbserver_cmd_start += "mkdir -p %s; " % self._gdbserver_tmp_dir(server_mode)
gdbserver_cmd_start += "%s --multi :%s > %s 2>&1 & _gdbserver_pid=\\$!; " % (
@@ -205,9 +219,21 @@ class GdbCrossConfig(DebuggerCrossConfig):
raise DevtoolError("Unsupported gdbserver mode: %s" % server_mode)
return "\"/bin/sh -c '" + gdbserver_cmd_start + "'\""
- def _target_kill_cmd(self):
- """SSH command to kill gdbserver on the target device."""
- return "\"kill \\$(pgrep -o -f 'gdbserver --attach :%s') 2>/dev/null || true\"" % self.debug_server_port
+ def _target_stop_cmd(self, server_mode):
+ """SSH command to stop gdbserver on the target device.
+
+ Stopping is based on the PID file written by the start command. Other
+ debug sessions run their own gdbserver on the target, so anything
+ matching by process name would hit them as well.
+ """
+ pid_file = self._gdbserver_pid_file(server_mode)
+ gdbserver_cmd_stop = "if test -f %s; then _gdbserver_pid=\\$(cat %s); " % (
+ pid_file, pid_file)
+ gdbserver_cmd_stop += "kill \\$_gdbserver_pid 2>/dev/null; "
+ gdbserver_cmd_stop += self._target_wait_for_process_exit_cmd(
+ "gdbserver_pid")
+ gdbserver_cmd_stop += " fi; rm -rf %s" % self._gdbserver_tmp_dir(server_mode)
+ return "\"/bin/sh -c '" + gdbserver_cmd_stop + "'\""
class LldbServerConfig(DebuggerCrossConfig):
@@ -244,10 +270,7 @@ class LldbServerConfig(DebuggerCrossConfig):
# lldb-server 21.x and the remote lldb client connects from the host.
# Start from /tmp because lldb-server creates temp files in its cwd and
# the SSH default cwd (/home/root) may not exist on a minimal image.
- if mode == DebuggerServerModes.ONCE:
- cmd = "cd /tmp && %s platform --one-shot --server --listen *:%s" % (
- lldb_server, self.debug_server_port)
- elif mode == DebuggerServerModes.MULTI:
+ if mode == DebuggerServerModes.MULTI:
pid_file = self._lldb_server_pid_file(mode)
tmp_dir = self._lldb_server_tmp_dir(mode)
log_file = self._lldb_server_log_file(mode)
@@ -261,11 +284,11 @@ class LldbServerConfig(DebuggerCrossConfig):
"lldb_server_pid", log_file)
else:
raise DevtoolError(
- "lldb-server does not support mode %s "
- "(ATTACH is handled client-side with 'process attach')" % mode)
+ "lldb-server only supports MULTI mode; "
+ "ATTACH is handled client-side with 'process attach': %s" % mode)
return "\"/bin/sh -c '" + cmd + "'\""
- def _target_kill_cmd(self):
+ def _target_stop_cmd(self, server_mode):
"""SSH command to stop a MULTI-mode lldb-server on the target."""
pid_file = self._lldb_server_pid_file(DebuggerServerModes.MULTI)
tmp_dir = self._lldb_server_tmp_dir(DebuggerServerModes.MULTI)
@@ -32,14 +32,12 @@ class GdbCrossConfigVSCode(GdbCrossConfig):
self._target_start_cmd(mode)
]
- def target_ssh_gdbserver_kill_args(self):
- """Get the ssh command arguments to kill gdbserver on the target device
-
- returns something like:
- ['-p', '2222', 'root@target', '"kill $(pgrep -o -f \'gdbserver --attach :1234\') 2>/dev/null || true"']
- """
+ def target_ssh_gdbserver_stop_args(self, mode=None):
+ """Get the ssh command arguments to stop gdbserver on the target device"""
+ if mode is None:
+ mode = self.default_mode
return self._target_ssh_args() + [
- self._target_kill_cmd()
+ self._target_stop_cmd(mode)
]
@@ -59,10 +57,12 @@ class LldbServerConfigVSCode(LldbServerConfig):
self._target_start_cmd(mode)
]
- def target_ssh_gdbserver_kill_args(self):
+ def target_ssh_gdbserver_stop_args(self, mode=None):
"""SSH argument list to stop a running MULTI-mode lldb-server"""
+ if mode is None:
+ mode = self.default_mode
return self._target_ssh_args() + [
- self._target_kill_cmd()
+ self._target_stop_cmd(mode)
]
class IdeVSCode(IdeBase):
@@ -338,25 +338,43 @@ class IdeVSCode(IdeBase):
return self._vscode_launch_bin_dbg_lldb(cross_debug_config, server_mode)
return self._vscode_launch_bin_dbg_gdb(cross_debug_config, server_mode)
+ @staticmethod
+ def _stop_task_label(cross_debug_config, server_mode):
+ return "stop_%s_%s" % (cross_debug_config.DEBUG_SERVER_NAME,
+ cross_debug_config.id_pretty_mode(server_mode))
+
def _vscode_launch_bin_dbg_gdb(self, cross_debug_config, server_mode):
"""Generate a cppdbg (GDB) launch configuration entry for launch.json."""
modified_recipe = cross_debug_config.modified_recipe
+ is_attach = server_mode == DebuggerServerModes.ATTACH
+
launch_config = {
"name": cross_debug_config.id_pretty_mode(server_mode),
"type": "cppdbg",
- "request": "launch",
+ "request": "attach" if is_attach else "launch",
"program": cross_debug_config.binary.binary_host_path,
- "stopAtEntry": True,
- "cwd": "${workspaceFolder}",
- "environment": [],
- "externalConsole": False,
"MIMode": "gdb",
"preLaunchTask": cross_debug_config.id_pretty_mode(server_mode),
"miDebuggerPath": modified_recipe.debugger_cross.gdb,
"miDebuggerServerAddress": "%s:%d" % (modified_recipe.debugger_cross.host, cross_debug_config.debug_server_port)
}
+ if is_attach:
+ # Without useExtendedRemote, cppdbg rejects attaching to a remote
+ # target. It also makes cppdbg offer a picker listing the processes
+ # running on the target, so the PID does not have to be known when
+ # this configuration is generated. Stopping the session detaches
+ # from the process instead of killing it.
+ launch_config["useExtendedRemote"] = True
+ else:
+ # cwd, environment and externalConsole configure the process the
+ # debugger starts, they are not part of the attach schema.
+ launch_config["cwd"] = "${workspaceFolder}"
+ launch_config["environment"] = []
+ launch_config["externalConsole"] = False
+ launch_config["stopAtEntry"] = True
+
# Search for header files in recipe-sysroot.
src_file_map = {
"/usr/include": os.path.join(modified_recipe.recipe_sysroot, "usr", "include")
@@ -415,10 +433,10 @@ class IdeVSCode(IdeBase):
launch_config['sourceFileMap'] = src_file_map
launch_config['setupCommands'] = setup_commands
- # Add postDebugTask for attach mode to clean up gdbserver
- if server_mode == DebuggerServerModes.ATTACH:
- kill_task_label = "kill_gdbserver_" + cross_debug_config.id_pretty_mode(server_mode)
- launch_config["postDebugTask"] = kill_task_label
+ if is_attach:
+ # The extended-remote server outlives the debug session
+ launch_config["postDebugTask"] = self._stop_task_label(
+ cross_debug_config, server_mode)
return launch_config
@@ -574,8 +592,9 @@ class IdeVSCode(IdeBase):
if cross_debug_config.modified_recipe is not modified_recipe:
continue
for server_mode in cross_debug_config.server_modes():
- if server_mode == DebuggerServerModes.MULTI:
- # MULTI mode: the SSH command blocks until the port is ready
+ if server_mode in (DebuggerServerModes.MULTI,
+ DebuggerServerModes.ATTACH):
+ # The SSH command blocks until the port is ready
# (wait loop in _target_start_cmd), so VSCode treats this as
# a regular non-background task.
new_task = {
@@ -586,7 +605,7 @@ class IdeVSCode(IdeBase):
"problemMatcher": []
}
else:
- # ONCE / ATTACH: gdbserver runs in the foreground for the
+ # ONCE: gdbserver runs in the foreground for the
# whole session, so VSCode needs isBackground + a pattern
# matcher to avoid waiting for the task to exit.
new_task = {
@@ -608,7 +627,7 @@ class IdeVSCode(IdeBase):
"background": {
"activeOnStart": True,
"beginsPattern": ".",
- "endsPattern": ".",
+ "endsPattern": cross_debug_config.get_debug_server_ready_marker_pattern(),
}
}
]
@@ -621,28 +640,20 @@ class IdeVSCode(IdeBase):
tasks_dict['tasks'].append(new_task)
- # For attach mode, add a kill task to stop a previously running gdbserver
- # This is a known issue with gdbserver --attach that it does not terminate
- # after detaching. With this helper task, it is possible to:
- # 1. Start debugging in attach mode
- # 2. Add breakpoints, step, continue, etc.
- # 3. Press the Continue button
- # 4. Press the Stop button which detaches gdbserver from the debugged process
- # 5. Start debugging again in attach mode
- # Without this kill task, step 5 would fail because gdbserver is still running
+ # The extended-remote server used by attach mode keeps running
+ # after the debug session, launch.json refers to this task as
+ # postDebugTask.
if server_mode == DebuggerServerModes.ATTACH:
- new_task_kill_label = "kill_gdbserver_"+ cross_debug_config.id_pretty_mode(server_mode)
- new_task_kill = {
- "label": new_task_kill_label,
+ tasks_dict['tasks'].append({
+ "label": self._stop_task_label(cross_debug_config, server_mode),
"type": "shell",
"command": cross_debug_config.debugger_cross.target_device.ssh_sshexec,
- "args": cross_debug_config.target_ssh_gdbserver_kill_args(),
+ "args": cross_debug_config.target_ssh_gdbserver_stop_args(server_mode),
"presentation": {
"close": True
},
"problemMatcher": []
- }
- tasks_dict['tasks'].append(new_task_kill)
+ })
tasks_file = 'tasks.json'
IdeBase.update_json_file(
@@ -804,8 +815,9 @@ class IdeVSCode(IdeBase):
if cross_debug_config.modified_recipe is not modified_recipe:
continue
for server_mode in cross_debug_config.server_modes():
- if server_mode == DebuggerServerModes.MULTI:
- # MULTI mode: SSH command blocks until port is ready, treat as
+ if server_mode in (DebuggerServerModes.MULTI,
+ DebuggerServerModes.ATTACH):
+ # SSH command blocks until port is ready, treat as
# a regular non-background task (same as vscode_tasks_cpp).
new_task = {
"label": cross_debug_config.id_pretty_mode(server_mode),
@@ -815,7 +827,7 @@ class IdeVSCode(IdeBase):
"problemMatcher": []
}
else:
- # ONCE / ATTACH: server runs for the whole session, needs
+ # ONCE: server runs for the whole session, needs
# isBackground so VSCode does not wait for the task to exit.
new_task = {
"label": cross_debug_config.id_pretty_mode(server_mode),
@@ -21,16 +21,19 @@ class GdbCrossConfigNone(GdbCrossConfig):
default_mode)
def _target_gdbserver_stop_cmd(self, server_mode):
- """Kill a gdbserver process"""
- # This is the usual behavior: gdbserver is stopped on demand
- if server_mode == DebuggerServerModes.MULTI:
- gdbserver_cmd_stop = "test -f %s && kill \\$(cat %s);" % (
- self._gdbserver_pid_file(server_mode), self._gdbserver_pid_file(server_mode))
- gdbserver_cmd_stop += " rm -rf %s" % self._gdbserver_tmp_dir(server_mode)
- # This is unexpected since gdbserver should terminate after each debug session
- # Just kill all gdbserver instances to keep it simple
- else:
- gdbserver_cmd_stop = "killall gdbserver"
+ """Kill a gdbserver process
+
+ Stopping is based on the PID file written by the start command. Other
+ debug sessions run their own gdbserver on the target, so anything
+ matching by process name would hit them as well.
+ """
+ pid_file = self._gdbserver_pid_file(server_mode)
+ gdbserver_cmd_stop = "if test -f %s; then _gdbserver_pid=\\$(cat %s); " % (
+ pid_file, pid_file)
+ gdbserver_cmd_stop += "kill \\$_gdbserver_pid 2>/dev/null; "
+ gdbserver_cmd_stop += self._target_wait_for_process_exit_cmd(
+ "gdbserver_pid")
+ gdbserver_cmd_stop += " fi; rm -rf %s" % self._gdbserver_tmp_dir(server_mode)
return "\"/bin/sh -c '" + gdbserver_cmd_stop + "'\""
def _gen_gdbserver_start_script(self, server_mode=None):
@@ -165,14 +168,19 @@ class LldbServerConfigNone(LldbServerConfig):
return os.path.join(self.script_dir, 'lldb_' + self.id_pretty)
def _target_lldb_server_stop_cmd(self, server_mode):
- """SSH command to stop lldb-server on the target."""
- if server_mode == DebuggerServerModes.MULTI:
- pid_file = self._lldb_server_pid_file(server_mode)
- tmp_dir = self._lldb_server_tmp_dir(server_mode)
- cmd = ("test -f %(pf)s && kill \\$(cat %(pf)s) 2>/dev/null; rm -rf %(td)s"
- % {'pf': pid_file, 'td': tmp_dir})
- else:
- cmd = "killall lldb-server 2>/dev/null || true"
+ """SSH command to stop lldb-server on the target.
+
+ Stopping is based on the PID file written by the start command. Other
+ debug sessions run their own lldb-server on the target, so anything
+ matching by process name would hit them as well.
+ """
+ pid_file = self._lldb_server_pid_file(server_mode)
+ tmp_dir = self._lldb_server_tmp_dir(server_mode)
+ cmd = "if test -f %s; then _lldb_server_pid=\\$(cat %s); " % (
+ pid_file, pid_file)
+ cmd += "kill \\$_lldb_server_pid 2>/dev/null; "
+ cmd += self._target_wait_for_process_exit_cmd("lldb_server_pid")
+ cmd += " fi; rm -rf %s" % tmp_dir
return "\"/bin/sh -c '" + cmd + "'\""
def _gen_lldb_server_start_script(self, server_mode=None):