Path: blob/trunk/py/selenium/webdriver/common/service.py
1864 views
# Licensed to the Software Freedom Conservancy (SFC) under one1# or more contributor license agreements. See the NOTICE file2# distributed with this work for additional information3# regarding copyright ownership. The SFC licenses this file4# to you under the Apache License, Version 2.0 (the5# "License"); you may not use this file except in compliance6# with the License. You may obtain a copy of the License at7#8# http://www.apache.org/licenses/LICENSE-2.09#10# Unless required by applicable law or agreed to in writing,11# software distributed under the License is distributed on an12# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13# KIND, either express or implied. See the License for the14# specific language governing permissions and limitations15# under the License.1617import errno18import logging19import os20import subprocess21import sys22from abc import ABC, abstractmethod23from collections.abc import Mapping24from io import IOBase25from subprocess import PIPE26from time import sleep27from typing import IO, Any, Optional, Union, cast28from urllib import request29from urllib.error import URLError3031from selenium.common.exceptions import WebDriverException32from selenium.types import SubprocessStdAlias33from selenium.webdriver.common import utils3435logger = logging.getLogger(__name__)363738class Service(ABC):39"""The abstract base class for all service objects. Services typically40launch a child program in a new process as an interim process to41communicate with a browser.4243:param executable: install path of the executable.44:param port: Port for the service to run on, defaults to 0 where the operating system will decide.45:param log_output: (Optional) int representation of STDOUT/DEVNULL, any IO instance or String path to file.46:param env: (Optional) Mapping of environment variables for the new process, defaults to `os.environ`.47:param driver_path_env_key: (Optional) Environment variable to use to get the path to the driver executable.48"""4950def __init__(51self,52executable_path: Optional[str] = None,53port: int = 0,54log_output: Optional[SubprocessStdAlias] = None,55env: Optional[Mapping[Any, Any]] = None,56driver_path_env_key: Optional[str] = None,57**kwargs,58) -> None:59self.log_output: Optional[Union[int, IOBase]]60if isinstance(log_output, str):61self.log_output = cast(IOBase, open(log_output, "a+", encoding="utf-8"))62elif log_output == subprocess.STDOUT:63self.log_output = None64elif log_output is None or log_output == subprocess.DEVNULL:65self.log_output = subprocess.DEVNULL66else:67self.log_output = cast(Union[int, IOBase], log_output)6869self.port = port or utils.free_port()70# Default value for every python subprocess: subprocess.Popen(..., creationflags=0)71self.popen_kw = kwargs.pop("popen_kw", {})72self.creation_flags = self.popen_kw.pop("creation_flags", 0)73self.env = env or os.environ74self.DRIVER_PATH_ENV_KEY = driver_path_env_key75self._path = self.env_path() or executable_path7677@property78def service_url(self) -> str:79"""Gets the url of the Service."""80return f"http://{utils.join_host_port('localhost', self.port)}"8182@abstractmethod83def command_line_args(self) -> list[str]:84"""A List of program arguments (excluding the executable)."""85raise NotImplementedError("This method needs to be implemented in a sub class")8687@property88def path(self) -> str:89return self._path or ""9091@path.setter92def path(self, value: str) -> None:93self._path = str(value)9495def start(self) -> None:96"""Starts the Service.9798:Exceptions:99- WebDriverException : Raised either when it can't start the service100or when it can't connect to the service101"""102if self._path is None:103raise WebDriverException("Service path cannot be None.")104self._start_process(self._path)105106count = 0107while True:108self.assert_process_still_running()109if self.is_connectable():110break111# sleep increasing: 0.01, 0.06, 0.11, 0.16, 0.21, 0.26, 0.31, 0.36, 0.41, 0.46, 0.5112sleep(min(0.01 + 0.05 * count, 0.5))113count += 1114if count == 70:115raise WebDriverException(f"Can not connect to the Service {self._path}")116117def assert_process_still_running(self) -> None:118"""Check if the underlying process is still running."""119return_code = self.process.poll()120if return_code:121raise WebDriverException(f"Service {self._path} unexpectedly exited. Status code was: {return_code}")122123def is_connectable(self) -> bool:124"""Establishes a socket connection to determine if the service running125on the port is accessible."""126return utils.is_connectable(self.port)127128def send_remote_shutdown_command(self) -> None:129"""Dispatch an HTTP request to the shutdown endpoint for the service in130an attempt to stop it."""131try:132request.urlopen(f"{self.service_url}/shutdown")133except URLError:134return135136for _ in range(30):137if not self.is_connectable():138break139sleep(1)140141def stop(self) -> None:142"""Stops the service."""143144if self.log_output not in {PIPE, subprocess.DEVNULL}:145if isinstance(self.log_output, IOBase):146self.log_output.close()147elif isinstance(self.log_output, int):148os.close(self.log_output)149150if self.process is not None and self.process.poll() is None:151try:152self.send_remote_shutdown_command()153except TypeError:154pass155finally:156self._terminate_process()157158def _terminate_process(self) -> None:159"""Terminate the child process.160161On POSIX this attempts a graceful SIGTERM followed by a SIGKILL,162on a Windows OS kill is an alias to terminate. Terminating does163not raise itself if something has gone wrong but (currently)164silently ignores errors here.165"""166try:167stdin, stdout, stderr = (168self.process.stdin,169self.process.stdout,170self.process.stderr,171)172for stream in stdin, stdout, stderr:173try:174stream.close() # type: ignore175except AttributeError:176pass177self.process.terminate()178try:179self.process.wait(60)180except subprocess.TimeoutExpired:181logger.error(182"Service process refused to terminate gracefully with SIGTERM, escalating to SIGKILL.",183exc_info=True,184)185self.process.kill()186except OSError:187logger.error("Error terminating service process.", exc_info=True)188189def __del__(self) -> None:190# `subprocess.Popen` doesn't send signal on `__del__`;191# so we attempt to close the launched process when `__del__`192# is triggered.193# do not use globals here; interpreter shutdown may have already cleaned them up194# and they would be `None`. This goes for anything this method is referencing internally.195try:196self.stop()197except Exception:198pass199200def _start_process(self, path: str) -> None:201"""Creates a subprocess by executing the command provided.202203:param cmd: full command to execute204"""205cmd = [path]206cmd.extend(self.command_line_args())207close_file_descriptors = self.popen_kw.pop("close_fds", sys.platform != "win32")208try:209start_info = None210if sys.platform == "win32":211start_info = subprocess.STARTUPINFO()212start_info.dwFlags = subprocess.CREATE_NEW_CONSOLE | subprocess.STARTF_USESHOWWINDOW213start_info.wShowWindow = subprocess.SW_HIDE214215self.process = subprocess.Popen(216cmd,217env=self.env,218close_fds=close_file_descriptors,219stdout=cast(Optional[Union[int, IO[Any]]], self.log_output),220stderr=cast(Optional[Union[int, IO[Any]]], self.log_output),221stdin=PIPE,222creationflags=self.creation_flags,223startupinfo=start_info,224**self.popen_kw,225)226logger.debug(227"Started executable: `%s` in a child process with pid: %s using %s to output %s",228self._path,229self.process.pid,230self.creation_flags,231self.log_output,232)233except TypeError:234raise235except OSError as err:236if err.errno == errno.EACCES:237if self._path is None:238raise WebDriverException("Service path cannot be None.")239raise WebDriverException(240f"'{os.path.basename(self._path)}' executable may have wrong permissions."241) from err242raise243244def env_path(self) -> Optional[str]:245if self.DRIVER_PATH_ENV_KEY:246return os.getenv(self.DRIVER_PATH_ENV_KEY, None)247return None248249250