Merge "Add --vscode-launch-file to gdbclient.py" into main

This commit is contained in:
Nikita Putikhin
2023-07-18 14:24:18 +00:00
committed by Gerrit Code Review
2 changed files with 339 additions and 19 deletions

View File

@@ -15,11 +15,11 @@
# limitations under the License. # limitations under the License.
# #
import adb
import argparse import argparse
import json import json
import logging import logging
import os import os
import pathlib
import posixpath import posixpath
import re import re
import shutil import shutil
@@ -27,14 +27,17 @@ import subprocess
import sys import sys
import tempfile import tempfile
import textwrap import textwrap
from typing import Any, BinaryIO
from typing import BinaryIO, Any import adb
# Shared functions across gdbclient.py and ndk-gdb.py. # Shared functions across gdbclient.py and ndk-gdb.py.
import gdbrunner import gdbrunner
g_temp_dirs = [] 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: def read_toolchain_config(root: str) -> str:
"""Finds out current toolchain version.""" """Finds out current toolchain version."""
@@ -106,6 +109,14 @@ def parse_args() -> argparse.Namespace:
dest="vscode_launch_props", dest="vscode_launch_props",
help=("JSON with extra properties to add to launch parameters when using " + help=("JSON with extra properties to add to launch parameters when using " +
"vscode-lldb forwarding.")) "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( parser.add_argument(
"--env", nargs=1, action="append", metavar="VAR=VALUE", "--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) 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: def do_main() -> None:
required_env = ["ANDROID_BUILD_TOP", required_env = ["ANDROID_BUILD_TOP",
"ANDROID_PRODUCT_OUT", "TARGET_PRODUCT"] "ANDROID_PRODUCT_OUT", "TARGET_PRODUCT"]
@@ -384,9 +472,17 @@ def do_main() -> None:
vscode_launch_props = None vscode_launch_props = None
if args.vscode_launch_props: if args.vscode_launch_props:
if args.setup_forwarding != "vscode-lldb": 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_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: with binary_file:
if sys.platform.startswith("linux"): if sys.platform.startswith("linux"):
platform_name = "linux-x86" platform_name = "linux-x86"
@@ -446,19 +542,25 @@ def do_main() -> None:
# Start lldb. # Start lldb.
gdbrunner.start_gdb(debugger_path, setup_commands, lldb=True) gdbrunner.start_gdb(debugger_path, setup_commands, lldb=True)
else: else:
print("") if args.setup_forwarding == "vscode-lldb" and vscode_launch_file:
print(setup_commands) write_vscode_config(pathlib.Path(vscode_launch_file) , setup_commands)
print("") print(f"Generated config written to '{vscode_launch_file}'")
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."""))
else: else:
print(textwrap.dedent(""" print("")
Paste the lldb commands above into the lldb frontend to set up the print(setup_commands)
lldb-server connection. Press enter in this terminal once debugging is print("")
finished to shut lldb-server down and close all the ports.""")) 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("") print("")
input("Press enter to shut down lldb-server") input("Press enter to shut down lldb-server")

View File

@@ -14,13 +14,15 @@
# limitations under the License. # limitations under the License.
# #
import gdbclient
import unittest
import copy import copy
import json import json
import textwrap
import unittest
from typing import Any from typing import Any
import gdbclient
class LaunchConfigMergeTest(unittest.TestCase): class LaunchConfigMergeTest(unittest.TestCase):
def merge_compare(self, base: dict[str, Any], to_add: dict[str, Any] | None, expected: dict[str, Any]) -> None: def merge_compare(self, base: dict[str, Any], to_add: dict[str, Any] | None, expected: dict[str, Any]) -> None:
actual = copy.deepcopy(base) 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__': if __name__ == '__main__':
unittest.main(verbosity=2) unittest.main(verbosity=2)