tor-browser

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

geckoinstance.py (29685B)


      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 
      5 # ALL CHANGES TO THIS FILE MUST HAVE REVIEW FROM A MARIONETTE PEER!
      6 #
      7 # Please refer to INSTRUCTIONS TO ADD A NEW PREFERENCE in
      8 # remote/shared/RecommendedPreferences.sys.mjs
      9 #
     10 # The Marionette Python client is used out-of-tree with various builds of
     11 # Firefox. Removing a preference from this file will cause regressions,
     12 # so please be careful and get review from a Testing :: Marionette peer
     13 # before you make any changes to this file.
     14 
     15 import codecs
     16 import io
     17 import json
     18 import os
     19 import sys
     20 import tempfile
     21 import time
     22 import traceback
     23 from copy import deepcopy
     24 
     25 import mozversion
     26 from mozprofile import Profile
     27 from mozrunner import FennecEmulatorRunner, Runner
     28 
     29 from . import errors
     30 
     31 if sys.platform.startswith("darwin"):
     32    # Marionette's own processhandler is only used on MacOS for now
     33    from .processhandler import UNKNOWN_RETURNCODE, ProcessHandler
     34 
     35 
     36 class GeckoInstance:
     37    required_prefs = {
     38        # Make sure Shield doesn't hit the network.
     39        "app.normandy.api_url": "",
     40        # Increase the APZ content response timeout in tests to 1 minute.
     41        # This is to accommodate the fact that test environments tends to be slower
     42        # than production environments (with the b2g emulator being the slowest of them
     43        # all), resulting in the production timeout value sometimes being exceeded
     44        # and causing false-positive test failures. See bug 1176798, bug 1177018,
     45        # bug 1210465.
     46        "apz.content_response_timeout": 60000,
     47        # Disable extension discovery
     48        "browser.discovery.enabled": False,
     49        # Make sure error page is not shown for blank pages with 4xx or 5xx response code
     50        "browser.http.blank_page_with_error_response.enabled": True,
     51        # Disable CFR features for automated tests.
     52        "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features": False,
     53        # Don't pull sponsored Top Sites content from the network
     54        "browser.newtabpage.activity-stream.showSponsoredTopSites": False,
     55        # Disable geolocation ping (#1)
     56        "browser.region.network.url": "",
     57        # Don't pull Top Sites content from the network
     58        "browser.topsites.contile.enabled": False,
     59        # Disable translations
     60        "browser.translations.enable": False,
     61        # Disable UI tour
     62        "browser.uitour.enabled": False,
     63        # Disable captive portal
     64        "captivedetect.canonicalURL": "",
     65        # Defensively disable data reporting systems
     66        "datareporting.healthreport.documentServerURI": "http://%(server)s/dummy/healthreport/",
     67        "datareporting.healthreport.logging.consoleEnabled": False,
     68        "datareporting.healthreport.service.enabled": False,
     69        "datareporting.healthreport.service.firstRun": False,
     70        "datareporting.healthreport.uploadEnabled": False,
     71        "datareporting.usage.uploadEnabled": False,
     72        "telemetry.fog.test.localhost_port": -1,
     73        # Do not show datareporting policy notifications which can interfere with tests
     74        "datareporting.policy.dataSubmissionEnabled": False,
     75        "datareporting.policy.dataSubmissionPolicyBypassNotification": True,
     76        # Disable popup-blocker
     77        "dom.disable_open_during_load": False,
     78        # Enabling the support for File object creation in the content process.
     79        "dom.file.createInChild": True,
     80        # Disable delayed user input event handling
     81        "dom.input_events.security.minNumTicks": 0,
     82        # Disable delayed user input event handling
     83        "dom.input_events.security.minTimeElapsedInMS": 0,
     84        # Disable the ProcessHangMonitor
     85        "dom.ipc.reportProcessHangs": False,
     86        # No slow script dialogs
     87        "dom.max_chrome_script_run_time": 0,
     88        "dom.max_script_run_time": 0,
     89        # Disable navigation change rate limitation
     90        "dom.navigation.navigationRateLimit.count": 0,
     91        # DOM Push
     92        "dom.push.connection.enabled": False,
     93        # Screen Orientation API
     94        "dom.screenorientation.allow-lock": True,
     95        # Disable dialog abuse if alerts are triggered too quickly
     96        "dom.successive_dialog_time_limit": 0,
     97        # Only load extensions from the application and user profile
     98        # AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
     99        "extensions.autoDisableScopes": 0,
    100        "extensions.enabledScopes": 5,
    101        # Disable form autofill for extensions and credit cards
    102        "extensions.formautofill.addresses.enabled": False,
    103        "extensions.formautofill.creditCards.enabled": False,
    104        # Disable metadata caching for installed add-ons by default
    105        "extensions.getAddons.cache.enabled": False,
    106        # Disable intalling any distribution add-ons
    107        "extensions.installDistroAddons": False,
    108        # Turn off extension updates so they don't bother tests
    109        "extensions.update.enabled": False,
    110        "extensions.update.notifyUser": False,
    111        # Redirect various extension update URLs
    112        "extensions.blocklist.detailsURL": (
    113            "http://%(server)s/extensions-dummy/blocklistDetailsURL"
    114        ),
    115        "extensions.blocklist.itemURL": "http://%(server)s/extensions-dummy/blocklistItemURL",
    116        "extensions.hotfix.url": "http://%(server)s/extensions-dummy/hotfixURL",
    117        "extensions.systemAddon.update.enabled": False,
    118        "extensions.update.background.url": (
    119            "http://%(server)s/extensions-dummy/updateBackgroundURL"
    120        ),
    121        "extensions.update.url": "http://%(server)s/extensions-dummy/updateURL",
    122        # Make sure opening about:addons won"t hit the network
    123        "extensions.getAddons.discovery.api_url": "data:, ",
    124        "extensions.getAddons.get.url": "http://%(server)s/extensions-dummy/repositoryGetURL",
    125        "extensions.getAddons.search.browseURL": (
    126            "http://%(server)s/extensions-dummy/repositoryBrowseURL"
    127        ),
    128        # Allow the application to have focus even it runs in the background
    129        "focusmanager.testmode": True,
    130        # Disable useragent updates
    131        "general.useragent.updates.enabled": False,
    132        # Disable geolocation ping (#2)
    133        "geo.provider.network.url": "",
    134        # Always use network provider for geolocation tests
    135        # so we bypass the OSX dialog raised by the corelocation provider
    136        "geo.provider.testing": True,
    137        # Do not scan Wifi
    138        "geo.wifi.scan": False,
    139        # Ensure webrender is on, no need for environment variables
    140        "gfx.webrender.all": True,
    141        # Disable idle-daily notifications to avoid expensive operations
    142        # that may cause unexpected test timeouts.
    143        "idle.lastDailyNotification": -1,
    144        # Disable Firefox accounts ping
    145        "identity.fxaccounts.auth.uri": "https://{server}/dummy/fxa",
    146        # Disable download and usage of OpenH264, and Widevine plugins
    147        "media.gmp-manager.updateEnabled": False,
    148        # Disable the GFX sanity window
    149        "media.sanity-test.disabled": True,
    150        "media.volume_scale": "0.01",
    151        # Disable connectivity service pings
    152        "network.connectivity-service.enabled": False,
    153        # Do not prompt for temporary redirects
    154        "network.http.prompt-temp-redirect": False,
    155        # Do not automatically switch between offline and online
    156        "network.manage-offline-status": False,
    157        # Make sure SNTP requests don't hit the network
    158        "network.sntp.pools": "%(server)s",
    159        # Disabled for causing marionette crashes on OSX. See bug 1882856
    160        "network.dns.native_https_query": False,
    161        # Privacy and Tracking Protection
    162        "privacy.trackingprotection.enabled": False,
    163        "privacy.trackingprotection.pbmode.enabled": False,
    164        # Disable recommended automation prefs in CI
    165        "remote.prefs.recommended": False,
    166        # Don't do network connections for mitm priming
    167        "security.certerrors.mitm.priming.enabled": False,
    168        # Tests don't wait for the notification button security delay
    169        "security.notification_enable_delay": 0,
    170        # Do not download intermediate certificates
    171        "security.remote_settings.intermediates.enabled": False,
    172        # Disable logging for remote settings
    173        "services.settings.loglevel": "off",
    174        # Ensure blocklist updates don't hit the network
    175        "services.settings.server": "data:,#remote-settings-dummy/v1",
    176        # Disable password capture, so that tests that include forms aren"t
    177        # influenced by the presence of the persistent doorhanger notification
    178        "signon.rememberSignons": False,
    179        # Disable alerts for credential issues
    180        "signon.management.page.breach-alerts.enabled": False,
    181        "signon.management.page.vulnerable-passwords.enabled": False,
    182        # Prevent starting into safe mode after application crashes
    183        # Do not show TOU new user modal which can interfere with tests
    184        "termsofuse.bypassNotification": True,
    185        "toolkit.startup.max_resumed_crashes": -1,
    186        # Disable most telemetry pings
    187        "toolkit.telemetry.server": "https://%(server)s/telemetry-dummy/",
    188        # Disable window occlusion on Windows, see Bug 1802473.
    189        "widget.windows.window_occlusion_tracking.enabled": False,
    190    }
    191 
    192    def __init__(
    193        self,
    194        host=None,
    195        port=None,
    196        bin=None,
    197        profile=None,
    198        addons=None,
    199        app_args=None,
    200        debugger_info=None,
    201        symbols_path=None,
    202        gecko_log=None,
    203        prefs=None,
    204        workspace=None,
    205        verbose=0,
    206        headless=False,
    207    ):
    208        self.runner_class = Runner
    209        self.app_args = app_args or []
    210        self.debugger_info = debugger_info
    211        self.runner = None
    212        self.symbols_path = symbols_path
    213        self.binary = bin
    214 
    215        self.marionette_host = host
    216        self.marionette_port = port
    217        self.addons = addons
    218        self.prefs = prefs
    219        self.required_prefs = deepcopy(self.required_prefs)
    220        if prefs:
    221            self.required_prefs.update(prefs)
    222 
    223        self._gecko_log_option = gecko_log
    224        self._gecko_log = None
    225        self.verbose = verbose
    226        self.headless = headless
    227 
    228        # keep track of errors to decide whether instance is unresponsive
    229        self.unresponsive_count = 0
    230 
    231        # Alternative to default temporary directory
    232        self.workspace = workspace
    233 
    234        # Don't use the 'profile' property here, because sub-classes could add
    235        # further preferences and data, which would not be included in the new
    236        # profile
    237        self._profile = profile
    238 
    239    @property
    240    def gecko_log(self):
    241        if self._gecko_log:
    242            return self._gecko_log
    243 
    244        path = self._gecko_log_option
    245        if path != "-":
    246            if path is None:
    247                path = "gecko.log"
    248            elif os.path.isdir(path):
    249                fname = f"gecko-{time.time()}.log"
    250                path = os.path.join(path, fname)
    251 
    252            path = os.path.realpath(path)
    253            if os.access(path, os.F_OK):
    254                os.remove(path)
    255 
    256        self._gecko_log = path
    257        return self._gecko_log
    258 
    259    @property
    260    def profile(self):
    261        return self._profile
    262 
    263    @profile.setter
    264    def profile(self, value):
    265        self._update_profile(value)
    266 
    267    def _update_profile(self, profile=None, profile_name=None):
    268        """Check if the profile has to be created, or replaced.
    269 
    270        :param profile: A Profile instance to be used.
    271        :param name: Profile name to be used in the path.
    272        """
    273        if self.runner and self.runner.is_running():
    274            raise errors.MarionetteException(
    275                "The current profile can only be updated "
    276                "when the instance is not running"
    277            )
    278 
    279        if isinstance(profile, Profile):
    280            # Only replace the profile if it is not the current one
    281            if hasattr(self, "_profile") and profile is self._profile:
    282                return
    283 
    284        else:
    285            profile_args = self.profile_args
    286            profile_path = profile
    287 
    288            # If a path to a profile is given then clone it
    289            if isinstance(profile_path, str):
    290                profile_args["path_from"] = profile_path
    291                profile_args["path_to"] = tempfile.mkdtemp(
    292                    suffix=f".{profile_name or os.path.basename(profile_path)}",
    293                    dir=self.workspace,
    294                )
    295                # The target must not exist yet
    296                os.rmdir(profile_args["path_to"])
    297 
    298                profile = Profile.clone(**profile_args)
    299 
    300            # Otherwise create a new profile
    301            else:
    302                profile_args["profile"] = tempfile.mkdtemp(
    303                    suffix=".{}".format(profile_name or "mozrunner"),
    304                    dir=self.workspace,
    305                )
    306                profile = Profile(**profile_args)
    307                profile.create_new = True
    308 
    309        if isinstance(self.profile, Profile):
    310            self.profile.cleanup()
    311 
    312        self._profile = profile
    313 
    314    def switch_profile(self, profile_name=None, clone_from=None):
    315        """Switch the profile by using the given name, and optionally clone it.
    316 
    317        Compared to :attr:`profile` this method allows to switch the profile
    318        by giving control over the profile name as used for the new profile. It
    319        also always creates a new blank profile, or as clone of an existent one.
    320 
    321        :param profile_name: Optional, name of the profile, which will be used
    322            as part of the profile path (folder name containing the profile).
    323        :clone_from: Optional, if specified the new profile will be cloned
    324            based on the given profile. This argument can be an instance of
    325            ``mozprofile.Profile``, or the path of the profile.
    326        """
    327        if isinstance(clone_from, Profile):
    328            clone_from = clone_from.profile
    329 
    330        self._update_profile(clone_from, profile_name=profile_name)
    331 
    332    @property
    333    def profile_args(self):
    334        args = {"preferences": deepcopy(self.required_prefs)}
    335        args["preferences"]["marionette.port"] = self.marionette_port
    336        args["preferences"]["marionette.defaultPrefs.port"] = self.marionette_port
    337 
    338        if self.prefs:
    339            args["preferences"].update(self.prefs)
    340 
    341        if self.verbose:
    342            level = "Trace" if self.verbose >= 2 else "Debug"
    343            args["preferences"]["remote.log.level"] = level
    344 
    345        if "-jsdebugger" in self.app_args:
    346            args["preferences"].update({
    347                "devtools.browsertoolbox.panel": "jsdebugger",
    348                "devtools.chrome.enabled": True,
    349                "devtools.debugger.prompt-connection": False,
    350                "devtools.debugger.remote-enabled": True,
    351                "devtools.testing": True,
    352            })
    353 
    354        if self.addons:
    355            args["addons"] = self.addons
    356 
    357        return args
    358 
    359    @classmethod
    360    def create(cls, app=None, *args, **kwargs):
    361        try:
    362            if not app and kwargs["bin"] is not None:
    363                app_id = mozversion.get_version(binary=kwargs["bin"])["application_id"]
    364                app = app_ids[app_id]
    365 
    366            instance_class = apps[app]
    367        except (OSError, KeyError):
    368            exc, val, tb = sys.exc_info()
    369            msg = f'Application "{app}" unknown (should be one of {list(apps.keys())})'
    370            raise NotImplementedError(msg).with_traceback(tb)
    371 
    372        return instance_class(*args, **kwargs)
    373 
    374    def start(self):
    375        self._update_profile(self.profile)
    376        self.runner = self.runner_class(**self._get_runner_args())
    377 
    378        # debugger information
    379        debug_args = None
    380        interactive = False
    381 
    382        if self.debugger_info:
    383            debug_args = [self.debugger_info.path] + self.debugger_info.args
    384            interactive = self.debugger_info.interactive
    385 
    386        self.runner.start(debug_args, interactive)
    387 
    388    def _get_runner_args(self):
    389        process_args = {
    390            "processOutputLine": [NullOutput()],
    391            "universal_newlines": True,
    392        }
    393 
    394        if self.gecko_log == "-":
    395            if getattr(sys.stdout, "encoding") == "utf-8":
    396                process_args["stream"] = sys.stdout
    397            elif hasattr(sys.stdout, "buffer"):
    398                process_args["stream"] = codecs.getwriter("utf-8")(sys.stdout.buffer)
    399            elif isinstance(sys.stdout, io.TextIOBase):
    400                # If sys.stdout expects unicode strings, we can't wrap it because the
    401                # wrapper will write byte strings. This can happen when e.g. tests
    402                # replace sys.stdout with a io.StringIO().
    403                process_args["stream"] = sys.stdout
    404            else:
    405                process_args["stream"] = codecs.getwriter("utf-8")(sys.stdout)
    406        else:
    407            process_args["logfile"] = self.gecko_log
    408 
    409        env = os.environ.copy()
    410 
    411        # Store all required preferences for tests which need to create clean profiles.
    412        required_prefs_keys = list(self.required_prefs.keys())
    413        env["MOZ_MARIONETTE_REQUIRED_PREFS"] = json.dumps(required_prefs_keys)
    414 
    415        if self.headless:
    416            env["MOZ_HEADLESS"] = "1"
    417            env["DISPLAY"] = "77"  # Set a fake display.
    418 
    419        # environment variables needed for crashreporting
    420        # https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting
    421        env.update({
    422            "MOZ_CRASHREPORTER": "1",
    423            "MOZ_CRASHREPORTER_NO_REPORT": "1",
    424            "MOZ_CRASHREPORTER_SHUTDOWN": "1",
    425        })
    426 
    427        extra_args = ["-marionette", "-remote-allow-system-access"]
    428        args = {
    429            "binary": self.binary,
    430            "profile": self.profile,
    431            "cmdargs": extra_args + self.app_args,
    432            "env": env,
    433            "symbols_path": self.symbols_path,
    434            "process_args": process_args,
    435        }
    436 
    437        if sys.platform.startswith("darwin"):
    438            # Bug 1887666: The custom process handler class for Marionette is
    439            # only supported on MacOS at the moment.
    440            args["process_class"] = ProcessHandler
    441 
    442        return args
    443 
    444    def close(self, clean=False):
    445        """
    446        Close the managed Gecko process.
    447 
    448        Depending on self.runner_class, setting `clean` to True may also kill
    449        the emulator process in which this instance is running.
    450 
    451        :param clean: If True, also perform runner cleanup.
    452        """
    453        if self.runner:
    454            self.runner.stop()
    455            if clean:
    456                self.runner.cleanup()
    457 
    458        if clean:
    459            if isinstance(self.profile, Profile):
    460                self.profile.cleanup()
    461            self.profile = None
    462 
    463    def restart(self, prefs=None, clean=True):
    464        """
    465        Close then start the managed Gecko process.
    466 
    467        :param prefs: Dictionary of preference names and values.
    468        :param clean: If True, reset the profile before starting.
    469        """
    470        if prefs:
    471            self.prefs = prefs
    472        else:
    473            self.prefs = None
    474 
    475        self.close(clean=clean)
    476        self.start()
    477 
    478    def update_process(self, pid, timeout=None):
    479        """Update the process to track when the application re-launched itself"""
    480        if sys.platform.startswith("darwin"):
    481            # The new process handler is only supported on MacOS yet
    482            returncode = self.runner.process_handler.update_process(pid, timeout)
    483            if returncode not in [0, UNKNOWN_RETURNCODE]:
    484                raise OSError(
    485                    f"Old process inappropriately quit with exit code: {returncode}"
    486                )
    487 
    488        else:
    489            returncode = self.runner.process_handler.check_for_detached(pid)
    490 
    491 
    492 class FennecInstance(GeckoInstance):
    493    fennec_prefs = {
    494        # Enable output for dump() and chrome console API
    495        "browser.dom.window.dump.enabled": True,
    496        "devtools.console.stdout.chrome": True,
    497        # Disable safe browsing / tracking protection updates
    498        "browser.safebrowsing.update.enabled": False,
    499        # Do not restore the last open set of tabs if the browser has crashed
    500        "browser.sessionstore.resume_from_crash": False,
    501    }
    502 
    503    def __init__(
    504        self,
    505        emulator_binary=None,
    506        avd_home=None,
    507        avd=None,
    508        adb_path=None,
    509        serial=None,
    510        connect_to_running_emulator=False,
    511        package_name=None,
    512        env=None,
    513        *args,
    514        **kwargs,
    515    ):
    516        required_prefs = deepcopy(FennecInstance.fennec_prefs)
    517        required_prefs.update(kwargs.get("prefs", {}))
    518 
    519        super().__init__(*args, **kwargs)
    520        self.required_prefs.update(required_prefs)
    521 
    522        self.runner_class = FennecEmulatorRunner
    523        # runner args
    524        self._package_name = package_name
    525        self.emulator_binary = emulator_binary
    526        self.avd_home = avd_home
    527        self.adb_path = adb_path
    528        self.avd = avd
    529        self.env = env
    530        self.serial = serial
    531        self.connect_to_running_emulator = connect_to_running_emulator
    532 
    533    @property
    534    def package_name(self):
    535        """
    536        Name of app to run on emulator.
    537 
    538        Note that FennecInstance does not use self.binary
    539        """
    540        if self._package_name is None:
    541            self._package_name = "org.mozilla.fennec"
    542            user = os.getenv("USER")
    543            if user:
    544                self._package_name += "_" + user
    545        return self._package_name
    546 
    547    def start(self):
    548        self._update_profile(self.profile)
    549        self.runner = self.runner_class(**self._get_runner_args())
    550        try:
    551            if self.connect_to_running_emulator:
    552                self.runner.device.connect()
    553            self.runner.start()
    554        except Exception:
    555            exc_cls, exc, tb = sys.exc_info()
    556            raise exc_cls(
    557                f"Error possibly due to runner or device args: {exc}"
    558            ).with_traceback(tb)
    559 
    560        # forward marionette port
    561        self.runner.device.device.forward(
    562            local=f"tcp:{self.marionette_port}",
    563            remote=f"tcp:{self.marionette_port}",
    564        )
    565 
    566    def _get_runner_args(self):
    567        process_args = {
    568            "processOutputLine": [NullOutput()],
    569            "universal_newlines": True,
    570        }
    571 
    572        env = {} if self.env is None else self.env.copy()
    573 
    574        runner_args = {
    575            "app": self.package_name,
    576            "avd_home": self.avd_home,
    577            "adb_path": self.adb_path,
    578            "binary": self.emulator_binary,
    579            "env": env,
    580            "profile": self.profile,
    581            "cmdargs": ["-marionette"] + self.app_args,
    582            "symbols_path": self.symbols_path,
    583            "process_args": process_args,
    584            "logdir": self.workspace or os.getcwd(),
    585            "serial": self.serial,
    586        }
    587        if self.avd:
    588            runner_args["avd"] = self.avd
    589 
    590        return runner_args
    591 
    592    def close(self, clean=False):
    593        """
    594        Close the managed Gecko process.
    595 
    596        If `clean` is True and the Fennec instance is running in an
    597        emulator managed by mozrunner, this will stop the emulator.
    598 
    599        :param clean: If True, also perform runner cleanup.
    600        """
    601        super().close(clean)
    602        if clean and self.runner and self.runner.device.connected:
    603            try:
    604                self.runner.device.device.remove_forwards(f"tcp:{self.marionette_port}")
    605                self.unresponsive_count = 0
    606            except Exception:
    607                self.unresponsive_count += 1
    608                traceback.print_exception(*sys.exc_info())
    609 
    610 
    611 class DesktopInstance(GeckoInstance):
    612    desktop_prefs = {
    613        # Disable Firefox old build background check
    614        "app.update.checkInstallTime": False,
    615        # Disable automatically upgrading Firefox
    616        #
    617        # Note: Possible update tests could reset or flip the value to allow
    618        # updates to be downloaded and applied.
    619        "app.update.disabledForTesting": True,
    620        # !!! For backward compatibility up to Firefox 64. Only remove
    621        # when this Firefox version is no longer supported by the client !!!
    622        "app.update.auto": False,
    623        # Disable the profile backup service.
    624        "browser.backup.enabled": False,
    625        # Don't show the content blocking introduction panel
    626        # We use a larger number than the default 22 to have some buffer
    627        # This can be removed once Firefox 69 and 68 ESR and are no longer supported.
    628        "browser.contentblocking.introCount": 99,
    629        # Enable output for dump() and chrome console API
    630        "browser.dom.window.dump.enabled": True,
    631        "devtools.console.stdout.chrome": True,
    632        # Indicate that the download panel has been shown once so that whichever
    633        # download test runs first doesn"t show the popup inconsistently
    634        "browser.download.panel.shown": True,
    635        # Do not show the EULA notification which can interfer with tests
    636        "browser.EULA.override": True,
    637        # Disable all machine learning features by default
    638        "browser.ml.enable": False,
    639        # Do not initialize any activitystream features
    640        "browser.newtabpage.activity-stream.testing.shouldInitializeFeeds": False,
    641        # Background thumbnails in particular cause grief, and disabling thumbnails
    642        # in general can"t hurt - we re-enable them when tests need them
    643        "browser.pagethumbnails.capturing_disabled": True,
    644        # Disable safe browsing / tracking protection updates
    645        "browser.safebrowsing.update.enabled": False,
    646        # Disable updates to search engines
    647        "browser.search.update": False,
    648        # Do not restore the last open set of tabs if the browser has crashed
    649        "browser.sessionstore.resume_from_crash": False,
    650        # Don't check for the default web browser during startup
    651        "browser.shell.checkDefaultBrowser": False,
    652        # Disable session restore infobar
    653        "browser.startup.couldRestoreSession.count": -1,
    654        # Needed for branded builds to prevent opening a second tab on startup
    655        "browser.startup.homepage_override.mstone": "ignore",
    656        # Start with a blank page by default
    657        "browser.startup.page": 0,
    658        # Unload the previously selected tab immediately
    659        "browser.tabs.remote.unloadDelayMs": 0,
    660        # Don't unload tabs when available memory is running low
    661        "browser.tabs.unloadOnLowMemory": False,
    662        # Do not warn when closing all open tabs
    663        "browser.tabs.warnOnClose": False,
    664        # Do not warn when closing all other open tabs
    665        "browser.tabs.warnOnCloseOtherTabs": False,
    666        # Do not warn when multiple tabs will be opened
    667        "browser.tabs.warnOnOpen": False,
    668        # Don't show the Bookmarks Toolbar on any tab (the above pref that
    669        # disables the New Tab Page ends up showing the toolbar on about:blank).
    670        "browser.toolbars.bookmarks.visibility": "never",
    671        # Disable the UI tour
    672        "browser.uitour.enabled": False,
    673        # Turn off Merino suggestions in the location bar so as not to trigger network
    674        # connections.
    675        "browser.urlbar.merino.endpointURL": "",
    676        # Turn off search suggestions in the location bar so as not to trigger network
    677        # connections.
    678        "browser.urlbar.suggest.searches": False,
    679        # Don't warn when exiting the browser
    680        "browser.warnOnQuit": False,
    681        # Disable the QoS manager on MacOS and the priority manager on all other
    682        # platforms to not cause stalled processes in background tabs when the
    683        # overall CPU load on the machine is high.
    684        #
    685        # TODO: Should be considered to get removed once bug 1960741 is fixed.
    686        "threads.lower_mainthread_priority_in_background.enabled": False,
    687        "dom.ipc.processPriorityManager.enabled": False,
    688        # Turn off semantic history search as it triggers network connections to
    689        # download ML models.
    690        "places.semanticHistory.featureGate": False,
    691        # Disable first-run welcome page
    692        "startup.homepage_welcome_url": "about:blank",
    693        "startup.homepage_welcome_url.additional": "",
    694    }
    695 
    696    def __init__(self, *args, **kwargs):
    697        required_prefs = deepcopy(DesktopInstance.desktop_prefs)
    698        required_prefs.update(kwargs.get("prefs", {}))
    699 
    700        super().__init__(*args, **kwargs)
    701        self.required_prefs.update(required_prefs)
    702 
    703 
    704 class ThunderbirdInstance(GeckoInstance):
    705    def __init__(self, *args, **kwargs):
    706        super().__init__(*args, **kwargs)
    707        try:
    708            # Copied alongside in the test archive
    709            from .thunderbirdinstance import thunderbird_prefs
    710        except ImportError:
    711            try:
    712                # Directly from the source tree
    713                here = os.path.dirname(__file__)
    714                sys.path.append(
    715                    os.path.join(here, "../../../../comm/testing/marionette")
    716                )
    717                from thunderbirdinstance import thunderbird_prefs
    718            except ImportError:
    719                thunderbird_prefs = {}
    720        self.required_prefs.update(thunderbird_prefs)
    721 
    722 
    723 class NullOutput:
    724    def __call__(self, line):
    725        pass
    726 
    727 
    728 apps = {
    729    "fennec": FennecInstance,
    730    "fxdesktop": DesktopInstance,
    731    "thunderbird": ThunderbirdInstance,
    732 }
    733 
    734 app_ids = {
    735    "{aa3c5121-dab2-40e2-81ca-7ea25febc110}": "fennec",
    736    "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}": "fxdesktop",
    737    "{3550f703-e582-4d05-9a08-453d09bdfdc6}": "thunderbird",
    738 }