tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

android.py (9893B)


      1 # mypy: allow-untyped-defs
      2 
      3 import argparse
      4 import os
      5 import platform
      6 import signal
      7 import shutil
      8 import subprocess
      9 import threading
     10 
     11 import requests
     12 from .wpt import venv_dir
     13 
     14 android_device = None
     15 
     16 here = os.path.abspath(os.path.dirname(__file__))
     17 wpt_root = os.path.abspath(os.path.join(here, os.pardir, os.pardir))
     18 
     19 CMDLINE_TOOLS_VERSION_STRING = "12.0"
     20 CMDLINE_TOOLS_VERSION = "11076708"
     21 
     22 AVD_MANIFEST_X86_64 = {
     23    "emulator_package": "system-images;android-34;google_apis;x86_64",
     24    "emulator_avd_name": "mozemulator-android34-x86_64"
     25 }
     26 
     27 
     28 def do_delayed_imports(paths):
     29    global android_device
     30    from mozrunner.devices import android_device
     31 
     32    android_device.TOOLTOOL_PATH = os.path.join(os.path.dirname(__file__),
     33                                                os.pardir,
     34                                                "third_party",
     35                                                "tooltool",
     36                                                "tooltool.py")
     37    android_device.EMULATOR_HOME_DIR = paths["emulator_home"]
     38    android_device.AVD_DICT["x86_64"] = android_device.AvdInfo(
     39        "Android x86_64",
     40        "mozemulator-android34-x86_64",
     41        [
     42            "-skip-adb-auth",
     43            "-verbose",
     44            "-show-kernel",
     45            "-ranchu",
     46            "-selinux",
     47            "permissive",
     48            "-memory",
     49            "4096",
     50            "-cores",
     51            "4",
     52            "-prop",
     53            "ro.test_harness=true",
     54            "-no-snapstorage",
     55            "-no-snapshot",
     56            "-skin",
     57            "800x1280"
     58        ],
     59        True,
     60    )
     61 
     62 
     63 def get_parser_install():
     64    parser = argparse.ArgumentParser()
     65    parser.add_argument("--path", dest="dest",
     66                        help="Root path to use for emulator tooling")
     67    parser.add_argument("--reinstall", action="store_true",
     68                        help="Force reinstall even if the emulator already exists")
     69    parser.add_argument("--prompt", action="store_true",
     70                        help="Enable confirmation prompts")
     71    parser.add_argument("--no-prompt", dest="prompt", action="store_false",
     72                        help="Skip confirmation prompts")
     73    return parser
     74 
     75 
     76 def get_parser_start():
     77    parser = get_parser_install()
     78    parser.add_argument("--device-serial",
     79                        help="Device serial number for Android emulator, if not emulator-5554")
     80    return parser
     81 
     82 
     83 def get_paths(dest):
     84    os_name = platform.system().lower()
     85 
     86    if dest is None:
     87        # os.getcwd() doesn't include the venv path
     88        base_path = os.path.join(wpt_root, venv_dir(), "android")
     89    else:
     90        base_path = dest
     91 
     92    sdk_path = os.environ.get("ANDROID_SDK_HOME", os.path.join(base_path, f"android-sdk-{os_name}"))
     93    avd_path = os.environ.get("ANDROID_AVD_HOME", os.path.join(sdk_path, ".android", "avd"))
     94    return {
     95        "base": base_path,
     96        "sdk": sdk_path,
     97        "sdk_tools": os.path.join(sdk_path, "cmdline-tools", CMDLINE_TOOLS_VERSION_STRING),
     98        "avd": avd_path,
     99        "emulator_home": os.path.dirname(avd_path)
    100    }
    101 
    102 
    103 def get_sdk_manager_path(paths):
    104    os_name = platform.system().lower()
    105    file_name = "sdkmanager"
    106    if os_name.startswith("win"):
    107        file_name += ".bat"
    108    return os.path.join(paths["sdk_tools"], "bin", file_name)
    109 
    110 
    111 def get_avd_manager(paths):
    112    os_name = platform.system().lower()
    113    file_name = "avdmanager"
    114    if os_name.startswith("win"):
    115        file_name += ".bat"
    116    return os.path.join(paths["sdk_tools"], "bin", file_name)
    117 
    118 
    119 def uninstall_sdk(paths):
    120    if os.path.exists(paths["sdk"]) and os.path.isdir(paths["sdk"]):
    121        shutil.rmtree(paths["sdk"])
    122 
    123 
    124 def get_os_tag(logger):
    125    os_name = platform.system().lower()
    126    if os_name not in ["darwin", "linux", "windows"]:
    127        logger.critical("Unsupported platform %s" % os_name)
    128        raise NotImplementedError
    129 
    130    if os_name == "macosx":
    131        return "darwin"
    132    if os_name == "windows":
    133        return "win"
    134    return "linux"
    135 
    136 
    137 def download_and_extract(url, path):
    138    if not os.path.exists(path):
    139        os.makedirs(path)
    140    temp_path = os.path.join(path, url.rsplit("/", 1)[1])
    141    try:
    142        with open(temp_path, "wb") as f:
    143            with requests.get(url, stream=True) as resp:
    144                for chunk in resp.iter_content(2**16):
    145                    f.write(chunk)
    146        if not os.path.exists(temp_path):
    147            raise ValueError(f"Failed to download {url}, output path doesn't exist")
    148        # Python's zipfile module doesn't seem to work here
    149        subprocess.check_call(["unzip", temp_path], cwd=path)
    150    finally:
    151        if os.path.exists(temp_path):
    152            os.unlink(temp_path)
    153 
    154 
    155 def install_sdk(logger, paths):
    156    if os.path.isdir(paths["sdk_tools"]):
    157        logger.info("Using SDK installed at %s" % paths["sdk_tools"])
    158        return False
    159 
    160    if not os.path.exists(paths["sdk"]):
    161        os.makedirs(paths["sdk"])
    162 
    163    download_path = os.path.dirname(paths["sdk_tools"])
    164 
    165    url = f'https://dl.google.com/android/repository/commandlinetools-{get_os_tag(logger)}-{CMDLINE_TOOLS_VERSION}_latest.zip'
    166    logger.info("Getting SDK from %s" % url)
    167 
    168    download_and_extract(url, download_path)
    169    os.rename(os.path.join(download_path, "cmdline-tools"), paths["sdk_tools"])
    170 
    171    return True
    172 
    173 
    174 def install_android_packages(logger, paths, packages, prompt=True):
    175    sdk_manager = get_sdk_manager_path(paths)
    176    if not os.path.exists(sdk_manager):
    177        raise OSError(f"Can't find sdkmanager at {sdk_manager}")
    178 
    179    # TODO: make this work non-internactively
    180    logger.info(f"Installing Android packages {' '.join(packages)}")
    181    cmd = [sdk_manager] + packages
    182 
    183    input_data = None if prompt else "\n".join(["y"] * 100).encode("UTF-8")
    184    subprocess.run(cmd, check=True, input=input_data)
    185 
    186 
    187 def install_avd(logger, paths, prompt=True):
    188    avd_manager = get_avd_manager(paths)
    189    avd_manifest = AVD_MANIFEST_X86_64
    190 
    191    install_android_packages(logger, paths, [avd_manifest["emulator_package"]], prompt=prompt)
    192 
    193    cmd = [avd_manager,
    194           "--verbose",
    195           "create",
    196           "avd",
    197           "--force",
    198           "--name",
    199           avd_manifest["emulator_avd_name"],
    200           "--package",
    201           avd_manifest["emulator_package"]]
    202    input_data = None if prompt else b"no"
    203    subprocess.run(cmd, check=True, input=input_data)
    204 
    205 
    206 def get_emulator(paths, device_serial=None):
    207    if android_device is None:
    208        do_delayed_imports(paths)
    209 
    210    substs = {"top_srcdir": wpt_root, "TARGET_CPU": "x86"}
    211    emulator = android_device.AndroidEmulator(substs=substs,
    212                                              device_serial=device_serial,
    213                                              verbose=True)
    214    emulator.emulator_path = os.path.join(paths["sdk"], "emulator", "emulator")
    215    return emulator
    216 
    217 
    218 class Environ:
    219    def __init__(self, **kwargs):
    220        self.environ = None
    221        self.set_environ = kwargs
    222 
    223    def __enter__(self):
    224        self.environ = os.environ.copy()
    225        for key, value in self.set_environ.items():
    226            if value is None:
    227                if key in os.environ:
    228                    del os.environ[key]
    229            else:
    230                os.environ[key] = value
    231 
    232    def __exit__(self, *args, **kwargs):
    233        os.environ = self.environ
    234 
    235 
    236 def android_environment(paths):
    237    return Environ(ANDROID_EMULATOR_HOME=paths["emulator_home"],
    238                   ANDROID_AVD_HOME=paths["avd"],
    239                   ANDROID_SDK_ROOT=paths["sdk"],
    240                   ANDROID_SDK_HOME=paths["sdk"])
    241 
    242 
    243 def install(logger, dest=None, reinstall=False, prompt=True):
    244    paths = get_paths(dest)
    245 
    246    with android_environment(paths):
    247 
    248        if reinstall:
    249            uninstall_sdk(paths)
    250 
    251        new_install = install_sdk(logger, paths)
    252 
    253        if new_install:
    254            packages = ["platform-tools",
    255                        "build-tools;36.1.0",
    256                        "platforms;android-36.1",
    257                        "emulator"]
    258 
    259            install_android_packages(logger, paths, packages, prompt=prompt)
    260 
    261            install_avd(logger, paths, prompt=prompt)
    262 
    263        emulator = get_emulator(paths)
    264    return emulator
    265 
    266 
    267 def cancel_start(thread_id):
    268    def cancel_func():
    269        raise signal.pthread_kill(thread_id, signal.SIGINT)
    270    return cancel_func
    271 
    272 
    273 def start(logger, dest=None, reinstall=False, prompt=True, device_serial=None):
    274    paths = get_paths(dest)
    275 
    276    with android_environment(paths):
    277        install(logger, dest=dest, reinstall=reinstall, prompt=prompt)
    278 
    279        emulator = get_emulator(paths, device_serial=device_serial)
    280 
    281        if not emulator.check_avd():
    282            logger.critical("Android AVD not found, please run |wpt install-android-emulator|")
    283            raise OSError
    284 
    285        emulator.start()
    286        timer = threading.Timer(300, cancel_start(threading.get_ident()))
    287        timer.start()
    288        for i in range(10):
    289            logger.info(f"Wait for emulator to start attempt {i + 1}/10")
    290            try:
    291                emulator.wait_for_start()
    292            except Exception:
    293                import traceback
    294                logger.warning(f"""emulator.wait_for_start() failed:
    295 {traceback.format_exc()}""")
    296            else:
    297                break
    298        timer.cancel()
    299    return emulator
    300 
    301 
    302 def run_install(venv, **kwargs):
    303    try:
    304        import logging
    305        logging.basicConfig()
    306        logger = logging.getLogger()
    307 
    308        install(logger, **kwargs)
    309    except Exception:
    310        import traceback
    311        traceback.print_exc()
    312        import pdb
    313        pdb.post_mortem()
    314 
    315 
    316 def run_start(venv, **kwargs):
    317    try:
    318        import logging
    319        logging.basicConfig()
    320        logger = logging.getLogger()
    321 
    322        start(logger, **kwargs)
    323    except Exception:
    324        import traceback
    325        traceback.print_exc()
    326        import pdb
    327        pdb.post_mortem()