Skip to content

Commit abc5320

Browse files
committed
Handle session events for standalone
1 parent d50d853 commit abc5320

File tree

8 files changed

+155
-36
lines changed

8 files changed

+155
-36
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1115,7 +1115,7 @@ test_node_docker: hub standalone_docker standalone_chrome standalone_firefox sta
11151115
echo VIDEO_TAG=$(FFMPEG_TAG_VERSION)-$(BUILD_DATE) >> .env ; \
11161116
echo TEST_DRAIN_AFTER_SESSION_COUNT=$(or $(TEST_DRAIN_AFTER_SESSION_COUNT), 0) >> .env ; \
11171117
echo TEST_PARALLEL_HARDENING=$(or $(TEST_PARALLEL_HARDENING), "false") >> .env ; \
1118-
echo LOG_LEVEL=$(or $(LOG_LEVEL), "INFO") >> .env ; \
1118+
echo LOG_LEVEL=$(or $(LOG_LEVEL), "FINE") >> .env ; \
11191119
echo REQUEST_TIMEOUT=$(or $(REQUEST_TIMEOUT), 300) >> .env ; \
11201120
echo SELENIUM_ENABLE_MANAGED_DOWNLOADS=$(or $(SELENIUM_ENABLE_MANAGED_DOWNLOADS), "false") >> .env ; \
11211121
echo TEST_DELAY_AFTER_TEST=$(or $(TEST_DELAY_AFTER_TEST), 2) >> .env ; \

Standalone/Dockerfile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ ENV SE_SESSION_REQUEST_TIMEOUT="300" \
2727
SE_RELAX_CHECKS="true" \
2828
SE_REJECT_UNSUPPORTED_CAPS="true" \
2929
SE_OTEL_SERVICE_NAME="selenium-standalone" \
30-
SE_NODE_ENABLE_MANAGED_DOWNLOADS="true"
30+
SE_NODE_ENABLE_MANAGED_DOWNLOADS="true" \
31+
SE_EVENT_BUS_HOST="0.0.0.0" \
32+
SE_BIND_BUS="false" \
33+
SE_EVENT_BUS_IMPLEMENTATION=""
3134

3235
EXPOSE 4444
36+
EXPOSE 4443
37+
EXPOSE 4442

Standalone/start-selenium-standalone.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,17 @@ if [ ! -z "${SE_EVENT_BUS_HEARTBEAT_PERIOD}" ]; then
103103
append_se_opts "--eventbus-heartbeat-period" "${SE_EVENT_BUS_HEARTBEAT_PERIOD}"
104104
fi
105105

106+
if [ ! -z "${SE_EVENT_BUS_IMPLEMENTATION}" ]; then
107+
append_se_opts "--events-implementation" "${SE_EVENT_BUS_IMPLEMENTATION}"
108+
fi
109+
110+
if [ "${SE_BIND_BUS}" = "true" ]; then
111+
append_se_opts "--bind-bus" "${SE_BIND_BUS}"
112+
if [ -z "${SE_EVENT_BUS_IMPLEMENTATION}" ]; then
113+
append_se_opts "--events-implementation" "org.openqa.selenium.events.zeromq.ZeroMqEventBus"
114+
fi
115+
fi
116+
106117
if [ "${SE_ENABLE_TLS}" = "true" ]; then
107118
# Configure truststore for the server
108119
if [ ! -z "$SE_JAVA_SSL_TRUST_STORE" ]; then

Video/video_service.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,16 +160,21 @@ def __init__(self):
160160
self.file_name_trim_regex = os.environ.get("SE_VIDEO_FILE_NAME_TRIM_REGEX", "[^a-zA-Z0-9-_]")
161161
self.file_name_suffix = os.environ.get("SE_VIDEO_FILE_NAME_SUFFIX", "true").lower() == "true"
162162

163+
# Standalone mode: single node, no need to filter events by NodeId
164+
self.record_standalone = os.environ.get("SE_VIDEO_RECORD_STANDALONE", "false").lower() == "true"
165+
163166
# Node identity for filtering events in distributed (Hub-Nodes) setup.
164167
# In distributed mode, ZeroMQ broadcasts ALL session events to ALL subscribers.
165168
# Each Node's recorder must filter to only process events for its own Node.
166169
# Node ID is resolved from the Node /status endpoint on startup.
170+
# In standalone mode, NodeId filtering is skipped since there is only one node.
167171
self.node_id: Optional[str] = None
168172
self.node_external_uri: Optional[str] = None
169173

170174
# Node /status endpoint configuration
171175
self.se_server_protocol = os.environ.get("SE_SERVER_PROTOCOL", "http")
172-
self.se_node_port = os.environ.get("SE_NODE_PORT", "5555")
176+
default_node_port = "4444" if self.record_standalone else "5555"
177+
self.se_node_port = os.environ.get("SE_NODE_PORT", default_node_port)
173178
self.node_poll_interval = int(os.environ.get("SE_VIDEO_POLL_INTERVAL", "2"))
174179

175180
# Drain configuration
@@ -268,7 +273,12 @@ def is_own_node_event(self, data: dict) -> bool:
268273
269274
Matching is done by comparing the event's nodeId against the Node ID
270275
obtained from the Node /status endpoint on startup.
276+
277+
In standalone mode, all events belong to this Node, so filtering is skipped.
271278
"""
279+
if self.record_standalone:
280+
return True
281+
272282
if self.node_id is None:
273283
# Node ID not yet resolved, cannot filter
274284
logger.warning("Node ID not resolved yet, skipping event")
@@ -282,6 +292,10 @@ async def wait_for_node_ready(self) -> None:
282292
283293
Polls the Node /status endpoint until it returns HTTP 200,
284294
then extracts nodeId and externalUri from the response.
295+
296+
Response structure differs by mode:
297+
- Standalone: $.value.nodes[0].id, $.value.nodes[0].externalUri
298+
- Distributed: $.value.node.nodeId, $.value.node.externalUri
285299
"""
286300
node_status_url = f"{self.se_server_protocol}://{self.display_container}:{self.se_node_port}/status"
287301
headers = {}
@@ -296,9 +310,17 @@ async def wait_for_node_ready(self) -> None:
296310
with urlopen(req, timeout=5) as resp:
297311
if resp.status == 200:
298312
body = json.loads(resp.read().decode("utf-8"))
299-
node_info = body.get("value", {}).get("node", {})
300-
self.node_id = node_info.get("nodeId")
301-
self.node_external_uri = node_info.get("externalUri")
313+
314+
if self.record_standalone:
315+
nodes = body.get("value", {}).get("nodes", [])
316+
if nodes:
317+
node_info = nodes[0]
318+
self.node_id = node_info.get("id")
319+
self.node_external_uri = node_info.get("externalUri")
320+
else:
321+
node_info = body.get("value", {}).get("node", {})
322+
self.node_id = node_info.get("nodeId")
323+
self.node_external_uri = node_info.get("externalUri")
302324

303325
if self.node_id:
304326
logger.info(f"Node is ready. ID: {self.node_id}, URI: {self.node_external_uri}")
@@ -785,6 +807,7 @@ async def run(self) -> None:
785807
logger.info("Starting unified video recording and upload service")
786808
logger.info("=" * 60)
787809
logger.info(f"Configuration:")
810+
logger.info(f" Standalone mode: {self.record_standalone}")
788811
logger.info(f" Event bus: {self.event_bus_host}:{self.event_bus_port}")
789812
logger.info(f" Video folder: {self.video_folder}")
790813
logger.info(f" Video size: {self.video_size}")

tests/docker-compose-v3-event-driven-arm64.yml

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,29 @@
1-
# To execute this docker compose yml file use `docker compose -f docker-compose-v3-event-driven-arm64.yml up`
2-
# Add the `-d` flag at the end for detached execution
3-
# To stop the execution, hit Ctrl+C, and then `docker compose -f docker-compose-v3-event-driven-arm64.yml down`
4-
#
5-
# This compose file uses the event-driven video recording backend.
6-
# The recorder subscribes to Grid's ZeroMQ event bus for session lifecycle events
7-
# instead of polling the Node /status endpoint.
81
services:
2+
# Start a local FTP server to demonstrate video upload with RCLONE (https://github.com/delfer/docker-alpine-ftp-server)
3+
ftp_server:
4+
image: delfer/alpine-ftp-server:latest
5+
container_name: ftp_server
6+
environment:
7+
- USERS=seluser|selenium.dev
8+
volumes:
9+
# Mount the local directory `/home/${USER}/Videos/upload` to the FTP server's `/ftp/seluser` directory to check out the uploaded videos
10+
- /tmp/upload:/ftp/seluser
11+
command: ["/bin/sh", "-c", "/sbin/tini -- /bin/start_vsftpd.sh && tail -f /dev/null"]
12+
stop_grace_period: 30s
13+
14+
# File browser to manage the uploaded videos from the FTP server
15+
file_browser:
16+
image: filebrowser/filebrowser:latest
17+
container_name: file_browser
18+
restart: always
19+
ports:
20+
- "8081:80"
21+
volumes:
22+
# Mount the local directory `/tmp/upload` to file browser's `/srv` directory to check out the uploaded videos
23+
- /tmp/upload:/srv
24+
environment:
25+
- FB_NOAUTH=true
26+
927
chrome:
1028
deploy:
1129
mode: replicated
@@ -17,16 +35,24 @@ services:
1735
volumes:
1836
- /tmp/videos:/videos
1937
- ./../selenium_server_deploy.jar:/opt/selenium/selenium-server.jar
38+
- ./../Video/video_service.py:/opt/bin/video_service.py
2039
environment:
2140
- SE_EVENT_BUS_HOST=selenium-hub
2241
- SE_RECORD_VIDEO=true
2342
- SE_VIDEO_FILE_NAME=auto
2443
- SE_EVENT_DRIVEN_SERVICES=true
25-
# Only upload videos for failed sessions (optional, default: false)
26-
# - SE_UPLOAD_FAILURE_SESSION_ONLY=true
27-
# Upload configuration (optional)
28-
# - SE_VIDEO_UPLOAD_ENABLED=true
29-
# - SE_UPLOAD_DESTINATION_PREFIX=s3://bucket/videos
44+
- SE_VIDEO_UPLOAD_ENABLED=true
45+
# Remote name and destination path to upload
46+
- SE_UPLOAD_DESTINATION_PREFIX=myftp://ftp/seluser
47+
# All configs required for RCLONE to upload to remote name myftp
48+
- RCLONE_CONFIG_MYFTP_TYPE=ftp
49+
- RCLONE_CONFIG_MYFTP_HOST=ftp_server
50+
- RCLONE_CONFIG_MYFTP_PORT=21
51+
- RCLONE_CONFIG_MYFTP_USER=seluser
52+
# Password encrypted using command: rclone obscure <your_password>
53+
- RCLONE_CONFIG_MYFTP_PASS=KkK8RsUIba-MMTBUSnuYIdAKvcnFyLl2pdhQig
54+
- RCLONE_CONFIG_MYFTP_FTP_CONCURRENCY=10
55+
stop_grace_period: 30s
3056

3157
firefox:
3258
deploy:
@@ -39,16 +65,24 @@ services:
3965
volumes:
4066
- /tmp/videos:/videos
4167
- ./../selenium_server_deploy.jar:/opt/selenium/selenium-server.jar
68+
- ./../Video/video_service.py:/opt/bin/video_service.py
4269
environment:
4370
- SE_EVENT_BUS_HOST=selenium-hub
4471
- SE_RECORD_VIDEO=true
4572
- SE_VIDEO_FILE_NAME=auto
4673
- SE_EVENT_DRIVEN_SERVICES=true
47-
# Only upload videos for failed sessions (optional, default: false)
48-
# - SE_UPLOAD_FAILURE_SESSION_ONLY=true
49-
# Upload configuration (optional)
50-
# - SE_VIDEO_UPLOAD_ENABLED=true
51-
# - SE_UPLOAD_DESTINATION_PREFIX=s3://bucket/videos
74+
- SE_VIDEO_UPLOAD_ENABLED=true
75+
# Remote name and destination path to upload
76+
- SE_UPLOAD_DESTINATION_PREFIX=myftp://ftp/seluser
77+
# All configs required for RCLONE to upload to remote name myftp
78+
- RCLONE_CONFIG_MYFTP_TYPE=ftp
79+
- RCLONE_CONFIG_MYFTP_HOST=ftp_server
80+
- RCLONE_CONFIG_MYFTP_PORT=21
81+
- RCLONE_CONFIG_MYFTP_USER=seluser
82+
# Password encrypted using command: rclone obscure <your_password>
83+
- RCLONE_CONFIG_MYFTP_PASS=KkK8RsUIba-MMTBUSnuYIdAKvcnFyLl2pdhQig
84+
- RCLONE_CONFIG_MYFTP_FTP_CONCURRENCY=10
85+
stop_grace_period: 30s
5286

5387
selenium-hub:
5488
image: selenium/hub:4.40.0-20260204
@@ -59,15 +93,3 @@ services:
5993
- "4442:4442"
6094
- "4443:4443"
6195
- "4444:4444"
62-
63-
# File browser to manage the videos from local volume
64-
file_browser:
65-
image: filebrowser/filebrowser:latest
66-
container_name: file_browser
67-
restart: always
68-
ports:
69-
- "8081:80"
70-
volumes:
71-
- /tmp/videos:/srv
72-
environment:
73-
- FB_NOAUTH=true
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
services:
2+
# Start a local FTP server to demonstrate video upload with RCLONE (https://github.com/delfer/docker-alpine-ftp-server)
3+
ftp_server:
4+
image: delfer/alpine-ftp-server:latest
5+
container_name: ftp_server
6+
environment:
7+
- USERS=seluser|selenium.dev
8+
volumes:
9+
# Mount the local directory `/home/${USER}/Videos/upload` to the FTP server's `/ftp/seluser` directory to check out the uploaded videos
10+
- /tmp/upload:/ftp/seluser
11+
command: ["/bin/sh", "-c", "/sbin/tini -- /bin/start_vsftpd.sh && tail -f /dev/null"]
12+
stop_grace_period: 30s
13+
14+
# File browser to manage the uploaded videos from the FTP server
15+
file_browser:
16+
image: filebrowser/filebrowser:latest
17+
container_name: file_browser
18+
restart: always
19+
ports:
20+
- "8081:80"
21+
volumes:
22+
# Mount the local directory `/tmp/upload` to file browser's `/srv` directory to check out the uploaded videos
23+
- /tmp/upload:/srv
24+
environment:
25+
- FB_NOAUTH=true
26+
27+
chrome:
28+
deploy:
29+
mode: replicated
30+
replicas: 1
31+
image: selenium/standalone-chromium:4.40.0-20260209
32+
shm_size: 2gb
33+
ports:
34+
- "4444:4444"
35+
volumes:
36+
- /tmp/videos:/videos
37+
- ./../selenium_server_deploy.jar:/opt/selenium/selenium-server.jar
38+
- ./../Video/video_service.py:/opt/bin/video_service.py
39+
environment:
40+
- SE_VIDEO_RECORD_STANDALONE=true
41+
- SE_RECORD_VIDEO=true
42+
- SE_VIDEO_FILE_NAME=auto
43+
- SE_BIND_BUS=true
44+
- SE_EVENT_DRIVEN_SERVICES=true
45+
- SE_VIDEO_UPLOAD_ENABLED=true
46+
# Remote name and destination path to upload
47+
- SE_UPLOAD_DESTINATION_PREFIX=myftp://ftp/seluser
48+
# All configs required for RCLONE to upload to remote name myftp
49+
- RCLONE_CONFIG_MYFTP_TYPE=ftp
50+
- RCLONE_CONFIG_MYFTP_HOST=ftp_server
51+
- RCLONE_CONFIG_MYFTP_PORT=21
52+
- RCLONE_CONFIG_MYFTP_USER=seluser
53+
# Password encrypted using command: rclone obscure <your_password>
54+
- RCLONE_CONFIG_MYFTP_PASS=KkK8RsUIba-MMTBUSnuYIdAKvcnFyLl2pdhQig
55+
- RCLONE_CONFIG_MYFTP_FTP_CONCURRENCY=10
56+
stop_grace_period: 30s

tests/get_started.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def run_browser_instance(browser, grid_url):
4141
print(f"Session created: {driver.session_id} ({browser})")
4242
driver.get('https://www.google.com/')
4343
print(driver.title)
44-
time.sleep(100)
44+
time.sleep(15)
4545
driver.quit()
4646

4747

tests/test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ def launch_container(container, **kwargs):
259259
'SE_EVENT_BUS_PUBLISH_PORT': 4442,
260260
'SE_EVENT_BUS_SUBSCRIBE_PORT': 4443,
261261
}
262+
if "standalone" in container.lower():
263+
environment['SE_EVENT_BUS_HOST'] = '0.0.0.0'
262264
if container != 'Hub':
263265
environment['SE_NODE_ENABLE_MANAGED_DOWNLOADS'] = "true"
264266
container_id = client.containers.run(

0 commit comments

Comments
 (0)