| import abc |
| import errno |
| import os |
| import platform |
| import socket |
| import time |
| import traceback |
| from typing import ClassVar, Type |
| |
| import mozprocess |
| |
| from .browsers.base import OutputHandler |
| |
| |
| __all__ = ["SeleniumServer", "ChromeDriverServer", "CWTChromeDriverServer", |
| "EdgeChromiumDriverServer", "OperaDriverServer", |
| "InternetExplorerDriverServer", "EdgeDriverServer", |
| "ServoDriverServer", "WebKitDriverServer", "WebDriverServer"] |
| |
| |
| class WebDriverServer(object): |
| __metaclass__ = abc.ABCMeta |
| |
| default_base_path = "/" |
| output_handler_cls = OutputHandler # type: ClassVar[Type[OutputHandler]] |
| |
| def __init__(self, logger, binary, host="127.0.0.1", port=None, |
| base_path="", env=None, args=None): |
| if binary is None: |
| raise ValueError("WebDriver server binary must be given " |
| "to --webdriver-binary argument") |
| |
| self.logger = logger |
| self.binary = binary |
| self.host = host |
| |
| if base_path == "": |
| self.base_path = self.default_base_path |
| else: |
| self.base_path = base_path |
| self.env = os.environ.copy() if env is None else env |
| |
| self._output_handler = None |
| self._port = port |
| self._cmd = None |
| self._args = args if args is not None else [] |
| self._proc = None |
| |
| @abc.abstractmethod |
| def make_command(self): |
| """Returns the full command for starting the server process as a list.""" |
| |
| def start(self, |
| block=False, |
| output_handler_kwargs=None, |
| output_handler_start_kwargs=None): |
| try: |
| self._run(block, output_handler_kwargs, output_handler_start_kwargs) |
| except KeyboardInterrupt: |
| self.stop() |
| |
| def _run(self, block, output_handler_kwargs, output_handler_start_kwargs): |
| if output_handler_kwargs is None: |
| output_handler_kwargs = {} |
| if output_handler_start_kwargs is None: |
| output_handler_start_kwargs = {} |
| self._cmd = self.make_command() |
| self._output_handler = self.output_handler_cls(self.logger, |
| self._cmd, |
| **output_handler_kwargs) |
| self._proc = mozprocess.ProcessHandler( |
| self._cmd, |
| processOutputLine=self._output_handler, |
| env=self.env, |
| storeOutput=False) |
| |
| self.logger.debug("Starting WebDriver: %s" % ' '.join(self._cmd)) |
| try: |
| self._proc.run() |
| except OSError as e: |
| if e.errno == errno.ENOENT: |
| raise IOError( |
| "WebDriver executable not found: %s" % self.binary) |
| raise |
| self._output_handler.after_process_start(self._proc.pid) |
| |
| self.logger.debug( |
| "Waiting for WebDriver to become accessible: %s" % self.url) |
| try: |
| wait_for_service((self.host, self.port)) |
| except Exception: |
| self.logger.error( |
| "WebDriver was not accessible " |
| "within the timeout:\n%s" % traceback.format_exc()) |
| raise |
| self._output_handler.start(**output_handler_start_kwargs) |
| if block: |
| self._proc.wait() |
| |
| def stop(self, force=False): |
| self.logger.debug("Stopping WebDriver") |
| clean = True |
| if self.is_alive(): |
| kill_result = self._proc.kill() |
| if force and kill_result != 0: |
| clean = False |
| self._proc.kill(9) |
| success = not self.is_alive() |
| if success and self._output_handler is not None: |
| # Only try to do output post-processing if we managed to shut down |
| self._output_handler.after_process_stop(clean) |
| self._output_handler = None |
| return success |
| |
| def is_alive(self): |
| return hasattr(self._proc, "proc") and self._proc.poll() is None |
| |
| @property |
| def pid(self): |
| if self._proc is not None: |
| return self._proc.pid |
| |
| @property |
| def url(self): |
| return "http://%s:%i%s" % (self.host, self.port, self.base_path) |
| |
| @property |
| def port(self): |
| if self._port is None: |
| self._port = get_free_port() |
| return self._port |
| |
| |
| class SeleniumServer(WebDriverServer): |
| default_base_path = "/wd/hub" |
| |
| def make_command(self): |
| return ["java", "-jar", self.binary, "-port", str(self.port)] + self._args |
| |
| |
| class ChromeDriverServer(WebDriverServer): |
| def make_command(self): |
| return [self.binary, |
| cmd_arg("port", str(self.port)), |
| cmd_arg("url-base", self.base_path) if self.base_path else "", |
| cmd_arg("enable-chrome-logs")] + self._args |
| |
| |
| class CWTChromeDriverServer(WebDriverServer): |
| def make_command(self): |
| return [self.binary, |
| "--port=%s" % str(self.port)] + self._args |
| |
| |
| class EdgeChromiumDriverServer(WebDriverServer): |
| def make_command(self): |
| return [self.binary, |
| cmd_arg("port", str(self.port)), |
| cmd_arg("url-base", self.base_path) if self.base_path else ""] + self._args |
| |
| |
| class EdgeDriverServer(WebDriverServer): |
| def make_command(self): |
| return [self.binary, |
| "--port=%s" % str(self.port)] + self._args |
| |
| |
| class OperaDriverServer(ChromeDriverServer): |
| pass |
| |
| class InternetExplorerDriverServer(WebDriverServer): |
| def make_command(self): |
| return [self.binary, |
| "--port=%s" % str(self.port)] + self._args |
| |
| |
| class SafariDriverServer(WebDriverServer): |
| def make_command(self): |
| return [self.binary, |
| "--port=%s" % str(self.port)] + self._args |
| |
| |
| class ServoDriverServer(WebDriverServer): |
| def __init__(self, logger, binary="servo", binary_args=None, host="127.0.0.1", |
| port=None, env=None, args=None): |
| env = env if env is not None else os.environ.copy() |
| env["RUST_BACKTRACE"] = "1" |
| WebDriverServer.__init__(self, logger, binary, |
| host=host, |
| port=port, |
| env=env, |
| args=args) |
| self.binary_args = binary_args |
| |
| def make_command(self): |
| command = [self.binary, |
| "--webdriver=%s" % self.port, |
| "--hard-fail", |
| "--headless"] + self._args |
| if self.binary_args: |
| command += self.binary_args |
| return command |
| |
| |
| class WebKitDriverServer(WebDriverServer): |
| def make_command(self): |
| return [self.binary, "--port=%s" % str(self.port)] + self._args |
| |
| |
| def cmd_arg(name, value=None): |
| prefix = "-" if platform.system() == "Windows" else "--" |
| rv = prefix + name |
| if value is not None: |
| rv += "=" + value |
| return rv |
| |
| |
| def get_free_port(): |
| """Get a random unbound port""" |
| while True: |
| s = socket.socket() |
| try: |
| s.bind(("127.0.0.1", 0)) |
| except OSError: |
| continue |
| else: |
| return s.getsockname()[1] |
| finally: |
| s.close() |
| |
| |
| def wait_for_service(addr, timeout=60): |
| """Waits until network service given as a tuple of (host, port) becomes |
| available or the `timeout` duration is reached, at which point |
| ``socket.timeout`` is raised.""" |
| end = time.time() + timeout |
| while end > time.time(): |
| so = socket.socket() |
| try: |
| so.connect(addr) |
| except socket.timeout: |
| pass |
| except OSError as e: |
| if e.errno != errno.ECONNREFUSED: |
| raise |
| else: |
| return True |
| finally: |
| so.close() |
| time.sleep(0.5) |
| raise socket.timeout("Service is unavailable: %s:%i" % addr) |