From 7da179ef34e139a1f14f9ba3c9a22d3109bf5606 Mon Sep 17 00:00:00 2001 From: Nikita Putikhin Date: Mon, 3 Jul 2023 07:37:58 +0000 Subject: [PATCH] Add --vscode-launch-file to gdbclient.py The new argument makes the generator write the VSCode launch.json config into a file instead of stdout. The generator uses marker lines to insert the config. This way the user can control where in the file the launch config is written. Test: atest gdbclient_test Test: lldbclient.py --setup-forwarding vscode-lldb \ --vscode-launch-props= \ '{"internalConsoleOptions" : "openOnSessionStart"}' \ --vscode-launch-file=.vscode/launch.json -r test Change-Id: I92b3f479b5ebcb722933938f52d0f23ff098beac --- scripts/gdbclient.py | 134 ++++++++++++++++++++--- scripts/gdbclient_test.py | 224 +++++++++++++++++++++++++++++++++++++- 2 files changed, 339 insertions(+), 19 deletions(-) diff --git a/scripts/gdbclient.py b/scripts/gdbclient.py index b053a489d..f99427837 100755 --- a/scripts/gdbclient.py +++ b/scripts/gdbclient.py @@ -15,11 +15,11 @@ # limitations under the License. # -import adb import argparse import json import logging import os +import pathlib import posixpath import re import shutil @@ -27,14 +27,17 @@ import subprocess import sys import tempfile import textwrap +from typing import Any, BinaryIO -from typing import BinaryIO, Any - +import adb # Shared functions across gdbclient.py and ndk-gdb.py. import gdbrunner g_temp_dirs = [] +g_vscode_config_marker_begin = '// #lldbclient-generated-begin' +g_vscode_config_marker_end = '// #lldbclient-generated-end' + def read_toolchain_config(root: str) -> str: """Finds out current toolchain version.""" @@ -106,6 +109,14 @@ def parse_args() -> argparse.Namespace: dest="vscode_launch_props", help=("JSON with extra properties to add to launch parameters when using " + "vscode-lldb forwarding.")) + parser.add_argument( + "--vscode-launch-file", default=None, + dest="vscode_launch_file", + help=textwrap.dedent(f"""Path to .vscode/launch.json file for the generated launch + config when using vscode-lldb forwarding. The file needs to + contain two marker lines: '{g_vscode_config_marker_begin}' + and '{g_vscode_config_marker_end}'. The config will be written inline + between these lines, replacing any text that is already there.""")) parser.add_argument( "--env", nargs=1, action="append", metavar="VAR=VALUE", @@ -351,6 +362,83 @@ def generate_setup_script(sysroot: str, linker_search_dir: str | None, binary_na raise Exception("Unknown debugger type " + debugger) +def insert_commands_into_vscode_config(dst_launch_config: str, setup_commands: str) -> str: + """Inserts setup commands into launch config between two marker lines. + Marker lines are set in global variables g_vscode_config_marker_end and g_vscode_config_marker_end. + The commands are inserted with the same indentation as the first marker line. + + Args: + dst_launch_config: Config to insert commands into. + setup_commands: Commands to insert. + Returns: + Config with inserted commands. + Raises: + ValueError if the begin marker is not found or not terminated with an end marker. + """ + + # We expect the files to be small (~10s KB), so we use simple string concatenation + # for simplicity and readability even if it is slower. + output = "" + found_at_least_one_begin = False + unterminated_begin_line = None + + # It might be tempting to rewrite this using find() or even regexes, + # but keeping track of line numbers, preserving whitespace, and detecting indent + # becomes tricky enough that this simple loop is more clear. + for linenum, line in enumerate(dst_launch_config.splitlines(keepends=True), start=1): + if unterminated_begin_line != None: + if line.strip() == g_vscode_config_marker_end: + unterminated_begin_line = None + else: + continue + output += line + if line.strip() == g_vscode_config_marker_begin: + found_at_least_one_begin = True + unterminated_begin_line = linenum + marker_indent = line[:line.find(g_vscode_config_marker_begin)] + output += textwrap.indent(setup_commands, marker_indent) + '\n' + + if not found_at_least_one_begin: + raise ValueError(f"Did not find begin marker line '{g_vscode_config_marker_begin}' " + + "in the VSCode launch file") + + if unterminated_begin_line is not None: + raise ValueError(f"Unterminated begin marker at line {unterminated_begin_line} " + + f"in the VSCode launch file. Add end marker line to file: '{g_vscode_config_marker_end}'") + + return output + + +def replace_file_contents(dst_path: os.PathLike, contents: str) -> None: + """Replaces the contents of the file pointed to by dst_path. + + This function writes the new contents into a temporary file, then atomically swaps it with + the target file. This way if a write fails, the original file is not overwritten. + + Args: + dst_path: The path to the file to be replaced. + contents: The new contents of the file. + Raises: + Forwards exceptions from underlying filesystem methods. + """ + tempf = tempfile.NamedTemporaryFile('w', delete=False) + try: + tempf.write(contents) + os.replace(tempf.name, dst_path) + except: + os.remove(tempf.name) + raise + + +def write_vscode_config(vscode_launch_file: pathlib.Path, setup_commands: str) -> None: + """Writes setup_commands into the file pointed by vscode_launch_file. + + See insert_commands_into_vscode_config for the description of how the setup commands are written. + """ + contents = insert_commands_into_vscode_config(vscode_launch_file.read_text(), setup_commands) + replace_file_contents(vscode_launch_file, contents) + + def do_main() -> None: required_env = ["ANDROID_BUILD_TOP", "ANDROID_PRODUCT_OUT", "TARGET_PRODUCT"] @@ -384,9 +472,17 @@ def do_main() -> None: 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') + raise ValueError( + 'vscode-launch-props requires --setup-forwarding=vscode-lldb') vscode_launch_props = json.loads(args.vscode_launch_props) + vscode_launch_file = None + if args.vscode_launch_file: + if args.setup_forwarding != "vscode-lldb": + raise ValueError( + 'vscode-launch-file requires --setup-forwarding=vscode-lldb') + vscode_launch_file = args.vscode_launch_file + with binary_file: if sys.platform.startswith("linux"): platform_name = "linux-x86" @@ -446,19 +542,25 @@ def do_main() -> None: # Start lldb. gdbrunner.start_gdb(debugger_path, setup_commands, lldb=True) else: - print("") - print(setup_commands) - print("") - if args.setup_forwarding == "vscode-lldb": - print(textwrap.dedent(""" - Paste the above json into .vscode/launch.json and start the debugger as - normal. Press enter in this terminal once debugging is finished to shut - lldb-server down and close all the ports.""")) + if args.setup_forwarding == "vscode-lldb" and vscode_launch_file: + write_vscode_config(pathlib.Path(vscode_launch_file) , setup_commands) + print(f"Generated config written to '{vscode_launch_file}'") else: - print(textwrap.dedent(""" - Paste the lldb commands above into the lldb frontend to set up the - lldb-server connection. Press enter in this terminal once debugging is - finished to shut lldb-server down and close all the ports.""")) + print("") + print(setup_commands) + print("") + if args.setup_forwarding == "vscode-lldb": + print(textwrap.dedent(""" + Paste the above json into .vscode/launch.json and start the debugger as + normal.""")) + else: + print(textwrap.dedent(""" + Paste the lldb commands above into the lldb frontend to set up the + lldb-server connection.""")) + + print(textwrap.dedent(""" + Press enter in this terminal once debugging is finished to shut lldb-server + down and close all the ports.""")) print("") input("Press enter to shut down lldb-server") diff --git a/scripts/gdbclient_test.py b/scripts/gdbclient_test.py index ef61bc958..999bf74b0 100644 --- a/scripts/gdbclient_test.py +++ b/scripts/gdbclient_test.py @@ -14,13 +14,15 @@ # limitations under the License. # -import gdbclient -import unittest import copy import json - +import textwrap +import unittest from typing import Any +import gdbclient + + 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) @@ -161,5 +163,221 @@ class VsCodeLaunchGeneratorTest(unittest.TestCase): }) +class LaunchConfigInsertTest(unittest.TestCase): + def setUp(self) -> None: + # These tests can generate long diffs, so we remove the limit + self.maxDiff = None + + def test_insert_config(self) -> None: + dst = textwrap.dedent("""\ + // #lldbclient-generated-begin + // #lldbclient-generated-end""") + to_insert = textwrap.dedent("""\ + foo + bar""") + self.assertEqual(gdbclient.insert_commands_into_vscode_config(dst, + to_insert), + textwrap.dedent("""\ + // #lldbclient-generated-begin + foo + bar + // #lldbclient-generated-end""")) + + def test_insert_into_start(self) -> None: + dst = textwrap.dedent("""\ + // #lldbclient-generated-begin + // #lldbclient-generated-end + more content""") + to_insert = textwrap.dedent("""\ + foo + bar""") + self.assertEqual(gdbclient.insert_commands_into_vscode_config(dst, + to_insert), + textwrap.dedent("""\ + // #lldbclient-generated-begin + foo + bar + // #lldbclient-generated-end + more content""")) + + def test_insert_into_mid(self) -> None: + dst = textwrap.dedent("""\ + start content + // #lldbclient-generated-begin + // #lldbclient-generated-end + more content""") + to_insert = textwrap.dedent("""\ + foo + bar""") + self.assertEqual(gdbclient.insert_commands_into_vscode_config(dst, + to_insert), + textwrap.dedent("""\ + start content + // #lldbclient-generated-begin + foo + bar + // #lldbclient-generated-end + more content""")) + + def test_insert_into_end(self) -> None: + dst = textwrap.dedent("""\ + start content + // #lldbclient-generated-begin + // #lldbclient-generated-end""") + to_insert = textwrap.dedent("""\ + foo + bar""") + self.assertEqual(gdbclient.insert_commands_into_vscode_config(dst, + to_insert), + textwrap.dedent("""\ + start content + // #lldbclient-generated-begin + foo + bar + // #lldbclient-generated-end""")) + + def test_insert_twice(self) -> None: + dst = textwrap.dedent("""\ + // #lldbclient-generated-begin + // #lldbclient-generated-end + // #lldbclient-generated-begin + // #lldbclient-generated-end + """) + to_insert = 'foo' + self.assertEqual(gdbclient.insert_commands_into_vscode_config(dst, + to_insert), + textwrap.dedent("""\ + // #lldbclient-generated-begin + foo + // #lldbclient-generated-end + // #lldbclient-generated-begin + foo + // #lldbclient-generated-end + """)) + + def test_preserve_space_indent(self) -> None: + dst = textwrap.dedent("""\ + { + "version": "0.2.0", + "configurations": [ + // #lldbclient-generated-begin + // #lldbclient-generated-end + ] + } + """) + to_insert = textwrap.dedent("""\ + { + "name": "(lldbclient.py) Attach test", + "type": "lldb", + "processCreateCommands": [ + "gdb-remote 123", + "test" + ] + }""") + self.assertEqual(gdbclient.insert_commands_into_vscode_config(dst, + to_insert), + textwrap.dedent("""\ + { + "version": "0.2.0", + "configurations": [ + // #lldbclient-generated-begin + { + "name": "(lldbclient.py) Attach test", + "type": "lldb", + "processCreateCommands": [ + "gdb-remote 123", + "test" + ] + } + // #lldbclient-generated-end + ] + } + """)) + + def test_preserve_tab_indent(self) -> None: + dst = textwrap.dedent("""\ + { + \t"version": "0.2.0", + \t"configurations": [ + \t\t// #lldbclient-generated-begin + \t\t// #lldbclient-generated-end + \t] + } + """) + to_insert = textwrap.dedent("""\ + { + \t"name": "(lldbclient.py) Attach test", + \t"type": "lldb", + \t"processCreateCommands": [ + \t\t"gdb-remote 123", + \t\t"test" + \t] + }""") + self.assertEqual(gdbclient.insert_commands_into_vscode_config(dst, + to_insert), + textwrap.dedent("""\ + { + \t"version": "0.2.0", + \t"configurations": [ + \t\t// #lldbclient-generated-begin + \t\t{ + \t\t\t"name": "(lldbclient.py) Attach test", + \t\t\t"type": "lldb", + \t\t\t"processCreateCommands": [ + \t\t\t\t"gdb-remote 123", + \t\t\t\t"test" + \t\t\t] + \t\t} + \t\t// #lldbclient-generated-end + \t] + } + """)) + + def test_preserve_trailing_whitespace(self) -> None: + dst = textwrap.dedent("""\ + // #lldbclient-generated-begin \t + // #lldbclient-generated-end\t """) + to_insert = 'foo' + self.assertEqual(gdbclient.insert_commands_into_vscode_config(dst, + to_insert), + textwrap.dedent("""\ + // #lldbclient-generated-begin \t + foo + // #lldbclient-generated-end\t """)) + + def test_fail_if_no_begin(self) -> None: + dst = textwrap.dedent("""\ + // #lldbclient-generated-end""") + with self.assertRaisesRegex(ValueError, 'Did not find begin marker line'): + gdbclient.insert_commands_into_vscode_config(dst, 'foo') + + def test_fail_if_no_end(self) -> None: + dst = textwrap.dedent("""\ + // #lldbclient-generated-begin""") + with self.assertRaisesRegex(ValueError, 'Unterminated begin marker at line 1'): + gdbclient.insert_commands_into_vscode_config(dst, 'foo') + + def test_fail_if_begin_has_extra_text(self) -> None: + dst = textwrap.dedent("""\ + // #lldbclient-generated-begin text + // #lldbclient-generated-end""") + with self.assertRaisesRegex(ValueError, 'Did not find begin marker line'): + gdbclient.insert_commands_into_vscode_config(dst, 'foo') + + def test_fail_if_end_has_extra_text(self) -> None: + dst = textwrap.dedent("""\ + // #lldbclient-generated-begin + // #lldbclient-generated-end text""") + with self.assertRaisesRegex(ValueError, 'Unterminated begin marker at line 1'): + gdbclient.insert_commands_into_vscode_config(dst, 'foo') + + def test_fail_if_begin_end_swapped(self) -> None: + dst = textwrap.dedent("""\ + // #lldbclient-generated-end + // #lldbclient-generated-begin""") + with self.assertRaisesRegex(ValueError, 'Unterminated begin marker at line 2'): + gdbclient.insert_commands_into_vscode_config(dst, 'foo') + + if __name__ == '__main__': unittest.main(verbosity=2)