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
5import os
6import platform
7import subprocess
8import sys
9
10import mozinfo
11from mozprocess import ProcessHandler
12from mozprofile import FirefoxProfile, Preferences
13from mozprofile.permissions import ServerLocations
14from mozrunner import FirefoxRunner
15from mozrunner.utils import get_stack_fixer_function
16from mozcrash import mozcrash
17
18from .base import (get_free_port,
19                   Browser,
20                   ExecutorBrowser,
21                   require_arg,
22                   cmd_arg,
23                   browser_command)
24from ..executors import executor_kwargs as base_executor_kwargs
25from ..executors.executormarionette import (MarionetteTestharnessExecutor,
26                                            MarionetteRefTestExecutor,
27                                            MarionetteWdspecExecutor)
28from ..environment import hostnames
29
30
31here = os.path.join(os.path.split(__file__)[0])
32
33__wptrunner__ = {"product": "firefox",
34                 "check_args": "check_args",
35                 "browser": "FirefoxBrowser",
36                 "executor": {"testharness": "MarionetteTestharnessExecutor",
37                              "reftest": "MarionetteRefTestExecutor",
38                              "wdspec": "MarionetteWdspecExecutor"},
39                 "browser_kwargs": "browser_kwargs",
40                 "executor_kwargs": "executor_kwargs",
41                 "env_options": "env_options",
42                 "run_info_extras": "run_info_extras",
43                 "update_properties": "update_properties"}
44
45
46def check_args(**kwargs):
47    require_arg(kwargs, "binary")
48    if kwargs["ssl_type"] != "none":
49        require_arg(kwargs, "certutil_binary")
50
51
52def browser_kwargs(**kwargs):
53    return {"binary": kwargs["binary"],
54            "prefs_root": kwargs["prefs_root"],
55            "debug_info": kwargs["debug_info"],
56            "symbols_path": kwargs["symbols_path"],
57            "stackwalk_binary": kwargs["stackwalk_binary"],
58            "certutil_binary": kwargs["certutil_binary"],
59            "ca_certificate_path": kwargs["ssl_env"].ca_cert_path(),
60            "e10s": kwargs["gecko_e10s"],
61            "stackfix_dir": kwargs["stackfix_dir"]}
62
63
64def executor_kwargs(test_type, server_config, cache_manager, run_info_data,
65                    **kwargs):
66    executor_kwargs = base_executor_kwargs(test_type, server_config,
67                                           cache_manager, **kwargs)
68    executor_kwargs["close_after_done"] = test_type != "reftest"
69    if kwargs["timeout_multiplier"] is None:
70        if test_type == "reftest":
71            if run_info_data["debug"] or run_info_data.get("asan"):
72                executor_kwargs["timeout_multiplier"] = 4
73            else:
74                executor_kwargs["timeout_multiplier"] = 2
75        elif run_info_data["debug"] or run_info_data.get("asan"):
76            executor_kwargs["timeout_multiplier"] = 3
77    if test_type == "wdspec":
78        executor_kwargs["webdriver_binary"] = kwargs.get("webdriver_binary")
79    return executor_kwargs
80
81
82def env_options():
83    return {"host": "127.0.0.1",
84            "external_host": "web-platform.test",
85            "bind_hostname": "false",
86            "certificate_domain": "web-platform.test",
87            "supports_debugger": True}
88
89
90def run_info_extras(**kwargs):
91    return {"e10s": kwargs["gecko_e10s"]}
92
93
94def update_properties():
95    return ["debug", "e10s", "os", "version", "processor", "bits"], {"debug", "e10s"}
96
97
98class FirefoxBrowser(Browser):
99    used_ports = set()
100    init_timeout = 60
101
102    def __init__(self, logger, binary, prefs_root, debug_info=None,
103                 symbols_path=None, stackwalk_binary=None, certutil_binary=None,
104                 ca_certificate_path=None, e10s=False, stackfix_dir=None):
105        Browser.__init__(self, logger)
106        self.binary = binary
107        self.prefs_root = prefs_root
108        self.marionette_port = None
109        self.runner = None
110        self.debug_info = debug_info
111        self.profile = None
112        self.symbols_path = symbols_path
113        self.stackwalk_binary = stackwalk_binary
114        self.ca_certificate_path = ca_certificate_path
115        self.certutil_binary = certutil_binary
116        self.e10s = e10s
117        if self.symbols_path and stackfix_dir:
118            self.stack_fixer = get_stack_fixer_function(stackfix_dir,
119                                                        self.symbols_path)
120        else:
121            self.stack_fixer = None
122
123    def start(self):
124        self.marionette_port = get_free_port(2828, exclude=self.used_ports)
125        self.used_ports.add(self.marionette_port)
126
127        env = os.environ.copy()
128        env["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1"
129
130        locations = ServerLocations(filename=os.path.join(here, "server-locations.txt"))
131
132        preferences = self.load_prefs()
133
134        self.profile = FirefoxProfile(locations=locations,
135                                      preferences=preferences)
136        self.profile.set_preferences({"marionette.defaultPrefs.enabled": True,
137                                      "marionette.defaultPrefs.port": self.marionette_port,
138                                      "dom.disable_open_during_load": False,
139                                      "network.dns.localDomains": ",".join(hostnames),
140                                      "network.proxy.type": 0,
141                                      "places.history.enabled": False})
142        if self.e10s:
143            self.profile.set_preferences({"browser.tabs.remote.autostart": True})
144
145        # Bug 1262954: winxp + e10s, disable hwaccel
146        if (self.e10s and platform.system() in ("Windows", "Microsoft") and
147            '5.1' in platform.version()):
148            self.profile.set_preferences({"layers.acceleration.disabled": True})
149
150        if self.ca_certificate_path is not None:
151            self.setup_ssl()
152
153        debug_args, cmd = browser_command(self.binary, [cmd_arg("marionette"), "about:blank"],
154                                          self.debug_info)
155
156        self.runner = FirefoxRunner(profile=self.profile,
157                                    binary=cmd[0],
158                                    cmdargs=cmd[1:],
159                                    env=env,
160                                    process_class=ProcessHandler,
161                                    process_args={"processOutputLine": [self.on_output]})
162
163        self.logger.debug("Starting Firefox")
164
165        self.runner.start(debug_args=debug_args, interactive=self.debug_info and self.debug_info.interactive)
166        self.logger.debug("Firefox Started")
167
168    def load_prefs(self):
169        prefs_path = os.path.join(self.prefs_root, "prefs_general.js")
170        if os.path.exists(prefs_path):
171            preferences = Preferences.read_prefs(prefs_path)
172        else:
173            self.logger.warning("Failed to find base prefs file in %s" % prefs_path)
174            preferences = []
175
176        return preferences
177
178    def stop(self):
179        self.logger.debug("Stopping browser")
180        if self.runner is not None:
181            try:
182                self.runner.stop()
183            except OSError:
184                # This can happen on Windows if the process is already dead
185                pass
186
187    def pid(self):
188        if self.runner.process_handler is None:
189            return None
190
191        try:
192            return self.runner.process_handler.pid
193        except AttributeError:
194            return None
195
196    def on_output(self, line):
197        """Write a line of output from the firefox process to the log"""
198        data = line.decode("utf8", "replace")
199        if self.stack_fixer:
200            data = self.stack_fixer(data)
201        self.logger.process_output(self.pid(),
202                                   data,
203                                   command=" ".join(self.runner.command))
204
205    def is_alive(self):
206        if self.runner:
207            return self.runner.is_running()
208        return False
209
210    def cleanup(self):
211        self.stop()
212
213    def executor_browser(self):
214        assert self.marionette_port is not None
215        return ExecutorBrowser, {"marionette_port": self.marionette_port}
216
217    def log_crash(self, process, test):
218        dump_dir = os.path.join(self.profile.profile, "minidumps")
219
220        mozcrash.log_crashes(self.logger,
221                             dump_dir,
222                             symbols_path=self.symbols_path,
223                             stackwalk_binary=self.stackwalk_binary,
224                             process=process,
225                             test=test)
226
227    def setup_ssl(self):
228        """Create a certificate database to use in the test profile. This is configured
229        to trust the CA Certificate that has signed the web-platform.test server
230        certificate."""
231
232        self.logger.info("Setting up ssl")
233
234        # Make sure the certutil libraries from the source tree are loaded when using a
235        # local copy of certutil
236        # TODO: Maybe only set this if certutil won't launch?
237        env = os.environ.copy()
238        certutil_dir = os.path.dirname(self.binary)
239        if mozinfo.isMac:
240            env_var = "DYLD_LIBRARY_PATH"
241        elif mozinfo.isUnix:
242            env_var = "LD_LIBRARY_PATH"
243        else:
244            env_var = "PATH"
245
246
247        env[env_var] = (os.path.pathsep.join([certutil_dir, env[env_var]])
248                        if env_var in env else certutil_dir).encode(
249                                sys.getfilesystemencoding() or 'utf-8', 'replace')
250
251        def certutil(*args):
252            cmd = [self.certutil_binary] + list(args)
253            self.logger.process_output("certutil",
254                                       subprocess.check_output(cmd,
255                                                               env=env,
256                                                               stderr=subprocess.STDOUT),
257                                       " ".join(cmd))
258
259        pw_path = os.path.join(self.profile.profile, ".crtdbpw")
260        with open(pw_path, "w") as f:
261            # Use empty password for certificate db
262            f.write("\n")
263
264        cert_db_path = self.profile.profile
265
266        # Create a new certificate db
267        certutil("-N", "-d", cert_db_path, "-f", pw_path)
268
269        # Add the CA certificate to the database and mark as trusted to issue server certs
270        certutil("-A", "-d", cert_db_path, "-f", pw_path, "-t", "CT,,",
271                 "-n", "web-platform-tests", "-i", self.ca_certificate_path)
272
273        # List all certs in the database
274        certutil("-L", "-d", cert_db_path)
275