Skip to content

Commit 6b4281b

Browse files
committed
[py]: More internal refactoring of the base Service class
1 parent ba04acd commit 6b4281b

File tree

1 file changed

+44
-31
lines changed

1 file changed

+44
-31
lines changed

py/selenium/webdriver/common/service.py

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,29 @@
1616
# under the License.
1717
import contextlib
1818
import errno
19+
import logging
1920
import os
2021
import subprocess
2122
import typing
23+
from abc import ABC
24+
from abc import abstractmethod
2225
from platform import system
2326
from subprocess import DEVNULL
2427
from subprocess import PIPE
2528
from time import sleep
29+
from urllib import request
30+
from urllib.error import URLError
2631

2732
from selenium.common.exceptions import WebDriverException
2833
from selenium.webdriver.common import utils
2934

35+
log = logging.getLogger(__name__)
36+
37+
3038
_HAS_NATIVE_DEVNULL = True
3139

3240

33-
class Service:
41+
class Service(ABC):
3442
def __init__(
3543
self,
3644
executable: str,
@@ -46,19 +54,20 @@ def __init__(
4654
# Default value for every python subprocess: subprocess.Popen(..., creationflags=0)
4755
self.creation_flags = 0
4856
self.env = env or os.environ
49-
self.process = None
57+
self.process: typing.Optional[subprocess.Popen] = None
5058

5159
@property
52-
def service_url(self):
60+
def service_url(self) -> str:
5361
"""
5462
Gets the url of the Service
5563
"""
5664
return f"http://{utils.join_host_port('localhost', self.port)}"
5765

58-
def command_line_args(self):
66+
@abstractmethod
67+
def command_line_args(self) -> typing.List[str]:
5968
raise NotImplementedError("This method needs to be implemented in a sub class")
6069

61-
def start(self):
70+
def start(self) -> None:
6271
"""
6372
Starts the Service.
6473
@@ -109,26 +118,30 @@ def start(self):
109118
count += 1
110119
sleep(0.5)
111120
if count == 60:
112-
raise WebDriverException("Can not connect to the Service %s" % self.path)
121+
raise WebDriverException(f"Can not connect to the Service {self.path}")
113122

114-
def assert_process_still_running(self):
123+
def assert_process_still_running(self) -> None:
124+
"""Check if the underlying process is still running."""
115125
return_code = self.process.poll()
116126
if return_code:
117127
raise WebDriverException(f"Service {self.path} unexpectedly exited. Status code was: {return_code}")
118128

119-
def is_connectable(self):
129+
def is_connectable(self) -> bool:
130+
"""Establishes a socket connection to determine if the service running on
131+
the port is accessible."""
120132
return utils.is_connectable(self.port)
121133

122-
def send_remote_shutdown_command(self):
123-
from urllib import request
124-
from urllib.error import URLError
125-
134+
def send_remote_shutdown_command(self) -> None:
135+
"""
136+
Dispatch an HTTP request to the shutdown endpoint for the service in an
137+
attempt to stop it.
138+
"""
126139
try:
127140
request.urlopen(f"{self.service_url}/shutdown")
128141
except URLError:
129142
return
130143

131-
for x in range(30):
144+
for _ in range(30):
132145
if not self.is_connectable():
133146
break
134147
sleep(1)
@@ -142,27 +155,27 @@ def stop(self) -> None:
142155
# Todo: Be explicit in what we are catching here.
143156
self.log_file.close()
144157

145-
if not self.process:
146-
return
147-
148-
try:
149-
self.send_remote_shutdown_command()
150-
except TypeError:
151-
pass
158+
if self.process is not None:
159+
with contextlib.suppress(TypeError):
160+
self.send_remote_shutdown_command()
161+
self._terminate_process()
152162

163+
def _terminate_process(self) -> None:
164+
"""Terminate the child process. On POSIX this attempts a graceful
165+
SIGTERM followed by a SIGKILL, on a Windows OS kill is an alias to
166+
terminate. Terminating does not raise itself if something has gone
167+
wrong but (currently) silently ignores errors here."""
153168
try:
154-
if self.process:
155-
for stream in [self.process.stdin, self.process.stdout, self.process.stderr]:
156-
try:
157-
stream.close()
158-
except AttributeError:
159-
pass
160-
self.process.terminate()
161-
self.process.wait()
162-
self.process.kill()
163-
self.process = None
169+
stdin, stdout, stderr = self.process.stdin, self.process.stdout, self.process.stdout
170+
for stream in stdin, stdout, stderr:
171+
with contextlib.suppress(AttributeError):
172+
stream.close()
173+
self.process.terminate()
174+
self.process.wait(60)
175+
# Todo: only SIGKILL if necessary; the process may be cleanly exited by now.
176+
self.process.kill()
164177
except OSError:
165-
pass
178+
log.error("Error terminating service process.", exc_info=True)
166179

167180
def __del__(self) -> None:
168181
# `subprocess.Popen` doesn't send signal on `__del__`;

0 commit comments

Comments
 (0)