xref: /llvm-project/libcxx/utils/adb_run.py (revision 52dc4918ca8b874ddd4e4fcad873a66ecc5b6953)
1d173ce4aSRyan Prichard#!/usr/bin/env python3
2d173ce4aSRyan Prichard# ===----------------------------------------------------------------------===##
3d173ce4aSRyan Prichard#
4d173ce4aSRyan Prichard# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
5d173ce4aSRyan Prichard# See https://llvm.org/LICENSE.txt for license information.
6d173ce4aSRyan Prichard# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7d173ce4aSRyan Prichard#
8d173ce4aSRyan Prichard# ===----------------------------------------------------------------------===##
9d173ce4aSRyan Prichard
10d173ce4aSRyan Prichard"""adb_run.py is a utility for running a libc++ test program via adb.
11d173ce4aSRyan Prichard"""
12d173ce4aSRyan Prichard
13d173ce4aSRyan Prichardimport argparse
14d173ce4aSRyan Prichardimport hashlib
15d173ce4aSRyan Prichardimport os
16d173ce4aSRyan Prichardimport re
17d173ce4aSRyan Prichardimport shlex
18d173ce4aSRyan Prichardimport socket
19d173ce4aSRyan Prichardimport subprocess
20d173ce4aSRyan Prichardimport sys
21*0d3c40b8SStephan T. Lavavejfrom typing import List, Tuple
22d173ce4aSRyan Prichard
23d173ce4aSRyan Prichard
24d173ce4aSRyan Prichard# Sync a host file /path/to/dir/file to ${REMOTE_BASE_DIR}/run-${HASH}/dir/file.
25d173ce4aSRyan PrichardREMOTE_BASE_DIR = "/data/local/tmp/adb_run"
26d173ce4aSRyan Prichard
27d173ce4aSRyan Prichardg_job_limit_socket = None
28d173ce4aSRyan Prichardg_verbose = False
29d173ce4aSRyan Prichard
30d173ce4aSRyan Prichard
31d173ce4aSRyan Pricharddef run_adb_sync_command(command: List[str]) -> None:
32d173ce4aSRyan Prichard    """Run an adb command and discard the output, unless the command fails. If
33d173ce4aSRyan Prichard    the command fails, dump the output instead, and exit the script with
34d173ce4aSRyan Prichard    failure.
35d173ce4aSRyan Prichard    """
36d173ce4aSRyan Prichard    if g_verbose:
37d173ce4aSRyan Prichard        sys.stderr.write(f"running: {shlex.join(command)}\n")
38d173ce4aSRyan Prichard    proc = subprocess.run(command, universal_newlines=True,
39d173ce4aSRyan Prichard                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
40d173ce4aSRyan Prichard                          stderr=subprocess.STDOUT, encoding="utf-8")
41d173ce4aSRyan Prichard    if proc.returncode != 0:
42d173ce4aSRyan Prichard        # adb's stdout (e.g. for adb push) should normally be discarded, but
43d173ce4aSRyan Prichard        # on failure, it should be shown. Print it to stderr because it's
44d173ce4aSRyan Prichard        # unrelated to the test program's stdout output. A common error caught
45d173ce4aSRyan Prichard        # here is "No space left on device".
46d173ce4aSRyan Prichard        sys.stderr.write(f"{proc.stdout}\n"
47d173ce4aSRyan Prichard                         f"error: adb command exited with {proc.returncode}: "
48d173ce4aSRyan Prichard                         f"{shlex.join(command)}\n")
49d173ce4aSRyan Prichard        sys.exit(proc.returncode)
50d173ce4aSRyan Prichard
51d173ce4aSRyan Prichard
52d173ce4aSRyan Pricharddef sync_test_dir(local_dir: str, remote_dir: str) -> None:
53d173ce4aSRyan Prichard    """Sync the libc++ test directory on the host to the remote device."""
54d173ce4aSRyan Prichard
55d173ce4aSRyan Prichard    # Optimization: The typical libc++ test directory has only a single
56d173ce4aSRyan Prichard    # *.tmp.exe file in it. In that case, skip the `mkdir` command, which is
57d173ce4aSRyan Prichard    # normally necessary because we don't know if the target directory already
58d173ce4aSRyan Prichard    # exists on the device.
59d173ce4aSRyan Prichard    local_files = os.listdir(local_dir)
60d173ce4aSRyan Prichard    if len(local_files) == 1:
61d173ce4aSRyan Prichard        local_file = os.path.join(local_dir, local_files[0])
62d173ce4aSRyan Prichard        remote_file = os.path.join(remote_dir, local_files[0])
63d173ce4aSRyan Prichard        if not os.path.islink(local_file) and os.path.isfile(local_file):
64d173ce4aSRyan Prichard            run_adb_sync_command(["adb", "push", "--sync", local_file,
65d173ce4aSRyan Prichard                                  remote_file])
66d173ce4aSRyan Prichard            return
67d173ce4aSRyan Prichard
68d173ce4aSRyan Prichard    assert os.path.basename(local_dir) == os.path.basename(remote_dir)
69d173ce4aSRyan Prichard    run_adb_sync_command(["adb", "shell", "mkdir", "-p", remote_dir])
70d173ce4aSRyan Prichard    run_adb_sync_command(["adb", "push", "--sync", local_dir,
71d173ce4aSRyan Prichard                          os.path.dirname(remote_dir)])
72d173ce4aSRyan Prichard
73d173ce4aSRyan Prichard
74d173ce4aSRyan Pricharddef build_env_arg(env_args: List[str], prepend_path_args: List[Tuple[str, str]]) -> str:
75d173ce4aSRyan Prichard    components = []
76d173ce4aSRyan Prichard    for arg in env_args:
77d173ce4aSRyan Prichard        k, v = arg.split("=", 1)
78d173ce4aSRyan Prichard        components.append(f"export {k}={shlex.quote(v)}; ")
79d173ce4aSRyan Prichard    for k, v in prepend_path_args:
80d173ce4aSRyan Prichard        components.append(f"export {k}={shlex.quote(v)}${{{k}:+:${k}}}; ")
81d173ce4aSRyan Prichard    return "".join(components)
82d173ce4aSRyan Prichard
83d173ce4aSRyan Prichard
84d173ce4aSRyan Pricharddef run_command(args: argparse.Namespace) -> int:
85d173ce4aSRyan Prichard    local_dir = args.execdir
86d173ce4aSRyan Prichard    assert local_dir.startswith("/")
87d173ce4aSRyan Prichard    assert not local_dir.endswith("/")
88d173ce4aSRyan Prichard
89d173ce4aSRyan Prichard    # Copy each execdir to a subdir of REMOTE_BASE_DIR. Name the directory using
90d173ce4aSRyan Prichard    # a hash of local_dir so that concurrent adb_run invocations don't create
91d173ce4aSRyan Prichard    # the same intermediate parent directory. At least `adb push` has trouble
92d173ce4aSRyan Prichard    # with concurrent mkdir syscalls on common parent directories. (Somehow
93d173ce4aSRyan Prichard    # mkdir fails with EAGAIN/EWOULDBLOCK, see internal Google bug,
94d173ce4aSRyan Prichard    # b/289311228.)
95d173ce4aSRyan Prichard    local_dir_hash = hashlib.sha1(local_dir.encode()).hexdigest()
96d173ce4aSRyan Prichard    remote_dir = f"{REMOTE_BASE_DIR}/run-{local_dir_hash}/{os.path.basename(local_dir)}"
97d173ce4aSRyan Prichard    sync_test_dir(local_dir, remote_dir)
98d173ce4aSRyan Prichard
99d173ce4aSRyan Prichard    adb_shell_command = (
100d173ce4aSRyan Prichard        # Set the environment early so that PATH can be overridden. Overriding
101d173ce4aSRyan Prichard        # PATH is useful for:
102d173ce4aSRyan Prichard        #  - Replacing older shell utilities with toybox (e.g. on old devices).
103d173ce4aSRyan Prichard        #  - Adding a `bash` command that delegates to `sh` (mksh).
104d173ce4aSRyan Prichard        f"{build_env_arg(args.env, args.prepend_path_env)}"
105d173ce4aSRyan Prichard
106d173ce4aSRyan Prichard        # Set a high oom_score_adj so that, if the test program uses too much
107d173ce4aSRyan Prichard        # memory, it is killed before anything else on the device. The default
108d173ce4aSRyan Prichard        # oom_score_adj is -1000, so a test using too much memory typically
109d173ce4aSRyan Prichard        # crashes the device.
110d173ce4aSRyan Prichard        "echo 1000 >/proc/self/oom_score_adj; "
111d173ce4aSRyan Prichard
112d173ce4aSRyan Prichard        # If we're running as root, switch to the shell user. The libc++
113d173ce4aSRyan Prichard        # filesystem tests require running without root permissions. Some x86
114d173ce4aSRyan Prichard        # emulator devices (before Android N) do not have a working `adb unroot`
115d173ce4aSRyan Prichard        # and always run as root. Non-debug builds typically lack `su` and only
116d173ce4aSRyan Prichard        # run as the shell user.
117d173ce4aSRyan Prichard        #
118d173ce4aSRyan Prichard        # Some libc++ tests create temporary files in the working directory,
119d173ce4aSRyan Prichard        # which might be owned by root. Before switching to shell, make the
120d173ce4aSRyan Prichard        # cwd writable (and readable+executable) to every user.
121d173ce4aSRyan Prichard        #
122d173ce4aSRyan Prichard        # N.B.:
123d173ce4aSRyan Prichard        #  - Avoid "id -u" because it wasn't supported until Android M.
124d173ce4aSRyan Prichard        #  - The `env` and `which` commands were also added in Android M.
125d173ce4aSRyan Prichard        #  - Starting in Android M, su from root->shell resets PATH, so we need
126d173ce4aSRyan Prichard        #    to modify it again in the new environment.
127d173ce4aSRyan Prichard        #  - Avoid chmod's "a+rwx" syntax because it's not supported until
128d173ce4aSRyan Prichard        #    Android N.
129d173ce4aSRyan Prichard        #  - Defining this function allows specifying the arguments to the test
130d173ce4aSRyan Prichard        #    program (i.e. "$@") only once.
131d173ce4aSRyan Prichard        "run_without_root() {"
132d173ce4aSRyan Prichard        "  chmod 777 .;"
133d173ce4aSRyan Prichard        "  case \"$(id)\" in"
134d173ce4aSRyan Prichard        "    *\"uid=0(root)\"*)"
135d173ce4aSRyan Prichard        "    if command -v env >/dev/null; then"
136d173ce4aSRyan Prichard        "      su shell \"$(command -v env)\" PATH=\"$PATH\" \"$@\";"
137d173ce4aSRyan Prichard        "    else"
138d173ce4aSRyan Prichard        "      su shell \"$@\";"
139d173ce4aSRyan Prichard        "    fi;;"
140d173ce4aSRyan Prichard        "    *) \"$@\";;"
141d173ce4aSRyan Prichard        "  esac;"
142d173ce4aSRyan Prichard        "}; "
143d173ce4aSRyan Prichard    )
144d173ce4aSRyan Prichard
145d173ce4aSRyan Prichard    # Older versions of Bionic limit the length of argv[0] to 127 bytes
146d173ce4aSRyan Prichard    # (SOINFO_NAME_LEN-1), and the path to libc++ tests tend to exceed this
147d173ce4aSRyan Prichard    # limit. Changing the working directory works around this limit. The limit
148d173ce4aSRyan Prichard    # is increased to 4095 (PATH_MAX-1) in Android M (API 23).
149d173ce4aSRyan Prichard    command_line = [arg.replace(local_dir + "/", "./") for arg in args.command]
150d173ce4aSRyan Prichard
151d173ce4aSRyan Prichard    # Prior to the adb feature "shell_v2" (added in Android N), `adb shell`
152d173ce4aSRyan Prichard    # always created a pty:
153d173ce4aSRyan Prichard    #  - This merged stdout and stderr together.
154d173ce4aSRyan Prichard    #  - The pty converts LF to CRLF.
155d173ce4aSRyan Prichard    #  - The exit code of the shell command wasn't propagated.
156d173ce4aSRyan Prichard    # Work around all three limitations, unless "shell_v2" is present.
157d173ce4aSRyan Prichard    proc = subprocess.run(["adb", "features"], check=True,
158d173ce4aSRyan Prichard                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
159d173ce4aSRyan Prichard                          encoding="utf-8")
160d173ce4aSRyan Prichard    adb_features = set(proc.stdout.strip().split())
161d173ce4aSRyan Prichard    has_shell_v2 = "shell_v2" in adb_features
162d173ce4aSRyan Prichard    if has_shell_v2:
163d173ce4aSRyan Prichard        adb_shell_command += (
164d173ce4aSRyan Prichard            f"cd {remote_dir} && run_without_root {shlex.join(command_line)}"
165d173ce4aSRyan Prichard        )
166d173ce4aSRyan Prichard    else:
167d173ce4aSRyan Prichard        adb_shell_command += (
168d173ce4aSRyan Prichard            f"{{"
169d173ce4aSRyan Prichard            f"  stdout=$("
170d173ce4aSRyan Prichard            f"    cd {remote_dir} && run_without_root {shlex.join(command_line)};"
171d173ce4aSRyan Prichard            f"    echo -n __libcxx_adb_exit__=$?"
172d173ce4aSRyan Prichard            f"  ); "
173d173ce4aSRyan Prichard            f"}} 2>&1; "
174d173ce4aSRyan Prichard            f"echo -n __libcxx_adb_stdout__\"$stdout\""
175d173ce4aSRyan Prichard        )
176d173ce4aSRyan Prichard
177d173ce4aSRyan Prichard    adb_command_line = ["adb", "shell", adb_shell_command]
178d173ce4aSRyan Prichard    if g_verbose:
179d173ce4aSRyan Prichard        sys.stderr.write(f"running: {shlex.join(adb_command_line)}\n")
180d173ce4aSRyan Prichard
181d173ce4aSRyan Prichard    if has_shell_v2:
182d173ce4aSRyan Prichard        proc = subprocess.run(adb_command_line, shell=False, check=False,
183d173ce4aSRyan Prichard                              encoding="utf-8")
184d173ce4aSRyan Prichard        return proc.returncode
185d173ce4aSRyan Prichard    else:
186d173ce4aSRyan Prichard        proc = subprocess.run(adb_command_line, shell=False, check=False,
187d173ce4aSRyan Prichard                              stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
188d173ce4aSRyan Prichard                              encoding="utf-8")
189d173ce4aSRyan Prichard        # The old `adb shell` mode used a pty, which converted LF to CRLF.
190d173ce4aSRyan Prichard        # Convert it back.
191d173ce4aSRyan Prichard        output = proc.stdout.replace("\r\n", "\n")
192d173ce4aSRyan Prichard
193d173ce4aSRyan Prichard        if proc.returncode:
194d173ce4aSRyan Prichard            sys.stderr.write(f"error: adb failed:\n"
195d173ce4aSRyan Prichard                             f"  command: {shlex.join(adb_command_line)}\n"
196d173ce4aSRyan Prichard                             f"  output: {output}\n")
197d173ce4aSRyan Prichard            return proc.returncode
198d173ce4aSRyan Prichard
199d173ce4aSRyan Prichard        match = re.match(r"(.*)__libcxx_adb_stdout__(.*)__libcxx_adb_exit__=(\d+)$",
200d173ce4aSRyan Prichard                     output, re.DOTALL)
201d173ce4aSRyan Prichard        if not match:
202d173ce4aSRyan Prichard            sys.stderr.write(f"error: could not parse adb output:\n"
203d173ce4aSRyan Prichard                             f"  command: {shlex.join(adb_command_line)}\n"
204d173ce4aSRyan Prichard                             f"  output: {output}\n")
205d173ce4aSRyan Prichard            return 1
206d173ce4aSRyan Prichard
207d173ce4aSRyan Prichard        sys.stderr.write(match.group(1))
208d173ce4aSRyan Prichard        sys.stdout.write(match.group(2))
209d173ce4aSRyan Prichard        return int(match.group(3))
210d173ce4aSRyan Prichard
211d173ce4aSRyan Prichard
212d173ce4aSRyan Pricharddef connect_to_job_limiter_server(sock_addr: str) -> None:
213d173ce4aSRyan Prichard    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
214d173ce4aSRyan Prichard
215d173ce4aSRyan Prichard    try:
216d173ce4aSRyan Prichard        sock.connect(sock_addr)
217d173ce4aSRyan Prichard    except (FileNotFoundError, ConnectionRefusedError) as e:
218d173ce4aSRyan Prichard        # Copying-and-pasting an adb_run.py command-line from a lit test failure
219d173ce4aSRyan Prichard        # is likely to fail because the socket no longer exists (or is
220d173ce4aSRyan Prichard        # inactive), so just give a warning.
221d173ce4aSRyan Prichard        sys.stderr.write(f"warning: could not connect to {sock_addr}: {e}\n")
222d173ce4aSRyan Prichard        return
223d173ce4aSRyan Prichard
224d173ce4aSRyan Prichard    # The connect call can succeed before the server has called accept, because
225d173ce4aSRyan Prichard    # of the listen backlog, so wait for the server to send a byte.
226d173ce4aSRyan Prichard    sock.recv(1)
227d173ce4aSRyan Prichard
228d173ce4aSRyan Prichard    # Keep the socket open until this process ends, then let the OS close the
229d173ce4aSRyan Prichard    # connection automatically.
230d173ce4aSRyan Prichard    global g_job_limit_socket
231d173ce4aSRyan Prichard    g_job_limit_socket = sock
232d173ce4aSRyan Prichard
233d173ce4aSRyan Prichard
234d173ce4aSRyan Pricharddef main() -> int:
235d173ce4aSRyan Prichard    """Main function (pylint wants this docstring)."""
236d173ce4aSRyan Prichard    parser = argparse.ArgumentParser()
237d173ce4aSRyan Prichard    parser.add_argument("--execdir", type=str, required=True)
238d173ce4aSRyan Prichard    parser.add_argument("--env", type=str, required=False, action="append",
239d173ce4aSRyan Prichard                        default=[], metavar="NAME=VALUE")
240d173ce4aSRyan Prichard    parser.add_argument("--prepend-path-env", type=str, nargs=2, required=False,
241d173ce4aSRyan Prichard                        action="append", default=[],
242d173ce4aSRyan Prichard                        metavar=("NAME", "PATH"))
243d173ce4aSRyan Prichard    parser.add_argument("--job-limit-socket")
244d173ce4aSRyan Prichard    parser.add_argument("--verbose", "-v", default=False, action="store_true")
245d173ce4aSRyan Prichard    parser.add_argument("command", nargs=argparse.ONE_OR_MORE)
246d173ce4aSRyan Prichard    args = parser.parse_args()
247d173ce4aSRyan Prichard
248d173ce4aSRyan Prichard    global g_verbose
249d173ce4aSRyan Prichard    g_verbose = args.verbose
250d173ce4aSRyan Prichard    if args.job_limit_socket is not None:
251d173ce4aSRyan Prichard        connect_to_job_limiter_server(args.job_limit_socket)
252d173ce4aSRyan Prichard    return run_command(args)
253d173ce4aSRyan Prichard
254d173ce4aSRyan Prichard
255d173ce4aSRyan Prichardif __name__ == '__main__':
256d173ce4aSRyan Prichard    sys.exit(main())
257