Add --vscode-launch-props to gdbclient.py

The new argument allows the user to pass a JSON of properties to merge
into the generated launch.json config when setting up vscode-lldb
forwarding. This way the user can add pre-build tasks, extra init
commands etc.

Test: atest gdbclient_test
Test: lldbclient.py --setup-forwarding=vscode-lldb
  --vscode-launch-props='{"sourceMap": {"test1": "test2"},
  "postDebugTask": "Stop LLDB client", "processCreateCommands" :
  ["test"]}' -r test
Change-Id: I763dd15dde10421e86bc0a6ddfde974156ef1588
This commit is contained in:
Nikita Putikhin
2023-05-19 20:39:51 +00:00
parent 43e163e870
commit 9aa3bc65ba
4 changed files with 270 additions and 6 deletions

View File

@@ -0,0 +1,24 @@
// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package {
default_applicable_licenses: ["Android-Apache-2.0"],
}
python_library_host {
name: "gdbrunner",
srcs: [
"gdbrunner/__init__.py",
],
}

View File

@@ -59,3 +59,23 @@ python_test_host {
"pyfakefs",
]
}
python_test_host {
name: "gdbclient_test",
srcs: [
"gdbclient.py",
"gdbclient_test.py",
],
libs: [
"adb_py",
"gdbrunner",
],
test_options: {
unit_test: true,
},
version: {
py3: {
embedded_launcher: true,
},
},
}

View File

@@ -28,7 +28,7 @@ import sys
import tempfile
import textwrap
from typing import BinaryIO
from typing import BinaryIO, Any
# Shared functions across gdbclient.py and ndk-gdb.py.
import gdbrunner
@@ -98,8 +98,13 @@ def parse_args() -> argparse.Namespace:
choices=["lldb", "vscode-lldb"],
help=("Set up lldb-server and port forwarding. Prints commands or " +
".vscode/launch.json configuration needed to connect the debugging " +
"client to the server. 'vscode' with llbd and 'vscode-lldb' both " +
"client to the server. 'vscode' with lldb and 'vscode-lldb' both " +
"require the 'vadimcn.vscode-lldb' extension."))
parser.add_argument(
"--vscode-launch-props", default=None,
dest="vscode_launch_props",
help=("JSON with extra properties to add to launch parameters when using " +
"vscode-lldb forwarding."))
parser.add_argument(
"--env", nargs=1, action="append", metavar="VAR=VALUE",
@@ -244,7 +249,49 @@ def handle_switches(args, sysroot: str) -> tuple[BinaryIO, int | None, str | Non
return (binary_file, pid, run_cmd)
def generate_vscode_lldb_script(root: str, sysroot: str, binary_name: str, port: str | int, solib_search_path: list[str]) -> str:
def merge_launch_dict(base: dict[str, Any], to_add: dict[str, Any] | None) -> None:
"""Merges two dicts describing VSCode launch.json properties: base and
to_add. Base is modified in-place with items from to_add.
Items from to_add that are not present in base are inserted. Items that are
present are merged following these rules:
- Lists are merged with to_add elements appended to the end of base
list. Only a list can be merged with a list.
- dicts are merged recursively. Only a dict can be merged with a dict.
- Other present values in base get overwritten with values from to_add.
The reason for these rules is that merging in new values should prefer to
expand the existing set instead of overwriting where possible.
"""
if to_add is None:
return
for key, val in to_add.items():
if key not in base:
base[key] = val
else:
if isinstance(base[key], list) and not isinstance(val, list):
raise ValueError(f'Cannot merge non-list into list at key={key}. ' +
'You probably need to wrap your value into a list.')
if not isinstance(base[key], list) and isinstance(val, list):
raise ValueError(f'Cannot merge list into non-list at key={key}.')
if isinstance(base[key], dict) != isinstance(val, dict):
raise ValueError(f'Cannot merge dict and non-dict at key={key}')
# We don't allow the user to overwrite or interleave lists and don't allow
# to delete dict entries.
# It can be done but would make the implementation a bit more complicated
# and provides less value than adding elements.
# We expect that the config generated by gdbclient doesn't contain anything
# the user would want to remove.
if isinstance(base[key], list):
base[key] += val
elif isinstance(base[key], dict):
merge_launch_dict(base[key], val)
else:
base[key] = val
def generate_vscode_lldb_script(root: str, sysroot: str, binary_name: str, port: str | int, solib_search_path: list[str], extra_props: dict[str, Any] | None) -> str:
# TODO It would be nice if we didn't need to copy this or run the
# lldbclient.py program manually. Doing this would probably require
# writing a vscode extension or modifying an existing one.
@@ -262,6 +309,7 @@ def generate_vscode_lldb_script(root: str, sysroot: str, binary_name: str, port:
"target modules search-paths add / {}/".format(sysroot)],
"processCreateCommands": ["gdb-remote {}".format(str(port))]
}
merge_launch_dict(res, extra_props)
return json.dumps(res, indent=4)
def generate_lldb_script(root: str, sysroot: str, binary_name: str, port: str | int, solib_search_path: list[str]) -> str:
@@ -278,7 +326,7 @@ def generate_lldb_script(root: str, sysroot: str, binary_name: str, port: str |
return '\n'.join(commands)
def generate_setup_script(sysroot: str, linker_search_dir: str | None, binary_name: str, is64bit: bool, port: str | int, debugger: str) -> str:
def generate_setup_script(sysroot: str, linker_search_dir: str | None, binary_name: str, is64bit: bool, port: str | int, debugger: str, vscode_launch_props: dict[str, Any] | None) -> str:
# Generate a setup script.
root = os.environ["ANDROID_BUILD_TOP"]
symbols_dir = os.path.join(sysroot, "system", "lib64" if is64bit else "lib")
@@ -294,7 +342,7 @@ def generate_setup_script(sysroot: str, linker_search_dir: str | None, binary_na
if debugger == "vscode-lldb":
return generate_vscode_lldb_script(
root, sysroot, binary_name, port, solib_search_path)
root, sysroot, binary_name, port, solib_search_path, vscode_launch_props)
elif debugger == 'lldb':
return generate_lldb_script(
root, sysroot, binary_name, port, solib_search_path)
@@ -332,6 +380,12 @@ def do_main() -> None:
# Fetch binary for -p, -n.
binary_file, pid, run_cmd = handle_switches(args, sysroot)
vscode_launch_props = None
if args.vscode_launch_props:
if args.setup_forwarding != "vscode-lldb":
raise ValueError('vscode_launch_props requires --setup-forwarding=vscode-lldb')
vscode_launch_props = json.loads(args.vscode_launch_props)
with binary_file:
if sys.platform.startswith("linux"):
platform_name = "linux-x86"
@@ -381,7 +435,8 @@ def do_main() -> None:
binary_name=binary_file.name,
is64bit=is64bit,
port=args.port,
debugger=debugger)
debugger=debugger,
vscode_launch_props=vscode_launch_props)
if not args.setup_forwarding:
# Print a newline to separate our messages from the GDB session.

165
scripts/gdbclient_test.py Normal file
View File

@@ -0,0 +1,165 @@
#
# Copyright (C) 2023 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import gdbclient
import unittest
import copy
import json
from typing import Any
class LaunchConfigMergeTest(unittest.TestCase):
def merge_compare(self, base: dict[str, Any], to_add: dict[str, Any] | None, expected: dict[str, Any]) -> None:
actual = copy.deepcopy(base)
gdbclient.merge_launch_dict(actual, to_add)
self.assertEqual(actual, expected, f'base={base}, to_add={to_add}')
def test_add_none(self) -> None:
base = { 'foo' : 1 }
to_add = None
expected = { 'foo' : 1 }
self.merge_compare(base, to_add, expected)
def test_add_val(self) -> None:
base = { 'foo' : 1 }
to_add = { 'bar' : 2}
expected = { 'foo' : 1, 'bar' : 2 }
self.merge_compare(base, to_add, expected)
def test_overwrite_val(self) -> None:
base = { 'foo' : 1 }
to_add = { 'foo' : 2}
expected = { 'foo' : 2 }
self.merge_compare(base, to_add, expected)
def test_lists_get_appended(self) -> None:
base = { 'foo' : [1, 2] }
to_add = { 'foo' : [3, 4]}
expected = { 'foo' : [1, 2, 3, 4] }
self.merge_compare(base, to_add, expected)
def test_add_elem_to_dict(self) -> None:
base = { 'foo' : { 'bar' : 1 } }
to_add = { 'foo' : { 'baz' : 2 } }
expected = { 'foo' : { 'bar' : 1, 'baz' : 2 } }
self.merge_compare(base, to_add, expected)
def test_overwrite_elem_in_dict(self) -> None:
base = { 'foo' : { 'bar' : 1 } }
to_add = { 'foo' : { 'bar' : 2 } }
expected = { 'foo' : { 'bar' : 2 } }
self.merge_compare(base, to_add, expected)
def test_merging_dict_and_value_raises(self) -> None:
base = { 'foo' : { 'bar' : 1 } }
to_add = { 'foo' : 2 }
with self.assertRaises(ValueError):
gdbclient.merge_launch_dict(base, to_add)
def test_merging_value_and_dict_raises(self) -> None:
base = { 'foo' : 2 }
to_add = { 'foo' : { 'bar' : 1 } }
with self.assertRaises(ValueError):
gdbclient.merge_launch_dict(base, to_add)
def test_merging_dict_and_list_raises(self) -> None:
base = { 'foo' : { 'bar' : 1 } }
to_add = { 'foo' : [1] }
with self.assertRaises(ValueError):
gdbclient.merge_launch_dict(base, to_add)
def test_merging_list_and_dict_raises(self) -> None:
base = { 'foo' : [1] }
to_add = { 'foo' : { 'bar' : 1 } }
with self.assertRaises(ValueError):
gdbclient.merge_launch_dict(base, to_add)
def test_adding_elem_to_list_raises(self) -> None:
base = { 'foo' : [1] }
to_add = { 'foo' : 2}
with self.assertRaises(ValueError):
gdbclient.merge_launch_dict(base, to_add)
def test_adding_list_to_elem_raises(self) -> None:
base = { 'foo' : 1 }
to_add = { 'foo' : [2]}
with self.assertRaises(ValueError):
gdbclient.merge_launch_dict(base, to_add)
class VsCodeLaunchGeneratorTest(unittest.TestCase):
def setUp(self) -> None:
# These tests can generate long diffs, so we remove the limit
self.maxDiff = None
def test_generate_script(self) -> None:
self.assertEqual(json.loads(gdbclient.generate_vscode_lldb_script(root='/root',
sysroot='/sysroot',
binary_name='test',
port=123,
solib_search_path=['/path1',
'/path2'],
extra_props=None)),
{
'name': '(lldbclient.py) Attach test (port: 123)',
'type': 'lldb',
'request': 'custom',
'relativePathBase': '/root',
'sourceMap': { '/b/f/w' : '/root', '': '/root', '.': '/root' },
'initCommands': ['settings append target.exec-search-paths /path1 /path2'],
'targetCreateCommands': ['target create test',
'target modules search-paths add / /sysroot/'],
'processCreateCommands': ['gdb-remote 123']
})
def test_generate_script_with_extra_props(self) -> None:
extra_props = {
'initCommands' : ['settings append target.exec-search-paths /path3'],
'processCreateCommands' : ['break main', 'continue'],
'sourceMap' : { '/test/' : '/root/test'},
'preLaunchTask' : 'Build'
}
self.assertEqual(json.loads(gdbclient.generate_vscode_lldb_script(root='/root',
sysroot='/sysroot',
binary_name='test',
port=123,
solib_search_path=['/path1',
'/path2'],
extra_props=extra_props)),
{
'name': '(lldbclient.py) Attach test (port: 123)',
'type': 'lldb',
'request': 'custom',
'relativePathBase': '/root',
'sourceMap': { '/b/f/w' : '/root',
'': '/root',
'.': '/root',
'/test/' : '/root/test' },
'initCommands': [
'settings append target.exec-search-paths /path1 /path2',
'settings append target.exec-search-paths /path3',
],
'targetCreateCommands': ['target create test',
'target modules search-paths add / /sysroot/'],
'processCreateCommands': ['gdb-remote 123',
'break main',
'continue'],
'preLaunchTask' : 'Build'
})
if __name__ == '__main__':
unittest.main(verbosity=2)