gdbclient.py alternative frontend support

This adds a new --setup-forwarding {gdb|vscode} flag that will make
gdbclient.py (1) setup the port forwarding needed to connect to a
remote process and (2) print the commands/configuration needed to use
the frontend of your choice to run the debugger. Currently it supports
gdb and visual-studio code. In order to avoid using up all the ports
the gdbclient.py program will not exit until directed by user input,
holding the ports open until this occurs.

To use:

```
% ./development/scripts/gdbclient.py -n com.android.contacts --setup-forwarding vscode
Redirecting gdbserver output to /tmp/gdbclient.log

{
    "miDebuggerPath": "/fast-2/allight/aosp/prebuilts/gdb/linux-x86/bin/gdb",
    "program": "/fast-2/allight/aosp/out/target/product/walleye/symbols/system/bin/app_process64",
    "setupCommands": [
        {
            "text": "-enable-pretty-printing",
            "description": "Enable pretty-printing for gdb",
            "ignoreFailures": true
        },
        {
            "text": "-environment-directory /fast-2/allight/aosp",
            "description": "gdb command: dir",
            "ignoreFailures": false
        },
        {
            "text": "-gdb-set solib-search-path /fast-2/allight/aosp/out/target/product/walleye/symbols/system/lib64/:/fast-2/allight/aosp/out/target/product/walleye/symbols/system/lib64/hw:/fast-2/allight/aosp/out/target/product/walleye/symbols/system/lib64/ssl/engines:/fast-2/allight/aosp/out/target/product/walleye/symbols/system/lib64/drm:/fast-2/allight/aosp/out/target/product/walleye/symbols/system/lib64/egl:/fast-2/allight/aosp/out/target/product/walleye/symbols/system/lib64/soundfx:/fast-2/allight/aosp/out/target/product/walleye/symbols/vendor/lib64/:/fast-2/allight/aosp/out/target/product/walleye/symbols/vendor/lib64/hw:/fast-2/allight/aosp/out/target/product/walleye/symbols/vendor/lib64/egl",
            "description": "gdb command: set solib-search-path",
            "ignoreFailures": false
        },
        {
            "text": "-gdb-set solib-absolute-prefix /fast-2/allight/aosp/out/target/product/walleye/symbols",
            "description": "gdb command: set solib-absolute-prefix",
            "ignoreFailures": false
        },
        {
            "text": "-interpreter-exec console \"source /fast-2/allight/aosp/development/scripts/gdb/dalvik.gdb\"",
            "description": "gdb command: source art commands",
            "ignoreFailures": false
        }
    ],
    "name": "(gdbclient.py) Attach app_process64 (port: 5039)",
    "miDebuggerServerAddress": "localhost:5039",
    "request": "launch",
    "type": "cppdbg",
    "cwd": "/fast-2/allight/aosp",
    "MIMode": "gdb"
}

Paste the above json into .vscode/launch.json and start the debugger as
normal. Press enter in this terminal once debugging is finished to shutdown
the gdbserver and close all the ports.

Press enter to shutdown gdbserver
```

Or for gdb:

```
% ./development/scripts/gdbclient.py -n com.android.contacts --setup-forwarding gdb
Redirecting gdbserver output to /tmp/gdbclient.log

file '/fast-2/allight/aosp/out/target/product/walleye/symbols/system/bin/app_process64'
directory '/fast-2/allight/aosp'
set solib-absolute-prefix /fast-2/allight/aosp/out/target/product/walleye/symbols
set solib-search-path /fast-2/allight/aosp/out/target/product/walleye/symbols/system/lib64/:/fast-2/allight/aosp/out/target/product/walleye/symbols/system/lib64/hw:/fast-2/allight/aosp/out/target/product/walleye/symbols/system/lib64/ssl/engines:/fast-2/allight/aosp/out/target/product/walleye/symbols/system/lib64/drm:/fast-2/allight/aosp/out/target/product/walleye/symbols/system/lib64/egl:/fast-2/allight/aosp/out/target/product/walleye/symbols/system/lib64/soundfx:/fast-2/allight/aosp/out/target/product/walleye/symbols/vendor/lib64/:/fast-2/allight/aosp/out/target/product/walleye/symbols/vendor/lib64/hw:/fast-2/allight/aosp/out/target/product/walleye/symbols/vendor/lib64/egl
source /fast-2/allight/aosp/development/scripts/gdb/dalvik.gdb

python

def target_remote_with_retry(target, timeout_seconds):
  import time
  end_time = time.time() + timeout_seconds
  while True:
    try:
      gdb.execute("target extended-remote " + target)
      return True
    except gdb.error as e:
      time_left = end_time - time.time()
      if time_left < 0 or time_left > timeout_seconds:
        print("Error: unable to connect to device.")
        print(e)
        return False
      time.sleep(min(0.25, time_left))

target_remote_with_retry(':5039', 5)

end

Paste the above gdb commands into the gdb frontend to setup the gdbserver
connection. Press enter in this terminal once debugging is finished to
shutdown the gdbserver and close all the ports.

Press enter to shutdown gdbserver
```

Test: ./development/scripts/gdbclient.py -n com.android.contacts --setup-forwarding vscode
      Perform debugging.

Change-Id: Ifa105cfbb100c4ba872b85c1c609d49a4f194d8b
diff --git a/scripts/gdbclient.py b/scripts/gdbclient.py
index ff0ee40..611b2e8 100755
--- a/scripts/gdbclient.py
+++ b/scripts/gdbclient.py
@@ -17,11 +17,13 @@
 
 import adb
 import argparse
+import json
 import logging
 import os
 import re
 import subprocess
 import sys
+import textwrap
 
 # Shared functions across gdbclient.py and ndk-gdb.py.
 import gdbrunner
@@ -64,6 +66,11 @@
     parser.add_argument(
         "--user", nargs="?", default="root",
         help="user to run commands as on the device [default: root]")
+    parser.add_argument(
+        "--setup-forwarding", default=None, choices=["gdb", "vscode"],
+        help=("Setup the gdbserver and port forwarding. Prints commands or " +
+              ".vscode/launch.json configuration needed to connect the debugging " +
+              "client to the server."))
 
     return parser.parse_args()
 
@@ -153,33 +160,60 @@
 
     return (binary_file, pid, run_cmd)
 
+def generate_vscode_script(gdbpath, root, sysroot, binary_name, port, dalvik_gdb_script, solib_search_path):
+    # TODO It would be nice if we didn't need to copy this or run the
+    #      gdbclient.py program manually. Doing this would probably require
+    #      writing a vscode extension or modifying an existing one.
+    res = {
+        "name": "(gdbclient.py) Attach {} (port: {})".format(binary_name.split("/")[-1], port),
+        "type": "cppdbg",
+        "request": "launch",  # Needed for gdbserver.
+        "cwd": root,
+        "program": binary_name,
+        "MIMode": "gdb",
+        "miDebuggerServerAddress": "localhost:{}".format(port),
+        "miDebuggerPath": gdbpath,
+        "setupCommands": [
+            {
+                # Required for vscode.
+                "description": "Enable pretty-printing for gdb",
+                "text": "-enable-pretty-printing",
+                "ignoreFailures": True,
+            },
+            {
+                "description": "gdb command: dir",
+                "text": "-environment-directory {}".format(root),
+                "ignoreFailures": False
+            },
+            {
+                "description": "gdb command: set solib-search-path",
+                "text": "-gdb-set solib-search-path {}".format(":".join(solib_search_path)),
+                "ignoreFailures": False
+            },
+            {
+                "description": "gdb command: set solib-absolute-prefix",
+                "text": "-gdb-set solib-absolute-prefix {}".format(sysroot),
+                "ignoreFailures": False
+            },
+        ]
+    }
+    if dalvik_gdb_script:
+        res["setupCommands"].append({
+            "description": "gdb command: source art commands",
+            "text": "-interpreter-exec console \"source {}\"".format(dalvik_gdb_script),
+            "ignoreFailures": False,
+        })
+    return json.dumps(res, indent=4)
 
-def generate_gdb_script(sysroot, binary_file, is64bit, port, connect_timeout=5):
-    # Generate a gdb script.
-    # TODO: Detect the zygote and run 'art-on' automatically.
-    root = os.environ["ANDROID_BUILD_TOP"]
-    symbols_dir = os.path.join(sysroot, "system", "lib64" if is64bit else "lib")
-    vendor_dir = os.path.join(sysroot, "vendor", "lib64" if is64bit else "lib")
-
-    solib_search_path = []
-    symbols_paths = ["", "hw", "ssl/engines", "drm", "egl", "soundfx"]
-    vendor_paths = ["", "hw", "egl"]
-    solib_search_path += [os.path.join(symbols_dir, x) for x in symbols_paths]
-    solib_search_path += [os.path.join(vendor_dir, x) for x in vendor_paths]
+def generate_gdb_script(root, sysroot, binary_name, port, dalvik_gdb_script, solib_search_path, connect_timeout):
     solib_search_path = ":".join(solib_search_path)
 
     gdb_commands = ""
-    gdb_commands += "file '{}'\n".format(binary_file.name)
+    gdb_commands += "file '{}'\n".format(binary_name)
     gdb_commands += "directory '{}'\n".format(root)
     gdb_commands += "set solib-absolute-prefix {}\n".format(sysroot)
     gdb_commands += "set solib-search-path {}\n".format(solib_search_path)
-
-    dalvik_gdb_script = os.path.join(root, "development", "scripts", "gdb",
-                                     "dalvik.gdb")
-    if not os.path.exists(dalvik_gdb_script):
-        logging.warning(("couldn't find {} - ART debugging options will not " +
-                         "be available").format(dalvik_gdb_script))
-    else:
+    if dalvik_gdb_script:
         gdb_commands += "source {}\n".format(dalvik_gdb_script)
 
     # Try to connect for a few seconds, sometimes the device gdbserver takes
@@ -209,6 +243,33 @@
 
     return gdb_commands
 
+def generate_setup_script(gdbpath, sysroot, binary_file, is64bit, port, debugger, connect_timeout=5):
+    # Generate a setup script.
+    # TODO: Detect the zygote and run 'art-on' automatically.
+    root = os.environ["ANDROID_BUILD_TOP"]
+    symbols_dir = os.path.join(sysroot, "system", "lib64" if is64bit else "lib")
+    vendor_dir = os.path.join(sysroot, "vendor", "lib64" if is64bit else "lib")
+
+    solib_search_path = []
+    symbols_paths = ["", "hw", "ssl/engines", "drm", "egl", "soundfx"]
+    vendor_paths = ["", "hw", "egl"]
+    solib_search_path += [os.path.join(symbols_dir, x) for x in symbols_paths]
+    solib_search_path += [os.path.join(vendor_dir, x) for x in vendor_paths]
+
+    dalvik_gdb_script = os.path.join(root, "development", "scripts", "gdb", "dalvik.gdb")
+    if not os.path.exists(dalvik_gdb_script):
+        logging.warning(("couldn't find {} - ART debugging options will not " +
+                         "be available").format(dalvik_gdb_script))
+        dalvik_gdb_script = None
+
+    if debugger == "vscode":
+        return generate_vscode_script(
+            gdbpath, root, sysroot, binary_file.name, port, dalvik_gdb_script, solib_search_path)
+    elif debugger == "gdb":
+        return generate_gdb_script(root, sysroot, binary_file.name, port, dalvik_gdb_script, solib_search_path, connect_timeout)
+    else:
+        raise Exception("Unknown debugger type " + debugger)
+
 
 def main():
     required_env = ["ANDROID_BUILD_TOP",
@@ -258,12 +319,6 @@
             gdbrunner.forward_gdbserver_port(device, local=args.port,
                                              remote="tcp:{}".format(args.port))
 
-        # Generate a gdb script.
-        gdb_commands = generate_gdb_script(sysroot=sysroot,
-                                           binary_file=binary_file,
-                                           is64bit=is64bit,
-                                           port=args.port)
-
         # Find where gdb is
         if sys.platform.startswith("linux"):
             platform_name = "linux-x86"
@@ -271,14 +326,39 @@
             platform_name = "darwin-x86"
         else:
             sys.exit("Unknown platform: {}".format(sys.platform))
+
         gdb_path = os.path.join(root, "prebuilts", "gdb", platform_name, "bin",
                                 "gdb")
+        # Generate a gdb script.
+        setup_commands = generate_setup_script(gdbpath=gdb_path,
+                                               sysroot=sysroot,
+                                               binary_file=binary_file,
+                                               is64bit=is64bit,
+                                               port=args.port,
+                                               debugger=args.setup_forwarding or "gdb")
 
-        # Print a newline to separate our messages from the GDB session.
-        print("")
+        if not args.setup_forwarding:
+            # Print a newline to separate our messages from the GDB session.
+            print("")
 
-        # Start gdb.
-        gdbrunner.start_gdb(gdb_path, gdb_commands)
+            # Start gdb.
+            gdbrunner.start_gdb(gdb_path, setup_commands)
+        else:
+            print("")
+            print setup_commands
+            print("")
+            if args.setup_forwarding == "vscode":
+                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 shutdown
+                        the gdbserver and close all the ports.""")
+            else:
+                print textwrap.dedent("""
+                        Paste the above gdb commands into the gdb frontend to setup the gdbserver
+                        connection. Press enter in this terminal once debugging is finished to
+                        shutdown the gdbserver and close all the ports.""")
+            print("")
+            raw_input("Press enter to shutdown gdbserver")
 
 if __name__ == "__main__":
     main()