tor-browser

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

android.py (10137B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 """ Drives an android device.
      5 """
      6 import os
      7 import posixpath
      8 import tempfile
      9 import contextlib
     10 import time
     11 import logging
     12 
     13 import attr
     14 from arsenic.services import Geckodriver, free_port, subprocess_based_service
     15 from mozdevice import ADBDeviceFactory, ADBError
     16 
     17 from condprof.util import write_yml_file, logger, DEFAULT_PREFS, BaseEnv
     18 
     19 
     20 # XXX most of this code should migrate into mozdevice - see Bug 1574849
     21 class AndroidDevice:
     22    def __init__(
     23        self,
     24        app_name,
     25        marionette_port=2828,
     26        verbose=False,
     27        remote_test_root="/sdcard/test_root/",
     28    ):
     29        self.app_name = app_name
     30 
     31        # XXX make that an option
     32        if "fenix" in app_name:
     33            self.activity = "org.mozilla.fenix.IntentReceiverActivity"
     34        else:
     35            self.activity = "org.mozilla.geckoview_example.GeckoViewActivity"
     36        self.verbose = verbose
     37        self.device = None
     38        self.marionette_port = marionette_port
     39        self.profile = None
     40        self.remote_profile = None
     41        self.log_file = None
     42        self.remote_test_root = remote_test_root
     43        self._adb_fh = None
     44 
     45    def _set_adb_logger(self, log_file):
     46        self.log_file = log_file
     47        if self.log_file is None:
     48            return
     49        logger.info("Setting ADB log file to %s" % self.log_file)
     50        adb_logger = logging.getLogger("adb")
     51        adb_logger.setLevel(logging.DEBUG)
     52        self._adb_fh = logging.FileHandler(self.log_file)
     53        self._adb_fh.setLevel(logging.DEBUG)
     54        adb_logger.addHandler(self._adb_fh)
     55 
     56    def _unset_adb_logger(self):
     57        if self._adb_fh is None:
     58            return
     59        logging.getLogger("adb").removeHandler(self._adb_fh)
     60        self._adb_fh = None
     61 
     62    def clear_logcat(self, timeout=None, buffers=[]):
     63        if not self.device:
     64            return
     65        self.device.clear_logcat(timeout, buffers)
     66 
     67    def get_logcat(self):
     68        if not self.device:
     69            return None
     70        # we don't want to have ADBCommand dump the command
     71        # in the debug stream so we reduce its verbosity here
     72        # temporarely
     73        old_verbose = self.device._verbose
     74        self.device._verbose = False
     75        try:
     76            return self.device.get_logcat()
     77        finally:
     78            self.device._verbose = old_verbose
     79 
     80    def prepare(self, profile, logfile):
     81        self._set_adb_logger(logfile)
     82        try:
     83            # See android_emulator_pgo.py run_tests for more
     84            # details on why test_root must be /sdcard/test_root
     85            # for android pgo due to Android 4.3.
     86            self.device = ADBDeviceFactory(
     87                verbose=self.verbose, logger_name="adb", test_root=self.remote_test_root
     88            )
     89        except Exception:
     90            logger.error("Cannot initialize device")
     91            raise
     92        device = self.device
     93        self.profile = profile
     94 
     95        # checking that the app is installed
     96        if not device.is_app_installed(self.app_name):
     97            raise Exception("%s is not installed" % self.app_name)
     98 
     99        # debug flag
    100        logger.info("Setting %s as the debug app on the phone" % self.app_name)
    101        device.shell(
    102            "am set-debug-app --persistent %s" % self.app_name,
    103            stdout_callback=logger.info,
    104        )
    105 
    106        # creating the profile on the device
    107        logger.info("Creating the profile on the device")
    108 
    109        remote_profile = posixpath.join(self.remote_test_root, "profile")
    110        logger.info("The profile on the phone will be at %s" % remote_profile)
    111 
    112        device.rm(remote_profile, force=True, recursive=True)
    113        logger.info("Pushing %s on the phone" % self.profile)
    114        device.push(profile, remote_profile)
    115        device.chmod(remote_profile, recursive=True)
    116        self.profile = profile
    117        self.remote_profile = remote_profile
    118 
    119        # creating the yml file
    120        yml_data = {
    121            "args": ["-marionette", "-profile", self.remote_profile],
    122            "prefs": DEFAULT_PREFS,
    123            "env": {"LOG_VERBOSE": 1, "R_LOG_LEVEL": 6, "MOZ_LOG": ""},
    124        }
    125 
    126        yml_name = "%s-geckoview-config.yaml" % self.app_name
    127        yml_on_host = posixpath.join(tempfile.mkdtemp(), yml_name)
    128        write_yml_file(yml_on_host, yml_data)
    129        tmp_on_device = posixpath.join("/data", "local", "tmp")
    130        if not device.exists(tmp_on_device):
    131            raise IOError("%s does not exists on the device" % tmp_on_device)
    132        yml_on_device = posixpath.join(tmp_on_device, yml_name)
    133        try:
    134            device.rm(yml_on_device, force=True, recursive=True)
    135            device.push(yml_on_host, yml_on_device)
    136            device.chmod(yml_on_device, recursive=True)
    137        except Exception:
    138            logger.info("could not create the yaml file on device. Permission issue?")
    139            raise
    140 
    141        # command line 'extra' args not used with geckoview apps; instead we use
    142        # an on-device config.yml file
    143        intent = "android.intent.action.VIEW"
    144        device.stop_application(self.app_name)
    145        device.launch_application(
    146            self.app_name, self.activity, intent, extras=None, url="about:blank"
    147        )
    148        if not device.process_exist(self.app_name):
    149            raise Exception("Could not start %s" % self.app_name)
    150 
    151        logger.info("Creating socket forwarding on port %d" % self.marionette_port)
    152        device.forward(
    153            local="tcp:%d" % self.marionette_port,
    154            remote="tcp:%d" % self.marionette_port,
    155        )
    156 
    157        # we don't have a clean way for now to check that GV or Fenix
    158        # is ready to handle our tests. So here we just wait 30s
    159        logger.info("Sleeping for 30s")
    160        time.sleep(30)
    161 
    162    def stop_browser(self):
    163        logger.info("Stopping %s" % self.app_name)
    164        try:
    165            self.device.stop_application(self.app_name)
    166        except ADBError:
    167            logger.info("Could not stop the application using force-stop")
    168 
    169        time.sleep(5)
    170        if self.device.process_exist(self.app_name):
    171            logger.info("%s still running, trying SIGKILL" % self.app_name)
    172            num_tries = 0
    173            while self.device.process_exist(self.app_name) and num_tries < 5:
    174                try:
    175                    self.device.pkill(self.app_name)
    176                except ADBError:
    177                    pass
    178                num_tries += 1
    179                time.sleep(1)
    180        logger.info("%s stopped" % self.app_name)
    181 
    182    def collect_profile(self):
    183        logger.info("Collecting profile from %s" % self.remote_profile)
    184        self.device.pull(self.remote_profile, self.profile)
    185 
    186    def close(self):
    187        self._unset_adb_logger()
    188        if self.device is None:
    189            return
    190        try:
    191            self.device.remove_forwards("tcp:%d" % self.marionette_port)
    192        except ADBError:
    193            logger.warning("Could not remove forward port")
    194 
    195 
    196 # XXX redundant, remove
    197 @contextlib.contextmanager
    198 def device(
    199    app_name, marionette_port=2828, verbose=True, remote_test_root="/sdcard/test_root/"
    200 ):
    201    device_ = AndroidDevice(
    202        app_name, marionette_port, verbose, remote_test_root=remote_test_root
    203    )
    204    try:
    205        yield device_
    206    finally:
    207        device_.close()
    208 
    209 
    210 @attr.s
    211 class AndroidGeckodriver(Geckodriver):
    212    async def start(self):
    213        port = free_port()
    214        await self._check_version()
    215        logger.info("Running Webdriver on port %d" % port)
    216        logger.info("Running Marionette on port 2828")
    217        pargs = [
    218            self.binary,
    219            "--log",
    220            "trace",
    221            "--port",
    222            str(port),
    223            "--marionette-port",
    224            "2828",
    225        ]
    226        logger.info("Connecting on Android device")
    227        pargs.append("--connect-existing")
    228        return await subprocess_based_service(
    229            pargs, f"http://localhost:{port}", self.log_file
    230        )
    231 
    232 
    233 class AndroidEnv(BaseEnv):
    234    @contextlib.contextmanager
    235    def get_device(self, *args, **kw):
    236        with device(self.firefox, *args, **kw) as d:
    237            self.device = d
    238            yield self.device
    239 
    240    def get_target_platform(self):
    241        app = self.firefox.split("org.mozilla.")[-1]
    242        if self.device_name is None:
    243            return app
    244        return "%s-%s" % (self.device_name, app)
    245 
    246    def dump_logs(self):
    247        logger.info("Dumping Android logs")
    248        try:
    249            logcat = self.device.get_logcat()
    250            if logcat:
    251                # local path, not using posixpath
    252                logfile = os.path.join(self.archive, "logcat.log")
    253                logger.info("Writing logcat at %s" % logfile)
    254                with open(logfile, "wb") as f:
    255                    for line in logcat:
    256                        f.write(line.encode("utf8", errors="replace") + b"\n")
    257            else:
    258                logger.info("logcat came back empty")
    259        except Exception:
    260            logger.error("Could not extract the logcat", exc_info=True)
    261 
    262    @contextlib.contextmanager
    263    def get_browser(self):
    264        yield
    265 
    266    def get_browser_args(self, headless, prefs=None):
    267        # XXX merge with DesktopEnv.get_browser_args
    268        options = ["--allow-downgrade"]
    269        if headless:
    270            options.append("-headless")
    271        if prefs is None:
    272            prefs = {}
    273        return {"moz:firefoxOptions": {"args": options, "prefs": prefs}}
    274 
    275    def prepare(self, logfile):
    276        self.device.prepare(self.profile, logfile)
    277 
    278    def get_browser_version(self):
    279        return self.target_platform + "-XXXneedtograbversion"
    280 
    281    def get_geckodriver(self, log_file):
    282        return AndroidGeckodriver(binary=self.geckodriver, log_file=log_file)
    283 
    284    def check_session(self, session):
    285        async def fake_close(*args):
    286            pass
    287 
    288        session.close = fake_close
    289 
    290    def collect_profile(self):
    291        self.device.collect_profile()
    292 
    293    def stop_browser(self):
    294        self.device.stop_browser()