| 1 | #!/usr/bin/env python3 |
| 2 | # |
| 3 | # Run a gdbstub test case |
| 4 | # |
| 5 | # Copyright (c) 2019 Linaro |
| 6 | # |
| 7 | # Author: Alex Bennée <alex.bennee@linaro.org> |
| 8 | # |
| 9 | # This work is licensed under the terms of the GNU GPL, version 2 or later. |
| 10 | # See the COPYING file in the top-level directory. |
| 11 | # |
| 12 | # SPDX-License-Identifier: GPL-2.0-or-later |
| 13 | |
| 14 | import argparse |
| 15 | import subprocess |
| 16 | import shutil |
| 17 | import shlex |
| 18 | import os |
| 19 | from time import sleep |
| 20 | from tempfile import TemporaryDirectory |
| 21 | |
| 22 | def get_args(): |
| 23 | parser = argparse.ArgumentParser(description="A gdbstub test runner") |
| 24 | parser.add_argument("--qemu", help="Qemu binary for test", |
| 25 | required=True) |
| 26 | parser.add_argument("--qargs", help="Qemu arguments for test") |
| 27 | parser.add_argument("--binary", help="Binary to debug", |
| 28 | required=True) |
| 29 | parser.add_argument("--test", help="GDB test script") |
| 30 | parser.add_argument('test_args', nargs='*', |
| 31 | help="Additional args for GDB test script. " |
| 32 | "The args should be preceded by -- to avoid confusion " |
| 33 | "with flags for runner script") |
| 34 | parser.add_argument("--gdb", help="The gdb binary to use", |
| 35 | default=None) |
| 36 | parser.add_argument("--gdb-args", help="Additional gdb arguments") |
| 37 | parser.add_argument("--output", help="A file to redirect output to") |
| 38 | parser.add_argument("--stderr", help="A file to redirect stderr to") |
| 39 | parser.add_argument("--no-suspend", action="store_true", |
| 40 | help="Ask the binary to not wait for GDB connection") |
| 41 | |
| 42 | return parser.parse_args() |
| 43 | |
| 44 | |
| 45 | def log(output, msg): |
| 46 | if output: |
| 47 | output.write(msg + "\n") |
| 48 | output.flush() |
| 49 | else: |
| 50 | print(msg) |
| 51 | |
| 52 | |
| 53 | if __name__ == '__main__': |
| 54 | args = get_args() |
| 55 | |
| 56 | # Search for a gdb we can use |
| 57 | if not args.gdb: |
| 58 | args.gdb = shutil.which("gdb-multiarch") |
| 59 | if not args.gdb: |
| 60 | args.gdb = shutil.which("gdb") |
| 61 | if not args.gdb: |
| 62 | print("We need gdb to run the test") |
| 63 | exit(-1) |
| 64 | if args.output: |
| 65 | output = open(args.output, "w") |
| 66 | else: |
| 67 | output = None |
| 68 | if args.stderr: |
| 69 | stderr = open(args.stderr, "w") |
| 70 | else: |
| 71 | stderr = None |
| 72 | |
| 73 | socket_dir = TemporaryDirectory("qemu-gdbstub") |
| 74 | socket_name = os.path.join(socket_dir.name, "gdbstub.socket") |
| 75 | |
| 76 | # Launch QEMU with binary |
| 77 | if "system" in args.qemu: |
| 78 | if args.no_suspend: |
| 79 | suspend = '' |
| 80 | else: |
| 81 | suspend = ' -S' |
| 82 | cmd = f'{args.qemu} {args.qargs} {args.binary}' \ |
| 83 | f'{suspend} -gdb unix:path={socket_name},server=on' |
| 84 | else: |
| 85 | if args.no_suspend: |
| 86 | suspend = ',suspend=n' |
| 87 | else: |
| 88 | suspend = '' |
| 89 | cmd = f'{args.qemu} {args.qargs} -g {socket_name}{suspend}' \ |
| 90 | f' {args.binary}' |
| 91 | |
| 92 | log(output, "QEMU CMD: %s" % (cmd)) |
| 93 | inferior = subprocess.Popen(shlex.split(cmd)) |
| 94 | |
| 95 | # Now launch gdb with our test and collect the result |
| 96 | gdb_cmd = "%s %s" % (args.gdb, args.binary) |
| 97 | if args.gdb_args: |
| 98 | gdb_cmd += " %s" % (args.gdb_args) |
| 99 | # run quietly and ignore .gdbinit |
| 100 | gdb_cmd += " -q -n -batch" |
| 101 | # disable pagination |
| 102 | gdb_cmd += " -ex 'set pagination off'" |
| 103 | # disable prompts in case of crash |
| 104 | gdb_cmd += " -ex 'set confirm off'" |
| 105 | # connect to remote |
| 106 | gdb_cmd += " -ex 'target remote %s'" % (socket_name) |
| 107 | # finally the test script itself |
| 108 | if args.test: |
| 109 | if args.test_args: |
| 110 | gdb_cmd += f" -ex \"py sys.argv={args.test_args}\"" |
| 111 | gdb_cmd += " -x %s" % (args.test) |
| 112 | |
| 113 | |
| 114 | sleep(1) |
| 115 | log(output, "GDB CMD: %s" % (gdb_cmd)) |
| 116 | |
| 117 | gdb_env = dict(os.environ) |
| 118 | gdb_pythonpath = gdb_env.get("PYTHONPATH", "").split(os.pathsep) |
| 119 | gdb_pythonpath.append(os.path.dirname(os.path.realpath(__file__))) |
| 120 | gdb_env["PYTHONPATH"] = os.pathsep.join(gdb_pythonpath) |
| 121 | result = subprocess.call(gdb_cmd, shell=True, stdout=output, stderr=stderr, |
| 122 | env=gdb_env) |
| 123 | |
| 124 | # A result of greater than 128 indicates a fatal signal (likely a |
| 125 | # crash due to gdb internal failure). That's a problem for GDB and |
| 126 | # not the test so we force a return of 0 so we don't fail the test on |
| 127 | # account of broken external tools. |
| 128 | if result > 128: |
| 129 | log(output, "GDB crashed? (%d, %d) SKIPPING" % (result, result - 128)) |
| 130 | exit(0) |
| 131 | |
| 132 | try: |
| 133 | inferior.wait(2) |
| 134 | except subprocess.TimeoutExpired: |
| 135 | log(output, "GDB never connected? Killed guest") |
| 136 | inferior.kill() |
| 137 | |
| 138 | exit(result) |