Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/py/selenium/webdriver/remote/server.py
3998 views
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
18
import collections
19
import os
20
import re
21
import shutil
22
import socket
23
import subprocess
24
import time
25
import urllib
26
27
from selenium.webdriver.common.selenium_manager import SeleniumManager
28
29
30
class Server:
31
"""Manage a Selenium Grid (Remote) Server in standalone mode.
32
33
This class contains functionality for downloading the server and starting/stopping it.
34
35
For more information on Selenium Grid, see:
36
- https://www.selenium.dev/documentation/grid/getting_started/
37
38
Args:
39
host: Hostname or IP address to bind to (determined automatically if not specified).
40
port: Port to listen on (4444 if not specified).
41
path: Path/filename of existing server .jar file (Selenium Manager is used if not specified).
42
version: Version of server to download (latest version if not specified).
43
log_level: Logging level to control logging output ("INFO" if not specified).
44
Available levels: "SEVERE", "WARNING", "INFO", "CONFIG", "FINE", "FINER", "FINEST".
45
env: Mapping that defines the environment variables for the server process.
46
java_path: Path to the java executable to run the server.
47
"""
48
49
def __init__(
50
self,
51
host=None,
52
port=4444,
53
path=None,
54
version=None,
55
log_level="INFO",
56
env=None,
57
java_path=None,
58
startup_timeout=10,
59
):
60
if path and version:
61
raise TypeError("Not allowed to specify a version when using an existing server path")
62
63
self.host = host
64
self.port = port
65
self.path = path
66
self.version = version
67
self.log_level = log_level
68
self.env = env
69
self.java_path = java_path
70
self.startup_timeout = startup_timeout
71
self.process = None
72
73
@property
74
def startup_timeout(self):
75
return self._startup_timeout
76
77
@startup_timeout.setter
78
def startup_timeout(self, timeout):
79
self._startup_timeout = int(timeout)
80
81
@property
82
def status_url(self):
83
host = self.host if self.host is not None else "localhost"
84
return f"http://{host}:{self.port}/status"
85
86
@property
87
def path(self):
88
return self._path
89
90
@path.setter
91
def path(self, path):
92
if path and not os.path.exists(path):
93
raise OSError(f"Can't find server .jar located at {path}")
94
self._path = path
95
96
@property
97
def port(self):
98
return self._port
99
100
@port.setter
101
def port(self, port):
102
try:
103
port = int(port)
104
except ValueError:
105
raise TypeError(f"{__class__.__name__}.__init__() got an invalid port: '{port}'")
106
if not (0 <= port <= 65535):
107
raise ValueError("port must be 0-65535")
108
self._port = port
109
110
@property
111
def version(self):
112
return self._version
113
114
@version.setter
115
def version(self, version):
116
if version:
117
if not re.match(r"^\d+\.\d+\.\d+$", str(version)):
118
raise TypeError(f"{__class__.__name__}.__init__() got an invalid version: '{version}'")
119
self._version = version
120
121
@property
122
def log_level(self):
123
return self._log_level
124
125
@log_level.setter
126
def log_level(self, log_level):
127
levels = ("SEVERE", "WARNING", "INFO", "CONFIG", "FINE", "FINER", "FINEST")
128
if log_level not in levels:
129
raise TypeError(f"log_level must be one of: {', '.join(levels)}")
130
self._log_level = log_level
131
132
@property
133
def env(self):
134
return self._env
135
136
@env.setter
137
def env(self, env):
138
if env is not None and not isinstance(env, collections.abc.Mapping):
139
raise TypeError("env must be a mapping of environment variables")
140
self._env = env
141
142
@property
143
def java_path(self):
144
return self._java_path
145
146
@java_path.setter
147
def java_path(self, java_path):
148
if java_path and not os.path.exists(java_path):
149
raise OSError(f"Can't find java executable located at {java_path}")
150
self._java_path = java_path
151
152
def _wait_for_server(self, timeout=10):
153
start = time.time()
154
while time.time() - start < timeout:
155
try:
156
urllib.request.urlopen(self.status_url)
157
return True
158
except urllib.error.URLError:
159
time.sleep(0.2)
160
return False
161
162
def download_if_needed(self, version=None):
163
"""Download the server if it doesn't already exist.
164
165
Latest version is downloaded unless specified.
166
"""
167
args = ["--grid"]
168
if version is not None:
169
args.append(version)
170
return SeleniumManager().binary_paths(args)["driver_path"]
171
172
def start(self):
173
"""Start the server.
174
175
Selenium Manager will detect the server location and download it if necessary,
176
unless an existing server path was specified.
177
"""
178
path = self.download_if_needed(self.version) if self.path is None else self.path
179
180
java_path = self.java_path or shutil.which("java")
181
if java_path is None:
182
raise OSError("Can't find java on system PATH. JRE is required to run the Selenium server")
183
184
command = [
185
java_path,
186
"-jar",
187
path,
188
"standalone",
189
"--port",
190
str(self.port),
191
"--log-level",
192
self.log_level,
193
"--selenium-manager",
194
"true",
195
"--enable-managed-downloads",
196
"true",
197
]
198
if self.host is not None:
199
command.extend(["--host", self.host])
200
201
host = self.host if self.host is not None else "localhost"
202
203
try:
204
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
205
sock.connect((host, self.port))
206
raise ConnectionError(f"Selenium server is already running, or something else is using port {self.port}")
207
except ConnectionRefusedError:
208
print("Starting Selenium server...")
209
self.process = subprocess.Popen(command, env=self.env)
210
print(f"Selenium server running as process: {self.process.pid}")
211
if not self._wait_for_server(timeout=self.startup_timeout):
212
raise TimeoutError(f"Timed out waiting for Selenium server at {self.status_url}")
213
print("Selenium server is ready")
214
return self.process
215
216
def stop(self):
217
"""Stop the server."""
218
if self.process is None:
219
raise RuntimeError("Selenium server isn't running")
220
else:
221
if self.process.poll() is None:
222
self.process.terminate()
223
self.process.wait()
224
self.process = None
225
print("Selenium server has been terminated")
226
227