diff --git a/java/src/org/openqa/selenium/grid/data/SessionHistoryEntry.java b/java/src/org/openqa/selenium/grid/data/SessionHistoryEntry.java new file mode 100644 index 0000000000000..bc902bdf545be --- /dev/null +++ b/java/src/org/openqa/selenium/grid/data/SessionHistoryEntry.java @@ -0,0 +1,66 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.openqa.selenium.grid.data; + +import java.time.Instant; +import java.util.Objects; +import org.openqa.selenium.internal.Require; +import org.openqa.selenium.remote.SessionId; + +public class SessionHistoryEntry { + private final SessionId sessionId; + private final Instant startTime; + private Instant stopTime; + + public SessionHistoryEntry(SessionId sessionId, Instant startTime, Instant stopTime) { + this.sessionId = Require.nonNull("Session ID", sessionId); + this.startTime = Require.nonNull("Start time", startTime); + this.stopTime = stopTime; // Can be null for ongoing sessions + } + + public SessionId getSessionId() { + return sessionId; + } + + public Instant getStartTime() { + return startTime; + } + + public Instant getStopTime() { + return stopTime; + } + + public void setStopTime(Instant stopTime) { + this.stopTime = stopTime; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SessionHistoryEntry)) return false; + SessionHistoryEntry that = (SessionHistoryEntry) o; + return Objects.equals(sessionId, that.sessionId) + && Objects.equals(startTime, that.startTime) + && Objects.equals(stopTime, that.stopTime); + } + + @Override + public int hashCode() { + return Objects.hash(sessionId, startTime, stopTime); + } +} diff --git a/java/src/org/openqa/selenium/grid/data/SessionStartedEvent.java b/java/src/org/openqa/selenium/grid/data/SessionStartedEvent.java new file mode 100644 index 0000000000000..c0f3e5c5060f9 --- /dev/null +++ b/java/src/org/openqa/selenium/grid/data/SessionStartedEvent.java @@ -0,0 +1,40 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.openqa.selenium.grid.data; + +import java.util.function.Consumer; +import org.openqa.selenium.events.Event; +import org.openqa.selenium.events.EventListener; +import org.openqa.selenium.events.EventName; +import org.openqa.selenium.internal.Require; +import org.openqa.selenium.remote.SessionId; + +public class SessionStartedEvent extends Event { + + private static final EventName SESSION_STARTED = new EventName("session-started"); + + public SessionStartedEvent(SessionId id) { + super(SESSION_STARTED, id); + } + + public static EventListener listener(Consumer handler) { + Require.nonNull("Handler", handler); + + return new EventListener<>(SESSION_STARTED, SessionId.class, handler); + } +} diff --git a/java/src/org/openqa/selenium/grid/node/GetNodeSessionHistory.java b/java/src/org/openqa/selenium/grid/node/GetNodeSessionHistory.java new file mode 100644 index 0000000000000..041ec94e88294 --- /dev/null +++ b/java/src/org/openqa/selenium/grid/node/GetNodeSessionHistory.java @@ -0,0 +1,45 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.openqa.selenium.grid.node; + +import static org.openqa.selenium.remote.http.Contents.asJson; + +import com.google.common.collect.ImmutableMap; +import java.io.UncheckedIOException; +import java.util.List; +import org.openqa.selenium.grid.data.SessionHistoryEntry; +import org.openqa.selenium.internal.Require; +import org.openqa.selenium.remote.http.HttpHandler; +import org.openqa.selenium.remote.http.HttpRequest; +import org.openqa.selenium.remote.http.HttpResponse; + +class GetNodeSessionHistory implements HttpHandler { + + private final Node node; + + GetNodeSessionHistory(Node node) { + this.node = Require.nonNull("Node", node); + } + + @Override + public HttpResponse execute(HttpRequest req) throws UncheckedIOException { + List sessionHistory = node.getSessionHistory(); + + return new HttpResponse().setContent(asJson(ImmutableMap.of("value", sessionHistory))); + } +} diff --git a/java/src/org/openqa/selenium/grid/node/Node.java b/java/src/org/openqa/selenium/grid/node/Node.java index d2b0d5223802b..e00e79be675e2 100644 --- a/java/src/org/openqa/selenium/grid/node/Node.java +++ b/java/src/org/openqa/selenium/grid/node/Node.java @@ -29,6 +29,8 @@ import java.io.IOException; import java.net.URI; import java.time.Duration; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.ServiceLoader; import java.util.Set; @@ -45,6 +47,7 @@ import org.openqa.selenium.grid.data.NodeId; import org.openqa.selenium.grid.data.NodeStatus; import org.openqa.selenium.grid.data.Session; +import org.openqa.selenium.grid.data.SessionHistoryEntry; import org.openqa.selenium.grid.security.RequiresSecretFilter; import org.openqa.selenium.grid.security.Secret; import org.openqa.selenium.internal.Either; @@ -189,6 +192,9 @@ protected Node( delete("/se/grid/node/session/{sessionId}") .to(params -> new StopNodeSession(this, sessionIdFrom(params))) .with(spanDecorator("node.stop_session").andThen(requiresSecret)), + get("/se/grid/node/session-history") + .to(() -> new GetNodeSessionHistory(this)) + .with(spanDecorator("node.get_session_history").andThen(requiresSecret)), get("/se/grid/node/session/{sessionId}") .to(params -> new GetNodeSession(this, sessionIdFrom(params))) .with(spanDecorator("node.get_session").andThen(requiresSecret)), @@ -268,6 +274,10 @@ public TemporaryFilesystem getDownloadsFilesystem(SessionId id) throws IOExcepti public abstract HealthCheck getHealthCheck(); + public List getSessionHistory() { + return Collections.emptyList(); + } + public Duration getSessionTimeout() { return sessionTimeout; } diff --git a/java/src/org/openqa/selenium/grid/node/config/NodeFlags.java b/java/src/org/openqa/selenium/grid/node/config/NodeFlags.java index bb65259ee3baf..8d1dc0c9308f5 100644 --- a/java/src/org/openqa/selenium/grid/node/config/NodeFlags.java +++ b/java/src/org/openqa/selenium/grid/node/config/NodeFlags.java @@ -283,6 +283,27 @@ public class NodeFlags implements HasRoles { @ConfigValue(section = NODE_SECTION, name = "enable-managed-downloads", example = "false") public Boolean managedDownloadsEnabled; + @Parameter( + names = {"--status-to-file"}, + description = + "Path to a local file where the Node will write its status information " + + "in JSON format. This file will be updated periodically and can be " + + "consumed by other services running on the same machine.") + @ConfigValue(section = NODE_SECTION, name = "status-to-file", example = "node-status.json") + public String statusFile; + + @Parameter( + names = {"--session-history-to-file"}, + description = + "Path to a local file where the Node will write session history information " + + "in JSON format. This file will contain chronological records of session " + + "start and stop events with sessionId, startTime, and stopTime.") + @ConfigValue( + section = NODE_SECTION, + name = "session-history-to-file", + example = "session-history.json") + public String sessionHistoryFile; + @Override public Set getRoles() { return Collections.singleton(NODE_ROLE); diff --git a/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java b/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java index 6691650c0ac20..b2486cdb2934f 100644 --- a/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java +++ b/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java @@ -168,6 +168,14 @@ public boolean isManagedDownloadsEnabled() { return config.getBool(NODE_SECTION, "enable-managed-downloads").orElse(Boolean.FALSE); } + public Optional getStatusFile() { + return config.get(NODE_SECTION, "status-to-file"); + } + + public Optional getSessionHistoryFile() { + return config.get(NODE_SECTION, "session-history-to-file"); + } + public String getGridSubPath() { return normalizeSubPath(getPublicGridUri().map(URI::getPath).orElse("")); } diff --git a/java/src/org/openqa/selenium/grid/node/local/LocalNode.java b/java/src/org/openqa/selenium/grid/node/local/LocalNode.java index 8561d77d2e2f5..94a65faec6c8b 100644 --- a/java/src/org/openqa/selenium/grid/node/local/LocalNode.java +++ b/java/src/org/openqa/selenium/grid/node/local/LocalNode.java @@ -47,16 +47,23 @@ import java.io.UncheckedIOException; import java.net.URI; import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import java.time.Clock; import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Queue; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -87,6 +94,9 @@ import org.openqa.selenium.grid.data.NodeId; import org.openqa.selenium.grid.data.NodeStatus; import org.openqa.selenium.grid.data.Session; +import org.openqa.selenium.grid.data.SessionClosedEvent; +import org.openqa.selenium.grid.data.SessionHistoryEntry; +import org.openqa.selenium.grid.data.SessionStartedEvent; import org.openqa.selenium.grid.data.Slot; import org.openqa.selenium.grid.data.SlotId; import org.openqa.selenium.grid.jmx.JMXHelper; @@ -146,6 +156,9 @@ public class LocalNode extends Node implements Closeable { private final AtomicInteger sessionCount = new AtomicInteger(); private final Runnable shutdown; private final ReadWriteLock drainLock = new ReentrantReadWriteLock(); + private final Optional statusFilePath; + private final Optional sessionHistoryFilePath; + private final Queue sessionHistory = new ConcurrentLinkedQueue<>(); protected LocalNode( Tracer tracer, @@ -163,7 +176,9 @@ protected LocalNode( List factories, Secret registrationSecret, boolean managedDownloadsEnabled, - int connectionLimitPerSession) { + int connectionLimitPerSession, + Optional statusFilePath, + Optional sessionHistoryFilePath) { super( tracer, new NodeId(UUID.randomUUID()), @@ -187,6 +202,8 @@ protected LocalNode( this.bidiEnabled = bidiEnabled; this.managedDownloadsEnabled = managedDownloadsEnabled; this.connectionLimitPerSession = connectionLimitPerSession; + this.statusFilePath = statusFilePath; + this.sessionHistoryFilePath = sessionHistoryFilePath; this.healthCheck = healthCheck == null @@ -277,11 +294,20 @@ protected LocalNode( return thread; }); heartbeatNodeService.scheduleAtFixedRate( - GuardedRunnable.guard(() -> bus.fire(new NodeHeartBeatEvent(getStatus()))), + GuardedRunnable.guard( + () -> { + NodeStatus status = getStatus(); + bus.fire(new NodeHeartBeatEvent(status)); + writeStatusToFile(status); + }), heartbeatPeriod.getSeconds(), heartbeatPeriod.getSeconds(), TimeUnit.SECONDS); + bus.addListener(SessionStartedEvent.listener(this::recordSessionStart)); + bus.addListener(SessionClosedEvent.listener(this::recordSessionStop)); + bus.addListener(NodeHeartBeatEvent.listener(this::cleanupSessionHistory)); + shutdown = () -> { if (heartbeatNodeService.isShutdown()) return; @@ -1005,6 +1031,11 @@ public NodeStatus getStatus() { getOsInfo()); } + @Override + public List getSessionHistory() { + return new ArrayList<>(sessionHistory); + } + @Override public HealthCheck getHealthCheck() { return healthCheck; @@ -1083,6 +1114,94 @@ private Map toJson() { factories.stream().map(SessionSlot::getStereotype).collect(Collectors.toSet())); } + private void writeStatusToFile(NodeStatus status) { + if (statusFilePath.isEmpty()) { + return; + } + + try { + String statusJson = JSON.toJson(status); + Files.write( + statusFilePath.get(), + statusJson.getBytes(), + StandardOpenOption.CREATE, + StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to write status to file: " + statusFilePath.get(), e); + } + } + + private void recordSessionStart(SessionId sessionId) { + if (!isSessionOwner(sessionId)) { + return; + } + Instant startTime = Instant.now(); + sessionHistory.add(new SessionHistoryEntry(sessionId, startTime, null)); + writeSessionHistoryToFile(); + } + + private void recordSessionStop(SessionId sessionId) { + Instant stopTime = Instant.now(); + // Find and update the existing history entry + sessionHistory.stream() + .filter(entry -> entry.getSessionId().equals(sessionId)) + .findFirst() + .ifPresent( + entry -> { + entry.setStopTime(stopTime); + writeSessionHistoryToFile(); + }); + } + + private void cleanupSessionHistory(NodeStatus status) { + int maxHistorySize = 100; + if (!status.getNodeId().equals(getId()) || sessionHistory.size() < maxHistorySize) { + return; + } + + // Keep only the last 100 completed sessions + List completedSessions = + sessionHistory.stream() + .filter(entry -> entry.getStopTime() != null) + .sorted( + (a, b) -> + b.getStartTime().compareTo(a.getStartTime())) // Sort by start time descending + .limit(100) + .collect(Collectors.toList()); + + // Keep all ongoing sessions + List ongoingSessions = + sessionHistory.stream() + .filter(entry -> entry.getStopTime() == null) + .collect(Collectors.toList()); + + // Clear and rebuild the history queue + sessionHistory.clear(); + sessionHistory.addAll(completedSessions); + sessionHistory.addAll(ongoingSessions); + + // Write the cleaned history to file + writeSessionHistoryToFile(); + } + + private void writeSessionHistoryToFile() { + if (sessionHistoryFilePath.isPresent()) { + try { + List sortedHistory = new ArrayList<>(sessionHistory); + String historyJson = JSON.toJson(sortedHistory); + Files.write( + sessionHistoryFilePath.get(), + historyJson.getBytes(), + StandardOpenOption.CREATE, + StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING); + } catch (IOException e) { + LOG.log(Level.WARNING, "Unable to write session history to file", e); + } + } + } + public static class Builder { private final Tracer tracer; @@ -1177,7 +1296,9 @@ public LocalNode build() { factories.build(), registrationSecret, managedDownloadsEnabled, - connectionLimitPerSession); + connectionLimitPerSession, + Optional.empty(), + Optional.empty()); } public Advanced advanced() { @@ -1202,6 +1323,40 @@ public Advanced healthCheck(HealthCheck healthCheck) { return this; } + public Advanced statusFile(Optional statusFile) { + return sessionHistoryFile(statusFile, Optional.empty()); + } + + public Advanced sessionHistoryFile( + Optional statusFile, Optional sessionHistoryFile) { + Optional statusFilePath = statusFile.map(Paths::get); + Optional sessionHistoryFilePath = sessionHistoryFile.map(Paths::get); + return new Advanced() { + @Override + public Node build() { + return new LocalNode( + tracer, + bus, + uri, + gridUri, + healthCheck, + maxSessions, + drainAfterSessionCount, + cdpEnabled, + bidiEnabled, + ticker, + sessionTimeout, + heartbeatPeriod, + factories.build(), + registrationSecret, + managedDownloadsEnabled, + connectionLimitPerSession, + statusFilePath, + sessionHistoryFilePath); + } + }; + } + public Node build() { return Builder.this.build(); } diff --git a/java/src/org/openqa/selenium/grid/node/local/LocalNodeFactory.java b/java/src/org/openqa/selenium/grid/node/local/LocalNodeFactory.java index 600f516b02992..319cb7531ff37 100644 --- a/java/src/org/openqa/selenium/grid/node/local/LocalNodeFactory.java +++ b/java/src/org/openqa/selenium/grid/node/local/LocalNodeFactory.java @@ -100,7 +100,10 @@ public static Node create(Config config) { .forEach((caps, factories) -> factories.forEach(factory -> builder.add(caps, factory))); } - return builder.build(); + return builder + .advanced() + .sessionHistoryFile(nodeOptions.getStatusFile(), nodeOptions.getSessionHistoryFile()) + .build(); } private static Collection createSessionFactory( diff --git a/java/src/org/openqa/selenium/grid/node/local/SessionSlot.java b/java/src/org/openqa/selenium/grid/node/local/SessionSlot.java index 3c51b785c13c0..54400d41f1e30 100644 --- a/java/src/org/openqa/selenium/grid/node/local/SessionSlot.java +++ b/java/src/org/openqa/selenium/grid/node/local/SessionSlot.java @@ -37,6 +37,7 @@ import org.openqa.selenium.events.EventBus; import org.openqa.selenium.grid.data.CreateSessionRequest; import org.openqa.selenium.grid.data.SessionClosedEvent; +import org.openqa.selenium.grid.data.SessionStartedEvent; import org.openqa.selenium.grid.node.ActiveSession; import org.openqa.selenium.grid.node.SessionFactory; import org.openqa.selenium.grid.node.relay.RelaySessionFactory; @@ -153,6 +154,7 @@ public Either apply(CreateSessionRequest sess ActiveSession session = possibleSession.right(); currentSession = session; connectionCounter.set(0); + bus.fire(new SessionStartedEvent(session.getId())); return Either.right(session); } else { return Either.left(possibleSession.left()); diff --git a/java/src/org/openqa/selenium/grid/node/remote/RemoteNode.java b/java/src/org/openqa/selenium/grid/node/remote/RemoteNode.java index 947ee18e79978..58f2bd262d114 100644 --- a/java/src/org/openqa/selenium/grid/node/remote/RemoteNode.java +++ b/java/src/org/openqa/selenium/grid/node/remote/RemoteNode.java @@ -38,6 +38,7 @@ import java.net.URI; import java.time.Duration; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -53,6 +54,7 @@ import org.openqa.selenium.grid.data.NodeId; import org.openqa.selenium.grid.data.NodeStatus; import org.openqa.selenium.grid.data.Session; +import org.openqa.selenium.grid.data.SessionHistoryEntry; import org.openqa.selenium.grid.node.HealthCheck; import org.openqa.selenium.grid.node.Node; import org.openqa.selenium.grid.security.AddSecretFilter; @@ -208,6 +210,16 @@ public void releaseConnection(SessionId id) { Values.get(res, Void.class); } + @Override + public List getSessionHistory() { + HttpRequest req = new HttpRequest(GET, "/se/grid/node/session-history"); + HttpTracing.inject(tracer, tracer.getCurrentContext(), req); + + HttpResponse res = client.with(addSecret).execute(req); + + return Values.get(res, List.class); + } + @Override public Session getSession(SessionId id) throws NoSuchSessionException { Require.nonNull("Session ID", id); diff --git a/java/test/org/openqa/selenium/grid/node/GetNodeSessionHistoryTest.java b/java/test/org/openqa/selenium/grid/node/GetNodeSessionHistoryTest.java new file mode 100644 index 0000000000000..f7de55f518524 --- /dev/null +++ b/java/test/org/openqa/selenium/grid/node/GetNodeSessionHistoryTest.java @@ -0,0 +1,149 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.openqa.selenium.grid.node; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openqa.selenium.remote.http.HttpMethod.GET; + +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.grid.data.SessionHistoryEntry; +import org.openqa.selenium.json.Json; +import org.openqa.selenium.remote.SessionId; +import org.openqa.selenium.remote.http.HttpRequest; +import org.openqa.selenium.remote.http.HttpResponse; + +class GetNodeSessionHistoryTest { + + @Test + void shouldReturnSessionHistoryAsJson() { + SessionId sessionId = new SessionId("test-session"); + Instant startTime = Instant.now(); + Instant stopTime = startTime.plusSeconds(60); + SessionHistoryEntry entry = new SessionHistoryEntry(sessionId, startTime, stopTime); + + TestNode node = new TestNode(Arrays.asList(entry)); + GetNodeSessionHistory handler = new GetNodeSessionHistory(node); + + HttpResponse response = handler.execute(new HttpRequest(GET, "/")); + + assertThat(response.getStatus()).isEqualTo(200); + String content = response.getContentString(); + assertThat(content).contains("test-session"); + assertThat(content).contains("value"); + + Json json = new Json(); + Object responseObj = json.toType(content, Object.class); + assertThat(responseObj).isNotNull(); + } + + @Test + void shouldReturnEmptyListWhenNoHistory() { + TestNode node = new TestNode(Arrays.asList()); + GetNodeSessionHistory handler = new GetNodeSessionHistory(node); + + HttpResponse response = handler.execute(new HttpRequest(GET, "/")); + + assertThat(response.getStatus()).isEqualTo(200); + String content = response.getContentString(); + assertThat(content).contains("value"); + assertThat(content).contains("[]"); + } + + private static class TestNode extends Node { + private final List history; + + TestNode(List history) { + super(null, null, null, null, null); + this.history = history; + } + + @Override + public List getSessionHistory() { + return history; + } + + @Override + public org.openqa.selenium.internal.Either< + org.openqa.selenium.WebDriverException, + org.openqa.selenium.grid.data.CreateSessionResponse> + newSession(org.openqa.selenium.grid.data.CreateSessionRequest sessionRequest) { + return null; + } + + @Override + public org.openqa.selenium.remote.http.HttpResponse executeWebDriverCommand( + org.openqa.selenium.remote.http.HttpRequest req) { + return null; + } + + @Override + public org.openqa.selenium.grid.data.Session getSession( + org.openqa.selenium.remote.SessionId id) { + return null; + } + + @Override + public org.openqa.selenium.remote.http.HttpResponse uploadFile( + org.openqa.selenium.remote.http.HttpRequest req, org.openqa.selenium.remote.SessionId id) { + return null; + } + + @Override + public org.openqa.selenium.remote.http.HttpResponse downloadFile( + org.openqa.selenium.remote.http.HttpRequest req, org.openqa.selenium.remote.SessionId id) { + return null; + } + + @Override + public void stop(org.openqa.selenium.remote.SessionId id) {} + + @Override + public boolean isSessionOwner(org.openqa.selenium.remote.SessionId id) { + return false; + } + + @Override + public boolean tryAcquireConnection(org.openqa.selenium.remote.SessionId id) { + return false; + } + + @Override + public void releaseConnection(org.openqa.selenium.remote.SessionId id) {} + + @Override + public boolean isSupporting(org.openqa.selenium.Capabilities capabilities) { + return false; + } + + @Override + public org.openqa.selenium.grid.data.NodeStatus getStatus() { + return null; + } + + @Override + public org.openqa.selenium.grid.node.HealthCheck getHealthCheck() { + return null; + } + + @Override + public void drain() {} + } +} diff --git a/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java b/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java index 904c3cd158874..05ba70a903439 100644 --- a/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java +++ b/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java @@ -193,6 +193,39 @@ void cdpCanBeDisabled() { assertThat(nodeOptions.isCdpEnabled()).isFalse(); } + @Test + void statusFileCanBeConfigured() { + Config config = + new MapConfig(singletonMap("node", singletonMap("status-to-file", "node-status.json"))); + NodeOptions nodeOptions = new NodeOptions(config); + assertThat(nodeOptions.getStatusFile()).isPresent(); + assertThat(nodeOptions.getStatusFile().get()).isEqualTo("node-status.json"); + } + + @Test + void statusFileIsOptionalByDefault() { + Config config = new MapConfig(emptyMap()); + NodeOptions nodeOptions = new NodeOptions(config); + assertThat(nodeOptions.getStatusFile()).isEmpty(); + } + + @Test + void sessionHistoryFileCanBeConfigured() { + Config config = + new MapConfig( + singletonMap("node", singletonMap("session-history-to-file", "session-history.json"))); + NodeOptions nodeOptions = new NodeOptions(config); + assertThat(nodeOptions.getSessionHistoryFile()).isPresent(); + assertThat(nodeOptions.getSessionHistoryFile().get()).isEqualTo("session-history.json"); + } + + @Test + void sessionHistoryFileIsOptionalByDefault() { + Config config = new MapConfig(emptyMap()); + NodeOptions nodeOptions = new NodeOptions(config); + assertThat(nodeOptions.getSessionHistoryFile()).isEmpty(); + } + @Test void shouldDetectCorrectDriversOnMac() { assumeTrue(Platform.getCurrent().is(Platform.MAC)); diff --git a/java/test/org/openqa/selenium/grid/node/local/LocalNodeTest.java b/java/test/org/openqa/selenium/grid/node/local/LocalNodeTest.java index ccd2b44f3dae8..4df7ba559f27a 100644 --- a/java/test/org/openqa/selenium/grid/node/local/LocalNodeTest.java +++ b/java/test/org/openqa/selenium/grid/node/local/LocalNodeTest.java @@ -25,8 +25,11 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Instant; import java.util.ArrayList; import java.util.List; @@ -434,4 +437,143 @@ void bidiIsDisabledAndResponseCapsShowThat() throws URISyntaxException { assertThat(bidiEnabled).isNotNull(); assertThat(Boolean.parseBoolean(bidiEnabled.toString())).isFalse(); } + + @Test + void statusFileIsWrittenWhenConfigured() throws URISyntaxException, IOException { + Tracer tracer = DefaultTestTracer.createTracer(); + EventBus bus = new GuavaEventBus(); + URI uri = new URI("http://localhost:7890"); + Capabilities stereotype = new ImmutableCapabilities("browserName", "cheese"); + + Path tempStatusFile = Files.createTempFile("node-status", ".json"); + tempStatusFile.toFile().deleteOnExit(); + + LocalNode localNode = + LocalNode.builder(tracer, bus, uri, uri, registrationSecret) + .add( + stereotype, + new TestSessionFactory( + (id, caps) -> new Session(id, uri, stereotype, caps, Instant.now()))) + .advanced() + .statusFile(Optional.of(tempStatusFile.toString())) + .build(); + + NodeStatus status = localNode.getStatus(); + assertThat(status).isNotNull(); + + assertThat(Files.exists(tempStatusFile)).isTrue(); + String statusContent = Files.readString(tempStatusFile); + assertThat(statusContent).isNotEmpty(); + assertThat(statusContent).contains("\"nodeId\""); + assertThat(statusContent).contains("\"uri\""); + } + + @Test + void statusFileIsNotWrittenWhenNotConfigured() throws URISyntaxException { + Tracer tracer = DefaultTestTracer.createTracer(); + EventBus bus = new GuavaEventBus(); + URI uri = new URI("http://localhost:7890"); + Capabilities stereotype = new ImmutableCapabilities("browserName", "cheese"); + + LocalNode localNode = + LocalNode.builder(tracer, bus, uri, uri, registrationSecret) + .add( + stereotype, + new TestSessionFactory( + (id, caps) -> new Session(id, uri, stereotype, caps, Instant.now()))) + .build(); + + NodeStatus status = localNode.getStatus(); + assertThat(status).isNotNull(); + } + + @Test + void sessionHistoryIsWrittenWhenConfigured() throws URISyntaxException, IOException { + Tracer tracer = DefaultTestTracer.createTracer(); + EventBus bus = new GuavaEventBus(); + URI uri = new URI("http://localhost:7890"); + Capabilities stereotype = new ImmutableCapabilities("browserName", "cheese"); + + Path tempHistoryFile = Files.createTempFile("session-history", ".json"); + tempHistoryFile.toFile().deleteOnExit(); + + LocalNode localNode = + LocalNode.builder(tracer, bus, uri, uri, registrationSecret) + .add( + stereotype, + new TestSessionFactory( + (id, caps) -> new Session(id, uri, stereotype, caps, Instant.now()))) + .advanced() + .sessionHistoryFile(Optional.empty(), Optional.of(tempHistoryFile.toString())) + .build(); + + Either response = + localNode.newSession( + new CreateSessionRequest(ImmutableSet.of(W3C), stereotype, ImmutableMap.of())); + assertThat(response.isRight()).isTrue(); + + SessionId sessionId = response.right().getSession().getId(); + localNode.stop(sessionId); + + assertThat(Files.exists(tempHistoryFile)).isTrue(); + String historyContent = Files.readString(tempHistoryFile); + assertThat(historyContent).isNotEmpty(); + assertThat(historyContent).contains(sessionId.toString()); + assertThat(historyContent).contains("startTime"); + assertThat(historyContent).contains("stopTime"); + } + + @Test + void sessionHistoryIsNotWrittenWhenNotConfigured() throws URISyntaxException { + Tracer tracer = DefaultTestTracer.createTracer(); + EventBus bus = new GuavaEventBus(); + URI uri = new URI("http://localhost:7890"); + Capabilities stereotype = new ImmutableCapabilities("browserName", "cheese"); + + LocalNode localNode = + LocalNode.builder(tracer, bus, uri, uri, registrationSecret) + .add( + stereotype, + new TestSessionFactory( + (id, caps) -> new Session(id, uri, stereotype, caps, Instant.now()))) + .build(); + + Either response = + localNode.newSession( + new CreateSessionRequest(ImmutableSet.of(W3C), stereotype, ImmutableMap.of())); + assertThat(response.isRight()).isTrue(); + + SessionId sessionId = response.right().getSession().getId(); + localNode.stop(sessionId); + } + + @Test + void sessionHistoryEndpointReturnsCorrectData() throws URISyntaxException { + Tracer tracer = DefaultTestTracer.createTracer(); + EventBus bus = new GuavaEventBus(); + URI uri = new URI("http://localhost:7890"); + Capabilities stereotype = new ImmutableCapabilities("browserName", "cheese"); + + LocalNode localNode = + LocalNode.builder(tracer, bus, uri, uri, registrationSecret) + .add( + stereotype, + new TestSessionFactory( + (id, caps) -> new Session(id, uri, stereotype, caps, Instant.now()))) + .build(); + + Either response = + localNode.newSession( + new CreateSessionRequest(ImmutableSet.of(W3C), stereotype, ImmutableMap.of())); + assertThat(response.isRight()).isTrue(); + + SessionId sessionId = response.right().getSession().getId(); + localNode.stop(sessionId); + + List history = localNode.getSessionHistory(); + assertThat(history).hasSize(1); + assertThat(history.get(0).getSessionId()).isEqualTo(sessionId); + assertThat(history.get(0).getStartTime()).isNotNull(); + assertThat(history.get(0).getStopTime()).isNotNull(); + } }