Skip to content

Commit c2f4983

Browse files
committed
Prevent: reuse of closed sys.stdout in service log output
1 parent 1cad0ca commit c2f4983

File tree

2 files changed

+18
-50
lines changed

2 files changed

+18
-50
lines changed

py/selenium/webdriver/common/service.py

Lines changed: 9 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,7 @@
2424
from platform import system
2525
from subprocess import PIPE
2626
from time import sleep
27-
from typing import IO
28-
from typing import Any
29-
from typing import List
30-
from typing import Mapping
31-
from typing import Optional
32-
from typing import Union
33-
from typing import cast
27+
from typing import IO, Any, List, Mapping, Optional, Union, cast
3428
from urllib import request
3529
from urllib.error import URLError
3630

@@ -42,17 +36,6 @@
4236

4337

4438
class Service(ABC):
45-
"""The abstract base class for all service objects. Services typically
46-
launch a child program in a new process as an interim process to
47-
communicate with a browser.
48-
49-
:param executable: install path of the executable.
50-
:param port: Port for the service to run on, defaults to 0 where the operating system will decide.
51-
:param log_output: (Optional) int representation of STDOUT/DEVNULL, any IO instance or String path to file.
52-
:param env: (Optional) Mapping of environment variables for the new process, defaults to `os.environ`.
53-
:param driver_path_env_key: (Optional) Environment variable to use to get the path to the driver executable.
54-
"""
55-
5639
def __init__(
5740
self,
5841
executable_path: Optional[str] = None,
@@ -72,7 +55,6 @@ def __init__(
7255
self.log_output = log_output
7356

7457
self.port = port or utils.free_port()
75-
# Default value for every python subprocess: subprocess.Popen(..., creationflags=0)
7658
self.popen_kw = kwargs.pop("popen_kw", {})
7759
self.creation_flags = self.popen_kw.pop("creation_flags", 0)
7860
self.env = env or os.environ
@@ -81,12 +63,10 @@ def __init__(
8163

8264
@property
8365
def service_url(self) -> str:
84-
"""Gets the url of the Service."""
8566
return f"http://{utils.join_host_port('localhost', self.port)}"
8667

8768
@abstractmethod
8869
def command_line_args(self) -> List[str]:
89-
"""A List of program arguments (excluding the executable)."""
9070
raise NotImplementedError("This method needs to be implemented in a sub class")
9171

9272
@property
@@ -98,12 +78,6 @@ def path(self, value: str) -> None:
9878
self._path = str(value)
9979

10080
def start(self) -> None:
101-
"""Starts the Service.
102-
103-
:Exceptions:
104-
- WebDriverException : Raised either when it can't start the service
105-
or when it can't connect to the service
106-
"""
10781
if self._path is None:
10882
raise WebDriverException("Service path cannot be None.")
10983
self._start_process(self._path)
@@ -113,26 +87,20 @@ def start(self) -> None:
11387
self.assert_process_still_running()
11488
if self.is_connectable():
11589
break
116-
# sleep increasing: 0.01, 0.06, 0.11, 0.16, 0.21, 0.26, 0.31, 0.36, 0.41, 0.46, 0.5
11790
sleep(min(0.01 + 0.05 * count, 0.5))
11891
count += 1
11992
if count == 70:
12093
raise WebDriverException(f"Can not connect to the Service {self._path}")
12194

12295
def assert_process_still_running(self) -> None:
123-
"""Check if the underlying process is still running."""
12496
return_code = self.process.poll()
12597
if return_code:
12698
raise WebDriverException(f"Service {self._path} unexpectedly exited. Status code was: {return_code}")
12799

128100
def is_connectable(self) -> bool:
129-
"""Establishes a socket connection to determine if the service running
130-
on the port is accessible."""
131101
return utils.is_connectable(self.port)
132102

133103
def send_remote_shutdown_command(self) -> None:
134-
"""Dispatch an HTTP request to the shutdown endpoint for the service in
135-
an attempt to stop it."""
136104
try:
137105
request.urlopen(f"{self.service_url}/shutdown")
138106
except URLError:
@@ -144,8 +112,6 @@ def send_remote_shutdown_command(self) -> None:
144112
sleep(1)
145113

146114
def stop(self) -> None:
147-
"""Stops the service."""
148-
149115
if self.log_output not in {PIPE, subprocess.DEVNULL}:
150116
if isinstance(self.log_output, IOBase):
151117
self.log_output.close()
@@ -161,13 +127,6 @@ def stop(self) -> None:
161127
self._terminate_process()
162128

163129
def _terminate_process(self) -> None:
164-
"""Terminate the child process.
165-
166-
On POSIX this attempts a graceful SIGTERM followed by a SIGKILL,
167-
on a Windows OS kill is an alias to terminate. Terminating does
168-
not raise itself if something has gone wrong but (currently)
169-
silently ignores errors here.
170-
"""
171130
try:
172131
stdin, stdout, stderr = (
173132
self.process.stdin,
@@ -192,11 +151,6 @@ def _terminate_process(self) -> None:
192151
logger.error("Error terminating service process.", exc_info=True)
193152

194153
def __del__(self) -> None:
195-
# `subprocess.Popen` doesn't send signal on `__del__`;
196-
# so we attempt to close the launched process when `__del__`
197-
# is triggered.
198-
# do not use globals here; interpreter shutdown may have already cleaned them up
199-
# and they would be `None`. This goes for anything this method is referencing internally.
200154
try:
201155
self.stop()
202156
except Exception:
@@ -205,11 +159,18 @@ def __del__(self) -> None:
205159
def _start_process(self, path: str) -> None:
206160
"""Creates a subprocess by executing the command provided.
207161
208-
:param cmd: full command to execute
162+
:param path: full command to execute
209163
"""
164+
# Pre-check: validate path exists and is executable
165+
if not os.path.isfile(path):
166+
raise WebDriverException(f"The executable at path '{path}' does not exist.")
167+
if not os.access(path, os.X_OK):
168+
raise WebDriverException(f"The executable at path '{path}' is not executable.")
169+
210170
cmd = [path]
211171
cmd.extend(self.command_line_args())
212172
close_file_descriptors = self.popen_kw.pop("close_fds", system() != "Windows")
173+
213174
try:
214175
start_info = None
215176
if system() == "Windows":
@@ -239,8 +200,6 @@ def _start_process(self, path: str) -> None:
239200
raise
240201
except OSError as err:
241202
if err.errno == errno.EACCES:
242-
if self._path is None:
243-
raise WebDriverException("Service path cannot be None.")
244203
raise WebDriverException(
245204
f"'{os.path.basename(self._path)}' executable may have wrong permissions."
246205
) from err
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
def test_reusing_closed_stdout_fails():
2+
import sys
3+
from selenium.webdriver.chrome.service import Service
4+
from selenium.common.exceptions import WebDriverException
5+
6+
service = Service(log_output=sys.stdout)
7+
service._log_output.close()
8+
with pytest.raises(ValueError):
9+
Service(log_output=sys.stdout)

0 commit comments

Comments
 (0)