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