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