diff --git a/etc/run_in_headless_android_emulator.py b/etc/run_in_headless_android_emulator.py new file mode 100755 index 00000000000..45e088e0389 --- /dev/null +++ b/etc/run_in_headless_android_emulator.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python + +# Copyright 2018 The Servo Project Developers. See the COPYRIGHT +# file at the top-level directory of this distribution. +# +# Licensed under the Apache License, Version 2.0 or the MIT license +# , at your +# option. This file may not be copied, modified, or distributed +# except according to those terms. + +import contextlib +import os +import signal +import subprocess +import sys +import time + + +def main(avd_name, apk_path, *args): + emulator_port = "5580" + emulator_args = [ + tool_path("emulator", "emulator"), + "@" + avd_name, + "-wipe-data", + "-no-window", + "-no-snapshot", + "-no-snapstorage", + "-gpu", "guest", + "-port", emulator_port, + ] + with terminate_on_exit(emulator_args, stdout=sys.stderr) as emulator_process: + # This is hopefully enough time for the emulator to exit + # if it cannot start because of a configuration problem, + # and probably more time than it needs to boot anyway + time.sleep(2) + + if emulator_process.poll() is not None: + # The emulator process has terminated already, + # wait-for-device would block indefinitely + print("Emulator did not start") + return 1 + + adb = [tool_path("platform-tools", "adb"), "-s", "emulator-" + emulator_port] + + with terminate_on_exit(adb + ["wait-for-device"]) as wait_for_device: + wait_for_device.wait() + + # Now `adb shell` will work, but `adb install` needs a system service + # that might still be in the midle of starting and not be responsive yet. + + # https://stackoverflow.com/a/38896494/1162888 + while 1: + with terminate_on_exit( + adb + ["shell", "getprop", "sys.boot_completed"], + stdout=subprocess.PIPE, + ) as getprop: + stdout, stderr = getprop.communicate() + if "1" in stdout: + break + time.sleep(1) + + check_call(adb + ["install", "-r", apk_path]) + + data_dir = "/sdcard/Android/data/com.mozilla.servo/files" + params_file = data_dir + "/android_params" + + check_call(adb + ["shell", "mkdir -p %s" % data_dir]) + check_call(adb + ["shell", "echo 'servo' > %s" % params_file]) + for arg in args: + check_call(adb + ["shell", "echo %s >> %s" % (shell_quote(arg), params_file)]) + + check_call(adb + ["shell", "am start com.mozilla.servo/com.mozilla.servo.MainActivity"], + stdout=sys.stderr) + + logcat_args = ["RustAndroidGlueStdouterr:D", "*:S", "-v", "raw"] + with terminate_on_exit(adb + ["logcat"] + logcat_args) as logcat: + logcat.wait() + + +def tool_path(directory, bin_name): + if "ANDROID_SDK" in os.environ: + path = os.path.join(os.environ["ANDROID_SDK"], directory, bin_name) + if os.path.exists(path): + return path + + path = os.path.join(os.path.dirname(__file__), "..", "android-toolchains", "sdk", + directory, bin_name) + if os.path.exists(path): + return path + + return bin_name + + +@contextlib.contextmanager +def terminate_on_exit(*args, **kwargs): + process = subprocess.Popen(*args, **kwargs) + try: + yield process + finally: + if process.poll() is None: + # The process seems to be still running + process.terminate() + + +def check_call(*args, **kwargs): + with terminate_on_exit(*args, **kwargs) as process: + exit_code = process.wait() + if exit_code != 0: + sys.exit(exit_code) + + +# Copied from Python 3.3+'s shlex.quote() +def shell_quote(arg): + # use single quotes, and put single quotes into double quotes + # the string $'b is then quoted as '$'"'"'b' + return "'" + arg.replace("'", "'\"'\"'") + "'" + + +def interrupt(_signum, _frame): + raise KeyboardInterrupt + + +if __name__ == "__main__": + if len(sys.argv) < 3: + print("Usage: %s avd_name apk_path [servo args...]" % sys.argv[0]) + print("Example: %s servo-x86 target/i686-linux-android/release/servo.apk https://servo.org" + % sys.argv[0]) + sys.exit(1) + + try: + # When `./mach test-android-startup` runs `Popen.terminate()` on this process, + # raise an exception in order to make `finally:` blocks run + # and also terminate sub-subprocesses. + signal.signal(signal.SIGTERM, interrupt) + sys.exit(main(*sys.argv[1:])) + except KeyboardInterrupt: + sys.exit(1) diff --git a/python/servo/testing_commands.py b/python/servo/testing_commands.py index f705939ba53..f4092e702be 100644 --- a/python/servo/testing_commands.py +++ b/python/servo/testing_commands.py @@ -562,75 +562,36 @@ class MachCommands(CommandBase): if (release and dev) or not (release or dev): print("Please specify one of --dev or --release.") return 1 + + avd = "servo-x86" target = "i686-linux-android" print("Assuming --target " + target) + env = self.build_env(target=target) assert self.handle_android_target(target) + binary_path = self.get_binary_path(release, dev, android=True) + apk = binary_path + ".apk" - emulator_port = "5580" - adb = [self.android_adb_path(env), "-s", "emulator-" + emulator_port] - emulator_process = subprocess.Popen([ - self.android_emulator_path(env), - "@servo-x86", - "-no-window", - "-gpu", "guest", - "-port", emulator_port, - ]) + html = """ + + """ + url = "data:text/html;base64," + html.encode("base64").replace("\n", "") + py = path.join(self.context.topdir, "etc", "run_in_headless_android_emulator.py") + args = [sys.executable, py, avd, apk, url] + process = subprocess.Popen(args, stdout=subprocess.PIPE, env=env) try: - # This is hopefully enough time for the emulator to exit - # if it cannot start because of a configuration problem, - # and probably more time than it needs to boot anyway - time.sleep(1) - if emulator_process.poll() is not None: - # The process has terminated already, wait-for-device would block indefinitely - return 1 - - subprocess.call(adb + ["wait-for-device"]) - - # https://stackoverflow.com/a/38896494/1162888 while 1: - stdout, stderr = subprocess.Popen( - adb + ["shell", "getprop", "sys.boot_completed"], - stdout=subprocess.PIPE, - ).communicate() - if "1" in stdout: - break - print("Waiting for the emulator to boot") - time.sleep(1) - - binary_path = self.get_binary_path(release, dev, android=True) - result = subprocess.call(adb + ["install", "-r", binary_path + ".apk"]) - if result != 0: - return result - - html = """ - - """ - url = "data:text/html;base64," + html.encode("base64").replace("\n", "") - result = subprocess.call(adb + ["shell", """ - mkdir -p /sdcard/Android/data/com.mozilla.servo/files/ - echo 'servo' > /sdcard/Android/data/com.mozilla.servo/files/android_params - echo '%s' >> /sdcard/Android/data/com.mozilla.servo/files/android_params - am start com.mozilla.servo/com.mozilla.servo.MainActivity - """ % url]) - if result != 0: - return result - - logcat = adb + ["logcat", "RustAndroidGlueStdouterr:D", "*:S", "-v", "raw"] - logcat_process = subprocess.Popen(logcat, stdout=subprocess.PIPE) - while 1: - line = logcat_process.stdout.readline() + line = process.stdout.readline() + if len(line) == 0: + print("EOF without finding the expected line") + return 1 + print(line.rstrip()) if "JavaScript is running!" in line: - print(line) break - logcat_process.kill() finally: - try: - emulator_process.kill() - except OSError: - pass + process.terminate() @Command('test-jquery', description='Run the jQuery test suite',