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