Skip to content

Commit ba18ecc

Browse files
committed
[py] add initial support for selenium manager
1 parent 08dcad6 commit ba18ecc

File tree

4 files changed

+172
-41
lines changed

4 files changed

+172
-41
lines changed

.github/workflows/ci-python.yml

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,8 @@ jobs:
134134
uses: actions/setup-python@v2
135135
with:
136136
python-version: 3.7.10
137-
- name: Setup Java
138-
uses: actions/setup-java@v1
139-
with:
140-
java-version: '11'
141-
- name: Setup Chrome and ChromeDriver
142-
uses: ./.github/actions/setup-chrome
137+
- name: Setup Chrome
138+
uses: browser-actions/setup-chrome@latest
143139
- name: Start XVFB
144140
run: Xvfb :99 &
145141
- name: Start Fluxbox
@@ -179,8 +175,11 @@ jobs:
179175
uses: actions/setup-java@v1
180176
with:
181177
java-version: '11'
182-
- name: Setup Firefox and GeckoDriver
183-
uses: ./.github/actions/setup-firefox
178+
- name: Setup Firefox
179+
uses: abhi1693/[email protected]
180+
with:
181+
browser: firefox
182+
version: latest
184183
- name: Start XVFB
185184
run: Xvfb :99 &
186185
- name: Start Fluxbox
@@ -219,8 +218,11 @@ jobs:
219218
uses: actions/setup-java@v1
220219
with:
221220
java-version: '11'
222-
- name: Setup Firefox and GeckoDriver
223-
uses: ./.github/actions/setup-firefox
221+
- name: Setup Firefox
222+
uses: abhi1693/[email protected]
223+
with:
224+
browser: firefox
225+
version: latest
224226
- name: Start Xvfb
225227
run: Xvfb :99 &
226228
- name: Start Fluxbox

py/BUILD.bazel

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,24 @@ TEST_DEPS = [
4545
requirement("zipp"),
4646
]
4747

48+
copy_file(
49+
name = "manager-macos",
50+
src = "//common/manager:macos/selenium-manager",
51+
out = "selenium/webdriver/common/macos/selenium-manager",
52+
)
53+
54+
copy_file(
55+
name = "manager-linux",
56+
src = "//common/manager:linux/selenium-manager",
57+
out = "selenium/webdriver/common/linux/selenium-manager",
58+
)
59+
60+
copy_file(
61+
name = "manager-windows",
62+
src = "//common/manager:windows/selenium-manager.exe",
63+
out = "selenium/webdriver/common/windows/selenium-manager.exe",
64+
)
65+
4866
copy_file(
4967
name = "get-attribute",
5068
src = "//javascript/webdriver/atoms:get-attribute.js",
@@ -106,6 +124,9 @@ py_library(
106124
":get-attribute",
107125
":is-displayed",
108126
":mutation-listener",
127+
":manager-linux",
128+
":manager-macos",
129+
":manager-windows",
109130
] + [":create-cdp-srcs-" + n for n in BROWSER_VERSIONS],
110131
imports = ["."],
111132
visibility = ["//visibility:public"],
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Licensed to the Software Freedom Conservancy (SFC) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The SFC licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
import logging
18+
import re
19+
import subprocess
20+
import sys
21+
from pathlib import Path
22+
from typing import Tuple
23+
24+
from selenium.common.exceptions import WebDriverException
25+
26+
logger = logging.getLogger(__name__)
27+
28+
29+
class SeleniumManager:
30+
"""
31+
Wrapper for getting information from the Selenium Manager binaries.
32+
"""
33+
34+
@staticmethod
35+
def get_binary() -> Path:
36+
"""
37+
Determines the path of the correct Selenium Manager binary.
38+
:Returns: The Selenium Manager executable location
39+
"""
40+
directory = sys.platform
41+
if directory == "darwin":
42+
directory = "macos"
43+
elif directory in ("win32", "cygwin"):
44+
directory = "windows"
45+
46+
file = "selenium-manager.exe" if directory == "windows" else "selenium-manager"
47+
48+
path = Path(__file__).parent.joinpath(directory, file)
49+
50+
if not path.is_file():
51+
raise WebDriverException("Unable to obtain Selenium Manager")
52+
53+
return path
54+
55+
@staticmethod
56+
def driver_location(browser: str) -> str:
57+
"""
58+
Determines the path of the correct driver.
59+
:Args:
60+
- browser: which browser to get the driver path for.
61+
:Returns: The driver path to use
62+
"""
63+
if browser not in ("chrome", "firefox"):
64+
raise WebDriverException(f"Unable to locate driver associated with browser name: {browser}")
65+
66+
args = (str(SeleniumManager.get_binary()), "--browser", browser)
67+
result = SeleniumManager.run(args)
68+
command = result.split("\t")[-1].strip()
69+
logger.debug(f"Using driver at: {command}")
70+
return command
71+
72+
@staticmethod
73+
def run(args: Tuple[str, str, str]) -> str:
74+
"""
75+
Executes the Selenium Manager Binary.
76+
:Args:
77+
- args: the components of the command being executed.
78+
:Returns: The log string containing the driver location.
79+
"""
80+
logger.debug(f"Executing selenium manager with: {args}")
81+
result = subprocess.run(args, stdout=subprocess.PIPE).stdout.decode("utf-8")
82+
83+
if not re.match("^INFO\t", result):
84+
raise WebDriverException(f"Unsuccessful command executed: {args}")
85+
86+
return result

py/selenium/webdriver/common/service.py

Lines changed: 53 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@
3232
from selenium.common.exceptions import WebDriverException
3333
from selenium.types import SubprocessStdAlias
3434
from selenium.webdriver.common import utils
35+
from selenium.webdriver.common.selenium_manager import SeleniumManager
3536

36-
log = logging.getLogger(__name__)
37+
logger = logging.getLogger(__name__)
3738

3839

3940
_HAS_NATIVE_DEVNULL = True
@@ -87,35 +88,19 @@ def start(self) -> None:
8788
or when it can't connect to the service
8889
"""
8990
try:
90-
cmd = [self.path]
91-
cmd.extend(self.command_line_args())
92-
self.process = subprocess.Popen(
93-
cmd,
94-
env=self.env,
95-
close_fds=system() != "Windows",
96-
stdout=self.log_file,
97-
stderr=self.log_file,
98-
stdin=PIPE,
99-
creationflags=self.creation_flags,
100-
)
101-
log.debug(f"Started executable: `{self.path}` in a child process with pid: {self.process.pid}")
102-
except TypeError:
103-
raise
104-
except OSError as err:
105-
if err.errno == errno.ENOENT:
106-
raise WebDriverException(
107-
f"'{os.path.basename(self.path)}' executable needs to be in PATH. {self.start_error_message}"
108-
)
109-
elif err.errno == errno.EACCES:
110-
raise WebDriverException(
111-
f"'{os.path.basename(self.path)}' executable may have wrong permissions. {self.start_error_message}"
112-
)
113-
else:
114-
raise
115-
except Exception as e:
116-
raise WebDriverException(
117-
f"The executable {os.path.basename(self.path)} needs to be available in the path. {self.start_error_message}\n{str(e)}"
118-
)
91+
self._start_process(self.path)
92+
except WebDriverException as err:
93+
if "executable needs to be in PATH" in err.msg:
94+
logger.warning("driver not found in PATH, trying Selenium Manager")
95+
browser = self.__class__.__module__.split(".")[-2]
96+
try:
97+
path = SeleniumManager.driver_location(browser)
98+
except WebDriverException as new_err:
99+
logger.warning("Unable to obtain driver using Selenium Manager: " + new_err.msg)
100+
raise err
101+
102+
self._start_process(path)
103+
119104
count = 0
120105
while True:
121106
self.assert_process_still_running()
@@ -183,11 +168,48 @@ def _terminate_process(self) -> None:
183168
# Todo: only SIGKILL if necessary; the process may be cleanly exited by now.
184169
self.process.kill()
185170
except OSError:
186-
log.error("Error terminating service process.", exc_info=True)
171+
logger.error("Error terminating service process.", exc_info=True)
187172

188173
def __del__(self) -> None:
189174
# `subprocess.Popen` doesn't send signal on `__del__`;
190175
# so we attempt to close the launched process when `__del__`
191176
# is triggered.
192177
with contextlib.suppress(Exception):
193178
self.stop()
179+
180+
def _start_process(self, path: str) -> None:
181+
"""
182+
Creates a subprocess by executing the command provided.
183+
184+
:param cmd: full command to execute
185+
"""
186+
cmd = [path]
187+
cmd.extend(self.command_line_args())
188+
try:
189+
self.process = subprocess.Popen(
190+
cmd,
191+
env=self.env,
192+
close_fds=system() != "Windows",
193+
stdout=self.log_file,
194+
stderr=self.log_file,
195+
stdin=PIPE,
196+
creationflags=self.creation_flags,
197+
)
198+
logger.debug(f"Started executable: `{self.path}` in a child process with pid: {self.process.pid}")
199+
except TypeError:
200+
raise
201+
except OSError as err:
202+
if err.errno == errno.ENOENT:
203+
raise WebDriverException(
204+
f"'{os.path.basename(self.path)}' executable needs to be in PATH. {self.start_error_message}"
205+
)
206+
elif err.errno == errno.EACCES:
207+
raise WebDriverException(
208+
f"'{os.path.basename(self.path)}' executable may have wrong permissions. {self.start_error_message}"
209+
)
210+
else:
211+
raise
212+
except Exception as e:
213+
raise WebDriverException(
214+
f"The executable {os.path.basename(self.path)} needs to be available in the path. {self.start_error_message}\n{str(e)}"
215+
)

0 commit comments

Comments
 (0)