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