1import abc
2import errno
3import os
4import platform
5import socket
6import time
7import traceback
8from typing import ClassVar, Type
9
10import mozprocess
11
12from .browsers.base import OutputHandler
13
14
15__all__ = ["SeleniumServer", "ChromeDriverServer", "CWTChromeDriverServer",
16           "EdgeChromiumDriverServer", "OperaDriverServer",
17           "InternetExplorerDriverServer", "EdgeDriverServer",
18           "ServoDriverServer", "WebKitDriverServer", "WebDriverServer"]
19
20
21class WebDriverServer(object):
22    __metaclass__ = abc.ABCMeta
23
24    default_base_path = "/"
25    output_handler_cls = OutputHandler  # type: ClassVar[Type[OutputHandler]]
26
27    def __init__(self, logger, binary, host="127.0.0.1", port=None,
28                 base_path="", env=None, args=None):
29        if binary is None:
30            raise ValueError("WebDriver server binary must be given "
31                             "to --webdriver-binary argument")
32
33        self.logger = logger
34        self.binary = binary
35        self.host = host
36
37        if base_path == "":
38            self.base_path = self.default_base_path
39        else:
40            self.base_path = base_path
41        self.env = os.environ.copy() if env is None else env
42
43        self._output_handler = None
44        self._port = port
45        self._cmd = None
46        self._args = args if args is not None else []
47        self._proc = None
48
49    @abc.abstractmethod
50    def make_command(self):
51        """Returns the full command for starting the server process as a list."""
52
53    def start(self,
54              block=False,
55              output_handler_kwargs=None,
56              output_handler_start_kwargs=None):
57        try:
58            self._run(block, output_handler_kwargs, output_handler_start_kwargs)
59        except KeyboardInterrupt:
60            self.stop()
61
62    def _run(self, block, output_handler_kwargs, output_handler_start_kwargs):
63        if output_handler_kwargs is None:
64            output_handler_kwargs = {}
65        if output_handler_start_kwargs is None:
66            output_handler_start_kwargs = {}
67        self._cmd = self.make_command()
68        self._output_handler = self.output_handler_cls(self.logger,
69                                                       self._cmd,
70                                                       **output_handler_kwargs)
71        self._proc = mozprocess.ProcessHandler(
72            self._cmd,
73            processOutputLine=self._output_handler,
74            env=self.env,
75            storeOutput=False)
76
77        self.logger.debug("Starting WebDriver: %s" % ' '.join(self._cmd))
78        try:
79            self._proc.run()
80        except OSError as e:
81            if e.errno == errno.ENOENT:
82                raise IOError(
83                    "WebDriver executable not found: %s" % self.binary)
84            raise
85        self._output_handler.after_process_start(self._proc.pid)
86
87        self.logger.debug(
88            "Waiting for WebDriver to become accessible: %s" % self.url)
89        try:
90            wait_for_service((self.host, self.port))
91        except Exception:
92            self.logger.error(
93                "WebDriver was not accessible "
94                "within the timeout:\n%s" % traceback.format_exc())
95            raise
96        self._output_handler.start(**output_handler_start_kwargs)
97        if block:
98            self._proc.wait()
99
100    def stop(self, force=False):
101        self.logger.debug("Stopping WebDriver")
102        clean = True
103        if self.is_alive():
104            kill_result = self._proc.kill()
105            if force and kill_result != 0:
106                clean = False
107                self._proc.kill(9)
108        success = not self.is_alive()
109        if success and self._output_handler is not None:
110            # Only try to do output post-processing if we managed to shut down
111            self._output_handler.after_process_stop(clean)
112            self._output_handler = None
113        return success
114
115    def is_alive(self):
116        return hasattr(self._proc, "proc") and self._proc.poll() is None
117
118    @property
119    def pid(self):
120        if self._proc is not None:
121            return self._proc.pid
122
123    @property
124    def url(self):
125        return "http://%s:%i%s" % (self.host, self.port, self.base_path)
126
127    @property
128    def port(self):
129        if self._port is None:
130            self._port = get_free_port()
131        return self._port
132
133
134class SeleniumServer(WebDriverServer):
135    default_base_path = "/wd/hub"
136
137    def make_command(self):
138        return ["java", "-jar", self.binary, "-port", str(self.port)] + self._args
139
140
141class ChromeDriverServer(WebDriverServer):
142    def make_command(self):
143        return [self.binary,
144                cmd_arg("port", str(self.port)),
145                cmd_arg("url-base", self.base_path) if self.base_path else "",
146                cmd_arg("enable-chrome-logs")] + self._args
147
148
149class CWTChromeDriverServer(WebDriverServer):
150    def make_command(self):
151        return [self.binary,
152                "--port=%s" % str(self.port)] + self._args
153
154
155class EdgeChromiumDriverServer(WebDriverServer):
156    def make_command(self):
157        return [self.binary,
158                cmd_arg("port", str(self.port)),
159                cmd_arg("url-base", self.base_path) if self.base_path else ""] + self._args
160
161
162class EdgeDriverServer(WebDriverServer):
163    def make_command(self):
164        return [self.binary,
165                "--port=%s" % str(self.port)] + self._args
166
167
168class OperaDriverServer(ChromeDriverServer):
169    pass
170
171
172class InternetExplorerDriverServer(WebDriverServer):
173    def make_command(self):
174        return [self.binary,
175                "--port=%s" % str(self.port)] + self._args
176
177
178class SafariDriverServer(WebDriverServer):
179    def make_command(self):
180        return [self.binary,
181                "--port=%s" % str(self.port)] + self._args
182
183
184class ServoDriverServer(WebDriverServer):
185    def __init__(self, logger, binary="servo", binary_args=None, host="127.0.0.1",
186                 port=None, env=None, args=None):
187        env = env if env is not None else os.environ.copy()
188        env["RUST_BACKTRACE"] = "1"
189        WebDriverServer.__init__(self, logger, binary,
190                                 host=host,
191                                 port=port,
192                                 env=env,
193                                 args=args)
194        self.binary_args = binary_args
195
196    def make_command(self):
197        command = [self.binary,
198                   "--webdriver=%s" % self.port,
199                   "--hard-fail",
200                   "--headless"] + self._args
201        if self.binary_args:
202            command += self.binary_args
203        return command
204
205
206class WebKitDriverServer(WebDriverServer):
207    def make_command(self):
208        return [self.binary, "--port=%s" % str(self.port)] + self._args
209
210
211def cmd_arg(name, value=None):
212    prefix = "-" if platform.system() == "Windows" else "--"
213    rv = prefix + name
214    if value is not None:
215        rv += "=" + value
216    return rv
217
218
219def get_free_port():
220    """Get a random unbound port"""
221    while True:
222        s = socket.socket()
223        try:
224            s.bind(("127.0.0.1", 0))
225        except OSError:
226            continue
227        else:
228            return s.getsockname()[1]
229        finally:
230            s.close()
231
232
233def wait_for_service(addr, timeout=60):
234    """Waits until network service given as a tuple of (host, port) becomes
235    available or the `timeout` duration is reached, at which point
236    ``socket.timeout`` is raised."""
237    end = time.time() + timeout
238    while end > time.time():
239        so = socket.socket()
240        try:
241            so.connect(addr)
242        except socket.timeout:
243            pass
244        except OSError as e:
245            if e.errno != errno.ECONNREFUSED:
246                raise
247        else:
248            return True
249        finally:
250            so.close()
251        time.sleep(0.5)
252    raise socket.timeout("Service is unavailable: %s:%i" % addr)
253