From 9ad424d68d1ec2cb5132dad351e7388c87911d71 Mon Sep 17 00:00:00 2001 From: Scott Babcock Date: Tue, 21 Apr 2026 13:28:23 -0700 Subject: [PATCH] Add settings for PUB/SUB port assignments --- gradle.properties | 2 +- .../selenium/AbstractSeleniumConfig.java | 94 +++++++++++-- .../automation/selenium/DriverPlugin.java | 2 +- .../selenium/core/ServerPidFinder.java | 29 +++-- .../selenium/grid/BackendCapabilities.java | 60 +++++++++ .../selenium/grid/GridControlPlane.java | 34 +++++ .../selenium/grid/GridInstanceManager.java | 85 ++++++++++++ .../automation/selenium/grid/HubInstance.java | 24 ++++ .../automation/selenium/grid/HubSpec.java | 6 + .../selenium/grid/ManagedProcess.java | 81 ++++++++++++ .../selenium/grid/NodeInstance.java | 15 +++ .../automation/selenium/grid/NodeManager.java | 47 +++++++ .../automation/selenium/grid/NodeSpec.java | 6 + .../selenium/grid/Seleniuim4Backend.java | 9 ++ .../selenium/grid/Selenium3Backend.java | 78 +++++++++++ .../selenium/grid/Selenium3Capabilities.java | 18 +++ .../selenium/grid/Selenium4Capabilities.java | 18 +++ .../interfaces/BackendCapabilities.java | 16 +++ .../selenium/interfaces/GridBackend.java | 19 +++ .../interfaces/GridInstanceRegistry.java | 26 ++++ .../selenium/interfaces/GridRegistry.java | 18 +++ .../selenium/interfaces/NodeRegistry.java | 12 ++ .../plugins/RemoteWebDriverPlugin.java | 6 +- .../selenium/support/TestNgPlatformBase.java | 2 +- .../automation/selenium/SeleniumConfig.java | 10 ++ .../selenium/core/LocalSeleniumGrid.java | 29 +++++ .../selenium/core/ServerProcessKiller.java | 2 +- .../automation/selenium/SeleniumConfig.java | 18 ++- .../selenium/core/LocalSeleniumGrid.java | 58 ++++++++- .../selenium/core/ServerProcessKiller.java | 2 +- .../plugins/AbstractAppiumPlugin.java | 2 +- .../utility/GridHubPortAllocator.java | 123 ++++++++++++++++++ 32 files changed, 913 insertions(+), 38 deletions(-) create mode 100644 src/main/java/com/nordstrom/automation/selenium/grid/BackendCapabilities.java create mode 100644 src/main/java/com/nordstrom/automation/selenium/grid/GridControlPlane.java create mode 100644 src/main/java/com/nordstrom/automation/selenium/grid/GridInstanceManager.java create mode 100644 src/main/java/com/nordstrom/automation/selenium/grid/HubInstance.java create mode 100644 src/main/java/com/nordstrom/automation/selenium/grid/HubSpec.java create mode 100644 src/main/java/com/nordstrom/automation/selenium/grid/ManagedProcess.java create mode 100644 src/main/java/com/nordstrom/automation/selenium/grid/NodeInstance.java create mode 100644 src/main/java/com/nordstrom/automation/selenium/grid/NodeManager.java create mode 100644 src/main/java/com/nordstrom/automation/selenium/grid/NodeSpec.java create mode 100644 src/main/java/com/nordstrom/automation/selenium/grid/Seleniuim4Backend.java create mode 100644 src/main/java/com/nordstrom/automation/selenium/grid/Selenium3Backend.java create mode 100644 src/main/java/com/nordstrom/automation/selenium/grid/Selenium3Capabilities.java create mode 100644 src/main/java/com/nordstrom/automation/selenium/grid/Selenium4Capabilities.java create mode 100644 src/main/java/com/nordstrom/automation/selenium/interfaces/BackendCapabilities.java create mode 100644 src/main/java/com/nordstrom/automation/selenium/interfaces/GridBackend.java create mode 100644 src/main/java/com/nordstrom/automation/selenium/interfaces/GridInstanceRegistry.java create mode 100644 src/main/java/com/nordstrom/automation/selenium/interfaces/GridRegistry.java create mode 100644 src/main/java/com/nordstrom/automation/selenium/interfaces/NodeRegistry.java create mode 100644 src/selenium4/java/com/nordstrom/automation/selenium/utility/GridHubPortAllocator.java diff --git a/gradle.properties b/gradle.properties index 9ee10952..1c6eff29 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ profile=selenium4 -version=28.0.0-SNAPSHOT +version=34.0.1-SNAPSHOT diff --git a/src/main/java/com/nordstrom/automation/selenium/AbstractSeleniumConfig.java b/src/main/java/com/nordstrom/automation/selenium/AbstractSeleniumConfig.java index d2b48649..c48a5b0d 100644 --- a/src/main/java/com/nordstrom/automation/selenium/AbstractSeleniumConfig.java +++ b/src/main/java/com/nordstrom/automation/selenium/AbstractSeleniumConfig.java @@ -29,6 +29,7 @@ import org.apache.http.client.utils.URIBuilder; import org.openqa.selenium.Capabilities; import org.openqa.selenium.SearchContext; +import org.openqa.selenium.net.PortProber; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,6 +45,7 @@ import com.nordstrom.automation.selenium.servlet.ExamplePageServlet.FrameC_Servlet; import com.nordstrom.automation.selenium.servlet.ExamplePageServlet.FrameD_Servlet; import com.nordstrom.automation.selenium.support.SearchContextWait; +import com.nordstrom.automation.selenium.utility.GridHubPortAllocator; import com.nordstrom.automation.selenium.utility.HostUtils; import com.nordstrom.automation.settings.SettingsCore; import com.nordstrom.common.base.UncheckedThrow; @@ -159,8 +161,8 @@ public enum SeleniumSettings implements SettingsCore.SettingsAPI { * name: selenium.grid.launcher
* default: (populated by {@link SeleniumConfig#getDefaults() getDefaults()}) * */ GRID_LAUNCHER("selenium.grid.launcher", null), @@ -178,8 +180,8 @@ public enum SeleniumSettings implements SettingsCore.SettingsAPI { * This setting specifies the configuration file name/path for the local Selenium Grid hub server. *

* name: selenium.hub.config
- * Selenium 3: hubConfig-s3.json
- * Selenium 4: hubConfig-s4.json + * Selenium 4: hubConfig-s4.json
+ * Selenium 3: hubConfig-s3.json */ HUB_CONFIG("selenium.hub.config", null), @@ -187,8 +189,8 @@ public enum SeleniumSettings implements SettingsCore.SettingsAPI { * This is the URL for the Selenium Grid endpoint: [scheme:][//authority]/wd/hub *

* name: selenium.hub.host
- * Selenium 3: http://<{@code localhost}>:4445/wd/hub
- * Selenium 4: http://<{@code localhost}>:4446/wd/hub + * Selenium 4: http://<{@code localhost}>:4444/wd/hub
+ * Selenium 3: http://<{@code localhost}>:4445/wd/hub */ HUB_HOST("selenium.hub.host", null), @@ -196,11 +198,27 @@ public enum SeleniumSettings implements SettingsCore.SettingsAPI { * This is the port assigned to the local Selenium Grid hub server. *

* name: selenium.hub.port
- * Selenium 3: 4445
- * Selenium 4: 4446 + * Selenium 4: 4444
+ * Selenium 3: 4445 */ HUB_PORT("selenium.hub.port", null), + /** + * This is the port used by local Selenium Grid components for publishing events. + *

+ * name: selenium.publish.port
+ * default: 4442 + */ + PUBLISH_PORT("selenium.publish.port", "4442"), + + /** + * This is the port used by local Selenium Grid components for subscribing to events. + *

+ * name: selenium.subscribe.port
+ * default: 4443 + */ + SUBSCRIBE_PORT("selenium.subscribe.port", "4443"), + /** * This setting specifies the slot matcher used by the local Selenium Grid hub server. * @@ -221,8 +239,8 @@ public enum SeleniumSettings implements SettingsCore.SettingsAPI { * This setting specifies the configuration template name/path for local Selenium Grid node servers. *

* name: selenium.node.config
- * Selenium 3: nodeConfig-s3.json
- * Selenium 4: nodeConfig-s4.json + * Selenium 4: nodeConfig-s4.json
+ * Selenium 3: nodeConfig-s3.json */ NODE_CONFIG("selenium.node.config", null), @@ -354,11 +372,20 @@ public enum SeleniumSettings implements SettingsCore.SettingsAPI { */ CONTEXT_PLATFORM("selenium.context.platform", "support"), + /** + * This setting specifies the port that the {@code Appium} server listens on. + *

+ * name: appium.server.port
+ * default: 4723 + */ + APPIUM_SERVER_PORT("appium.server.port", "4723"), + /** * This setting specifies the path to an {@code Appium} configuration file provided to the server * when it's launched as a local Selenium Grid node server. *

- * NOTE: If specified, this setting + * NOTE: If specified, the config file indicated by this setting provides base values for the options + * it declares. These values can be supplemented or overridden via the {@link #APPIUM_CLI_ARGS} setting. *

* name: appium.config.path
* default: {@code null} @@ -693,6 +720,22 @@ public synchronized URL getHubUrl() { return hubUrl; } + /** + * Get the Selenium Grid event bus 'publish' URL. + * + * @return URL for publishing Grid events + * @see SeleniumSettings#PUBLISH_PORT + */ + public abstract String getPublishUrl(); + + /** + * Get the Selenium Grid event bus 'subscribe' URL. + * + * @return URL for subscribing to Grid events + * @see SeleniumSettings#SUBSCRIBE_PORT + */ + public abstract String getSubscribeUrl(); + /** * Get object that represents the active Selenium Grid. * @@ -822,6 +865,16 @@ public Path getHubConfigPath() { return hubConfigPath; } + /** + * Get the port that the {@code Appium} server listens on. + * + * @return {@link SeleniumSettings#APPIUM_SERVER_PORT APPIUM_SERVER_PORT} if defined and available; + * otherwise random available port + */ + public int getAppiumServerPort() { + return getAvailablePort(SeleniumSettings.APPIUM_SERVER_PORT); + } + /** * Get the path to the Appium configuration. * @@ -1117,4 +1170,25 @@ public boolean appiumWithPM2() { public String getSettingsPath() { return SETTINGS_FILE; } + + /** + * Get available port for the specified setting. + * + * @param setting setting to check for port availability + * @return value of setting if defined and available; otherwise random available port + */ + protected int getAvailablePort(final SeleniumSettings setting) { + int port = getInt(setting.key(), -1); + if (port != -1) { + if (!GridHubPortAllocator.isFree(port)) { + LOGGER.warn("{} port '{}' is unavailable; finding free port", setting.name(), port); + port = -1; + } + } + if (port == -1) { + port = PortProber.findFreePort(); + System.setProperty(setting.key(), Integer.toString(port)); + } + return port; + } } diff --git a/src/main/java/com/nordstrom/automation/selenium/DriverPlugin.java b/src/main/java/com/nordstrom/automation/selenium/DriverPlugin.java index db2a0415..08b0962f 100644 --- a/src/main/java/com/nordstrom/automation/selenium/DriverPlugin.java +++ b/src/main/java/com/nordstrom/automation/selenium/DriverPlugin.java @@ -109,7 +109,7 @@ default LocalGridServer create(SeleniumConfig config, String launcherClassName, * @throws IOException if an I/O error occurs */ LocalGridServer create(SeleniumConfig config, String launcherClassName, String[] dependencyContexts, - URL hubUrl, final Path workingPath, final Path outputPath) throws IOException; + URL hubUrl, Path workingPath, Path outputPath) throws IOException; /** * Get constructor for this driver's {@link RemoteWebDriver} implementation. diff --git a/src/main/java/com/nordstrom/automation/selenium/core/ServerPidFinder.java b/src/main/java/com/nordstrom/automation/selenium/core/ServerPidFinder.java index b9980655..81c68468 100644 --- a/src/main/java/com/nordstrom/automation/selenium/core/ServerPidFinder.java +++ b/src/main/java/com/nordstrom/automation/selenium/core/ServerPidFinder.java @@ -13,17 +13,25 @@ public class ServerPidFinder { private enum PidFinder { - WINDOWS("cmd.exe", "/c", "for /f \"tokens=5\" %%a in ('netstat -ano ^| findstr :%d ^| findstr LISTENING') do @echo %%a"), - MAC_UNIX("sh", "-c", "lsof -iTCP:%d -sTCP:LISTEN -t"); + WINDOWS("cmd.exe", + "/c", + "for /f \"tokens=5\" %%a in ('netstat -ano ^| findstr :%d ^| findstr LISTENING') do @echo %%a", + "for /f \"tokens=5\" %%a in ('netstat -ano ^| findstr :%d') do @echo %%a"), + MAC_UNIX("sh", + "-c", + "lsof -nP -iTCP:%d -sTCP:LISTEN -t", + "lsof -nP -iTCP:%d -t"); private String executable; private String commandOption; - private String commandFormat; + private String listenMode; + private String anyMode; - PidFinder(String execuable, String commandOption, String commandFormat) { + PidFinder(String execuable, String commandOption, String listenMode, String anyMode) { this.executable = execuable; this.commandOption = commandOption; - this.commandFormat = commandFormat; + this.listenMode = listenMode; + this.anyMode = anyMode; } String getExecutable() { @@ -34,8 +42,8 @@ String getOption() { return commandOption; } - String getCommand(int port) { - return String.format(commandFormat, port); + String getCommand(int port, boolean listen) { + return String.format(listen ? listenMode : anyMode, port); } } @@ -50,16 +58,17 @@ private ServerPidFinder() { * Get the process ID of the server listening to the specified port. * * @param port {@code localhost} port to check + * @param listen {@code true} to require LISTEN mode; {@code false} to accept any mode * @return if found, ID of listening process; otherwise {@code null} */ - public static String getPidOfServerAt(int port) { + public static String getPidOfServerAt(int port, boolean listen) { String pid = null; try { PidFinder finder = OSInfo.getDefault().getType() == OSInfo.OSType.WINDOWS ? PidFinder.WINDOWS : PidFinder.MAC_UNIX; - - ProcessBuilder pb = new ProcessBuilder(finder.getExecutable(), finder.getOption(), finder.getCommand(port)); + ProcessBuilder pb = + new ProcessBuilder(finder.getExecutable(), finder.getOption(), finder.getCommand(port, listen)); Process process = pb.start(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { diff --git a/src/main/java/com/nordstrom/automation/selenium/grid/BackendCapabilities.java b/src/main/java/com/nordstrom/automation/selenium/grid/BackendCapabilities.java new file mode 100644 index 00000000..39e76bbc --- /dev/null +++ b/src/main/java/com/nordstrom/automation/selenium/grid/BackendCapabilities.java @@ -0,0 +1,60 @@ +package com.nordstrom.automation.selenium.grid; + +public final class BackendCapabilities { + + private final boolean dynamicPorts; + private final boolean managedNodes; + private final boolean gracefulShutdown; + private final boolean healthCheck; + private final boolean externalDiscovery; + + public BackendCapabilities(boolean dynamicPorts, + boolean managedNodes, + boolean gracefulShutdown, + boolean healthCheck, + boolean externalDiscovery) { + this.dynamicPorts = dynamicPorts; + this.managedNodes = managedNodes; + this.gracefulShutdown = gracefulShutdown; + this.healthCheck = healthCheck; + this.externalDiscovery = externalDiscovery; + } + + public boolean supportsDynamicPorts() { return dynamicPorts; } + + public boolean supportsManagedNodes() { return managedNodes; } + + public boolean supportsGracefulShutdown() { return gracefulShutdown; } + + public boolean supportsHealthCheck() { return healthCheck; } + + public boolean supportsExternalDiscovery() { return externalDiscovery; } + + public static BackendCapabilities selenium3() { + return new BackendCapabilities( + true, + true, + false, + true, + true + ); + } + + public static BackendCapabilities selenium4() { + return new BackendCapabilities( + true, + true, + true, + true, + true + ); + } + + BackendCapabilities dockerCaps = new BackendCapabilities( + false, + false, + true, + true, + true + ); +} \ No newline at end of file diff --git a/src/main/java/com/nordstrom/automation/selenium/grid/GridControlPlane.java b/src/main/java/com/nordstrom/automation/selenium/grid/GridControlPlane.java new file mode 100644 index 00000000..d62afe1a --- /dev/null +++ b/src/main/java/com/nordstrom/automation/selenium/grid/GridControlPlane.java @@ -0,0 +1,34 @@ +package com.nordstrom.automation.selenium.grid; + +import com.nordstrom.automation.selenium.interfaces.BackendCapabilities; +import com.nordstrom.automation.selenium.interfaces.GridBackend; + +public class GridControlPlane implements GridBackend { + + private final GridBackend backend; + + public GridControlPlane(GridBackend backend) { + this.backend = backend; + } + + public HubInstance startHub(HubSpec spec) { + return backend.startHub(spec); + } + + public NodeInstance startNode(NodeSpec spec) { + return backend.startNode(spec); + } + + public void requestHubShutdown(HubInstance instance) { + backend.requestHubShutdown(instance); + } + + public void requestNodeShutdown(NodeInstance node) { + backend.requestNodeShutdown(node); + } + + public BackendCapabilities capabilities() { + return backend.capabilities(); + } + +} diff --git a/src/main/java/com/nordstrom/automation/selenium/grid/GridInstanceManager.java b/src/main/java/com/nordstrom/automation/selenium/grid/GridInstanceManager.java new file mode 100644 index 00000000..b6080a81 --- /dev/null +++ b/src/main/java/com/nordstrom/automation/selenium/grid/GridInstanceManager.java @@ -0,0 +1,85 @@ +package com.nordstrom.automation.selenium.grid; + +import java.io.IOException; +import java.util.List; + +import com.nordstrom.automation.selenium.interfaces.GridInstanceRegistry; +import com.nordstrom.automation.selenium.utility.GridHubPortAllocator; +import com.nordstrom.automation.selenium.utility.GridHubPortAllocator.GridPorts; + +public class GridInstanceManager { + + private final GridInstanceRegistry registry; + + public GridInstanceManager(GridInstanceRegistry registry) { + this.registry = registry; + } + + public GridInstance start(int hubPort) throws IOException { + + GridPorts ports = GridHubPortAllocator.allocate(hubPort); + + Process process = launchGrid(ports); + + Long pid = getPidIfAvailable(process); + + GridInstance instance = new GridInstance( + ports.hubPort, + ports.eventBusPubPort, + ports.eventBusSubPort, + pid, + "http://127.0.0.1:" + ports.hubPort + ); + + registry.register(instance); + + return instance; + } + + public void stop(GridInstance instance) { + + // Preferred path: PID shutdown + if (instance.pid != null) { + try { + ProcessHandle.of(instance.pid) + .ifPresent(ProcessHandle::destroy); + registry.remove(instance); + return; + } catch (Exception ignored) {} + } + + // Fallback path: port-based shutdown strategy + stopByPortStrategy(instance); + registry.remove(instance); + } + + private void stopByPortStrategy(GridInstance instance) { + + // Option A: if you control startup → send shutdown signal + tryShutdownEndpoint(instance.baseUrl); + + // Option B: last resort (safe, not hacky) + // do nothing except mark as stopped + } + + public GridInstance startHub(int hubPort) { + return hubManager.start(hubPort); + } + + public void stopHub(GridInstance instance) { + hubManager.stop(instance); + } + + public NodeInstance startNode(String hubUrl) { + return nodeManager.startManagedNode(hubUrl); + } + + public void stopNode(NodeInstance node) { + nodeManager.stopManagedNode(node); + } + + public List getExternalNodes(String hubUrl) { + return nodeManager.listExternalNodes(hubUrl); + } + + \ No newline at end of file diff --git a/src/main/java/com/nordstrom/automation/selenium/grid/HubInstance.java b/src/main/java/com/nordstrom/automation/selenium/grid/HubInstance.java new file mode 100644 index 00000000..0d911e6e --- /dev/null +++ b/src/main/java/com/nordstrom/automation/selenium/grid/HubInstance.java @@ -0,0 +1,24 @@ +package com.nordstrom.automation.selenium.grid; + +public final class HubInstance { + public final int hubPort; + public final Integer eventBusPubPort; + public final Integer eventBusSubPort; + + // optional (may be null in Termux/proot) + public final Long pid; + + public final String baseUrl; + + public HubInstance(int hubPort, + Integer pub, + Integer sub, + Long pid, + String baseUrl) { + this.hubPort = hubPort; + this.eventBusPubPort = pub; + this.eventBusSubPort = sub; + this.pid = pid; + this.baseUrl = baseUrl; + } +} diff --git a/src/main/java/com/nordstrom/automation/selenium/grid/HubSpec.java b/src/main/java/com/nordstrom/automation/selenium/grid/HubSpec.java new file mode 100644 index 00000000..b5fdf750 --- /dev/null +++ b/src/main/java/com/nordstrom/automation/selenium/grid/HubSpec.java @@ -0,0 +1,6 @@ +package com.nordstrom.automation.selenium.grid; + +public class HubSpec { + public int preferredPort; + public boolean allowDynamicPort; +} diff --git a/src/main/java/com/nordstrom/automation/selenium/grid/ManagedProcess.java b/src/main/java/com/nordstrom/automation/selenium/grid/ManagedProcess.java new file mode 100644 index 00000000..7b902038 --- /dev/null +++ b/src/main/java/com/nordstrom/automation/selenium/grid/ManagedProcess.java @@ -0,0 +1,81 @@ +package com.nordstrom.automation.selenium.grid; + +import java.net.HttpURLConnection; +import java.net.URL; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +public final class ManagedProcess { + + private final ProcessHandle handle; // may be null + private final Long pid; // persisted fallback + private final String endpoint; // e.g. http://localhost:4444 + + public ManagedProcess(Process process, String endpoint) { + this.handle = process.toHandle(); + this.pid = handle.pid(); + this.endpoint = endpoint; + } + + public ManagedProcess(Long pid, String endpoint) { + this.handle = null; + this.pid = pid; + this.endpoint = endpoint; + } + + public boolean stop(BackendCapabilities caps) { + + // 1. Try graceful shutdown first if supported + if (caps.supportsGracefulShutdown() && endpoint != null) { + if (tryHttpShutdown(endpoint)) { + return true; + } + } + + // 2. Try live ProcessHandle (best case) + if (handle != null && handle.isAlive()) { + handle.destroy(); + return true; + } + + // 3. Try PID fallback (after restart) + if (pid != null) { + return ProcessHandle.of(pid) + .map(ph -> { + ph.destroy(); + return true; + }) + .orElse(false); + } + + // 4. Last resort: give up safely + return false; + } + + public boolean stopAndWait(BackendCapabilities caps, Duration timeout) { + if (!stop(caps)) return false; + + if (pid != null) { + return ProcessHandle.of(pid) + .map(ph -> ph.onExit() + .completeOnTimeout(null, timeout.toMillis(), TimeUnit.MILLISECONDS) + .join() != null) + .orElse(true); + } + + return true; + } + + private boolean tryHttpShutdown(String endpoint) { + try { + HttpURLConnection conn = + (HttpURLConnection) new URL(endpoint + "/shutdown").openConnection(); + conn.setRequestMethod("POST"); + conn.setConnectTimeout(1000); + conn.setReadTimeout(1000); + return conn.getResponseCode() < 500; + } catch (Exception e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/nordstrom/automation/selenium/grid/NodeInstance.java b/src/main/java/com/nordstrom/automation/selenium/grid/NodeInstance.java new file mode 100644 index 00000000..01bcd543 --- /dev/null +++ b/src/main/java/com/nordstrom/automation/selenium/grid/NodeInstance.java @@ -0,0 +1,15 @@ +package com.nordstrom.automation.selenium.grid; + +public final class NodeInstance { + public final String nodeUrl; + public final String hubUrl; + + // optional metadata (only if YOU launched it) + public final Long pid; + + public NodeInstance(String nodeUrl, String hubUrl, Long pid) { + this.nodeUrl = nodeUrl; + this.hubUrl = hubUrl; + this.pid = pid; + } +} diff --git a/src/main/java/com/nordstrom/automation/selenium/grid/NodeManager.java b/src/main/java/com/nordstrom/automation/selenium/grid/NodeManager.java new file mode 100644 index 00000000..4f796001 --- /dev/null +++ b/src/main/java/com/nordstrom/automation/selenium/grid/NodeManager.java @@ -0,0 +1,47 @@ +package com.nordstrom.automation.selenium.grid; + +import java.util.List; + +import com.nordstrom.automation.selenium.interfaces.NodeRegistry; + +public class NodeManager { + + private final NodeRegistry registry; + + public NodeManager(NodeRegistry registry) { + this.registry = registry; + } + + public NodeInstance startManagedNode(String hubUrl) { + + Process process = launchNodeProcess(hubUrl); + + Long pid = tryGetPid(process); // optional + + NodeInstance node = new NodeInstance( + "http://localhost:auto", // or resolved endpoint + hubUrl, + pid + ); + + registry.register(node); + return node; + } + + public void stopManagedNode(NodeInstance node) { + + if (node.pid != null) { + ProcessHandle.of(node.pid) + .ifPresent(ProcessHandle::destroy); + } + + registry.remove(node); + } + + public List listExternalNodes(String hubUrl) { + + // Query Grid API instead of OS introspection + return queryHubForNodes(hubUrl); + } + +} diff --git a/src/main/java/com/nordstrom/automation/selenium/grid/NodeSpec.java b/src/main/java/com/nordstrom/automation/selenium/grid/NodeSpec.java new file mode 100644 index 00000000..1a016a1e --- /dev/null +++ b/src/main/java/com/nordstrom/automation/selenium/grid/NodeSpec.java @@ -0,0 +1,6 @@ +package com.nordstrom.automation.selenium.grid; + +public class NodeSpec { + public String hubUrl; + public int maxSessions; +} diff --git a/src/main/java/com/nordstrom/automation/selenium/grid/Seleniuim4Backend.java b/src/main/java/com/nordstrom/automation/selenium/grid/Seleniuim4Backend.java new file mode 100644 index 00000000..24bbbc0e --- /dev/null +++ b/src/main/java/com/nordstrom/automation/selenium/grid/Seleniuim4Backend.java @@ -0,0 +1,9 @@ +package com.nordstrom.automation.selenium.grid; + +import com.nordstrom.automation.selenium.interfaces.GridBackend; + +public class Selenium4Backend implements GridBackend { + + // hub includes EventBus ports + // node uses EventBus config +} diff --git a/src/main/java/com/nordstrom/automation/selenium/grid/Selenium3Backend.java b/src/main/java/com/nordstrom/automation/selenium/grid/Selenium3Backend.java new file mode 100644 index 00000000..02996595 --- /dev/null +++ b/src/main/java/com/nordstrom/automation/selenium/grid/Selenium3Backend.java @@ -0,0 +1,78 @@ +package com.nordstrom.automation.selenium.grid; + +import com.nordstrom.automation.selenium.interfaces.BackendCapabilities; +import com.nordstrom.automation.selenium.interfaces.GridBackend; + +public class Selenium3Backend implements GridBackend { + + @Override + public HubInstance startHub(HubSpec spec) { + Process hub = new ProcessBuilder( + "selenium-server-standalone", + "-role", "hub", + "-port", String.valueOf(spec.preferredPort) + ).start(); + + return new HubInstance(spec.preferredPort, null, null, tryPid(hub), "http://localhost:" + spec.preferredPort); + } + + @Override + public NodeInstance startNode(NodeSpec spec) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void requestHubShutdown(HubInstance instance) { + // TODO Auto-generated method stub + + } + + @Override + public void requestNodeShutdown(NodeInstance node) { + // TODO Auto-generated method stub + + } + + @Override + public BackendCapabilities capabilities() { + // TODO Auto-generated method stub + return null; + } + + private static Long tryPid(Process process) { + if (process == null) return null; + try { + long pid = process.pid(); + return (pid > 0) ? pid : null; + } catch (UnsupportedOperationException e) { + return null; + } + } + + private static Long tryPid2(Process process) { + try { + // UNIXProcess (Linux/macOS) + if (process.getClass().getName().equals("java.lang.UNIXProcess")) { + java.lang.reflect.Field f = process.getClass().getDeclaredField("pid"); + f.setAccessible(true); + return (long) f.get(process); + } + + // WindowsProcess (Windows) + if (process.getClass().getName().equals("java.lang.Win32Process") || + process.getClass().getName().equals("java.lang.ProcessImpl")) { + + java.lang.reflect.Field f = process.getClass().getDeclaredField("handle"); + f.setAccessible(true); + long handle = (long) f.get(process); + + // Convert handle → PID via JNA or native call (not trivial) + // Without JNA, you can’t reliably finish this part + } + + } catch (Exception ignored) {} + + return null; + } +} diff --git a/src/main/java/com/nordstrom/automation/selenium/grid/Selenium3Capabilities.java b/src/main/java/com/nordstrom/automation/selenium/grid/Selenium3Capabilities.java new file mode 100644 index 00000000..0f681428 --- /dev/null +++ b/src/main/java/com/nordstrom/automation/selenium/grid/Selenium3Capabilities.java @@ -0,0 +1,18 @@ +package com.nordstrom.automation.selenium.grid; + +import com.nordstrom.automation.selenium.interfaces.BackendCapabilities; + +public class Selenium3Capabilities implements BackendCapabilities { + + public boolean supportsDynamicPorts() { return true; } + + public boolean supportsNodeLifecycleManagement() { return true; } + + public boolean supportsHubLifecycleManagement() { return true; } + + public boolean supportsExternalNodeDiscovery() { return true; } + + public boolean supportsHealthCheck() { return true; } + + public boolean supportsGracefulShutdown() { return false; } +} diff --git a/src/main/java/com/nordstrom/automation/selenium/grid/Selenium4Capabilities.java b/src/main/java/com/nordstrom/automation/selenium/grid/Selenium4Capabilities.java new file mode 100644 index 00000000..11d133c6 --- /dev/null +++ b/src/main/java/com/nordstrom/automation/selenium/grid/Selenium4Capabilities.java @@ -0,0 +1,18 @@ +package com.nordstrom.automation.selenium.grid; + +import com.nordstrom.automation.selenium.interfaces.BackendCapabilities; + +public class Selenium4Capabilities implements BackendCapabilities { + + public boolean supportsDynamicPorts() { return true; } + + public boolean supportsNodeLifecycleManagement() { return true; } + + public boolean supportsHubLifecycleManagement() { return true; } + + public boolean supportsExternalNodeDiscovery() { return true; } + + public boolean supportsHealthCheck() { return true; } + + public boolean supportsGracefulShutdown() { return true; } +} diff --git a/src/main/java/com/nordstrom/automation/selenium/interfaces/BackendCapabilities.java b/src/main/java/com/nordstrom/automation/selenium/interfaces/BackendCapabilities.java new file mode 100644 index 00000000..9bfe8cb0 --- /dev/null +++ b/src/main/java/com/nordstrom/automation/selenium/interfaces/BackendCapabilities.java @@ -0,0 +1,16 @@ +package com.nordstrom.automation.selenium.interfaces; + +public interface BackendCapabilities { + + boolean supportsDynamicPorts(); + + boolean supportsNodeLifecycleManagement(); + + boolean supportsHubLifecycleManagement(); + + boolean supportsExternalNodeDiscovery(); + + boolean supportsHealthCheck(); + + boolean supportsGracefulShutdown(); +} diff --git a/src/main/java/com/nordstrom/automation/selenium/interfaces/GridBackend.java b/src/main/java/com/nordstrom/automation/selenium/interfaces/GridBackend.java new file mode 100644 index 00000000..4371e840 --- /dev/null +++ b/src/main/java/com/nordstrom/automation/selenium/interfaces/GridBackend.java @@ -0,0 +1,19 @@ +package com.nordstrom.automation.selenium.interfaces; + +import com.nordstrom.automation.selenium.grid.HubInstance; +import com.nordstrom.automation.selenium.grid.HubSpec; +import com.nordstrom.automation.selenium.grid.NodeInstance; +import com.nordstrom.automation.selenium.grid.NodeSpec; + +public interface GridBackend { + + HubInstance startHub(HubSpec spec); + + NodeInstance startNode(NodeSpec spec); + + void requestHubShutdown(HubInstance instance); + + void requestNodeShutdown(NodeInstance node); + + BackendCapabilities capabilities(); +} diff --git a/src/main/java/com/nordstrom/automation/selenium/interfaces/GridInstanceRegistry.java b/src/main/java/com/nordstrom/automation/selenium/interfaces/GridInstanceRegistry.java new file mode 100644 index 00000000..0e692e6e --- /dev/null +++ b/src/main/java/com/nordstrom/automation/selenium/interfaces/GridInstanceRegistry.java @@ -0,0 +1,26 @@ +package com.nordstrom.automation.selenium.interfaces; + +import java.util.List; +import java.util.Optional; + +import com.nordstrom.automation.selenium.grid.HubInstance; +import com.nordstrom.automation.selenium.grid.NodeInstance; + +public interface GridInstanceRegistry { + + void registerHub(HubInstance hub); + + void removeHub(HubInstance hub); + + Optional findHubByPort(int port); + + List listHubs(); + + void registerNode(NodeInstance node); + + void removeNode(NodeInstance node); + + List listNodes(); + + List findNodesByHub(String hubUrl); +} diff --git a/src/main/java/com/nordstrom/automation/selenium/interfaces/GridRegistry.java b/src/main/java/com/nordstrom/automation/selenium/interfaces/GridRegistry.java new file mode 100644 index 00000000..ef4653f7 --- /dev/null +++ b/src/main/java/com/nordstrom/automation/selenium/interfaces/GridRegistry.java @@ -0,0 +1,18 @@ +package com.nordstrom.automation.selenium.interfaces; + +import java.util.List; + +import com.nordstrom.automation.selenium.grid.HubInstance; +import com.nordstrom.automation.selenium.grid.NodeInstance; + +public interface GridRegistry { + + void saveHub(HubInstance hub); + void removeHub(HubInstance hub); + + void saveNode(NodeInstance node); + void removeNode(NodeInstance node); + + List hubs(); + List nodes(); +} diff --git a/src/main/java/com/nordstrom/automation/selenium/interfaces/NodeRegistry.java b/src/main/java/com/nordstrom/automation/selenium/interfaces/NodeRegistry.java new file mode 100644 index 00000000..a8765428 --- /dev/null +++ b/src/main/java/com/nordstrom/automation/selenium/interfaces/NodeRegistry.java @@ -0,0 +1,12 @@ +package com.nordstrom.automation.selenium.interfaces; + +import java.util.List; + +import com.nordstrom.automation.selenium.grid.NodeInstance; + +public interface NodeRegistry { + void register(NodeInstance node); + void remove(NodeInstance node); + List list(); + List findByHub(String hubUrl); +} diff --git a/src/main/java/com/nordstrom/automation/selenium/plugins/RemoteWebDriverPlugin.java b/src/main/java/com/nordstrom/automation/selenium/plugins/RemoteWebDriverPlugin.java index 65c0f2b6..7f6eb923 100644 --- a/src/main/java/com/nordstrom/automation/selenium/plugins/RemoteWebDriverPlugin.java +++ b/src/main/java/com/nordstrom/automation/selenium/plugins/RemoteWebDriverPlugin.java @@ -49,14 +49,14 @@ protected RemoteWebDriverPlugin(String browserName) { */ @Override public LocalGridServer create(SeleniumConfig config, String launcherClassName, String[] dependencyContexts, - URL hubUrl, final Path workingPath, final Path outputPath) throws IOException { + URL hubUrl, Path workingPath, Path outputPath) throws IOException { String[] combinedContexts = combineDependencyContexts(dependencyContexts, this); String capabilities = getCapabilities(config); Path nodeConfigPath = config.createNodeConfig(capabilities, hubUrl); String[] propertyNames = getPropertyNames(capabilities); - return LocalSeleniumGrid.create(config, launcherClassName, combinedContexts, - false, -1, nodeConfigPath, workingPath, outputPath, propertyNames); + return LocalSeleniumGrid.createNode(config, launcherClassName, combinedContexts, + -1, nodeConfigPath, workingPath, outputPath, propertyNames); } /** diff --git a/src/main/java/com/nordstrom/automation/selenium/support/TestNgPlatformBase.java b/src/main/java/com/nordstrom/automation/selenium/support/TestNgPlatformBase.java index 05939603..fb3bee95 100644 --- a/src/main/java/com/nordstrom/automation/selenium/support/TestNgPlatformBase.java +++ b/src/main/java/com/nordstrom/automation/selenium/support/TestNgPlatformBase.java @@ -58,7 +58,7 @@ public String[] getSubPath() { * {@inheritDoc} */ @Override - @SuppressWarnings({ "unchecked", "serial" }) + @SuppressWarnings("unchecked") public P getTargetPlatform() { P platform = null; ITestResult testResult = Reporter.getCurrentTestResult(); diff --git a/src/selenium3/java/com/nordstrom/automation/selenium/SeleniumConfig.java b/src/selenium3/java/com/nordstrom/automation/selenium/SeleniumConfig.java index 8bcc7fa2..70704538 100644 --- a/src/selenium3/java/com/nordstrom/automation/selenium/SeleniumConfig.java +++ b/src/selenium3/java/com/nordstrom/automation/selenium/SeleniumConfig.java @@ -230,6 +230,16 @@ protected Map getDefaults() { return defaults; } + @Override + public String getPublishUrl() { + return null; + } + + @Override + public String getSubscribeUrl() { + return null; + } + /** * {@inheritDoc} */ diff --git a/src/selenium3/java/com/nordstrom/automation/selenium/core/LocalSeleniumGrid.java b/src/selenium3/java/com/nordstrom/automation/selenium/core/LocalSeleniumGrid.java index bf943e59..a5b24c63 100644 --- a/src/selenium3/java/com/nordstrom/automation/selenium/core/LocalSeleniumGrid.java +++ b/src/selenium3/java/com/nordstrom/automation/selenium/core/LocalSeleniumGrid.java @@ -203,6 +203,35 @@ public static SeleniumGrid create(SeleniumConfig config, final URL hubUrl) throw } } + /** + * Create an object that represents a Selenium Grid server with the specified arguments. + *

+ * NOTE: The created object defines a separate process for managing the local server, but does NOT + * start this process. + * + * @param config {@link SeleniumConfig} object + * @param launcherClassName fully-qualified name of {@code GridLauncher} class + * @param dependencyContexts fully-qualified names of context classes for Selenium Grid dependencies + * @param port port that Grid server should use; -1 to specify auto-configuration + * @param configPath {@link Path} to server configuration file + * @param workingPath {@link Path} of working directory for server process; {@code null} for default + * @param outputPath {@link Path} to output log file; {@code null} to decline log-to-file + * @param propertyNames optional array of property names to propagate to server process + * @return {@link LocalGridServer} object for managing the server process + * @throws GridServerLaunchFailedException if a Grid component process failed to start + * @see #activate() + * @see LocalGridServer#start() + * @see + * Getting Command-Line Help + */ + public static LocalGridServer createNode(final SeleniumConfig config, final String launcherClassName, + final String[] dependencyContexts, final Integer port, final Path configPath, + final Path workingPath, final Path outputPath, final String... propertyNames) { + + return create(config, launcherClassName, dependencyContexts, false, + port, configPath, workingPath, outputPath, propertyNames); + } + /** * Create an object that represents a Selenium Grid server with the specified arguments. *

diff --git a/src/selenium3/java/com/nordstrom/automation/selenium/core/ServerProcessKiller.java b/src/selenium3/java/com/nordstrom/automation/selenium/core/ServerProcessKiller.java index 6946abf4..66621435 100644 --- a/src/selenium3/java/com/nordstrom/automation/selenium/core/ServerProcessKiller.java +++ b/src/selenium3/java/com/nordstrom/automation/selenium/core/ServerProcessKiller.java @@ -65,7 +65,7 @@ public static boolean killServerProcess(CommandLine cmdLine, URL serverUrl) thro } } - String pid = ServerPidFinder.getPidOfServerAt(serverUrl.getPort()); + String pid = ServerPidFinder.getPidOfServerAt(serverUrl.getPort(), true); if (pid != null) { LOGGER.debug("Local server with process ID '{}' listening to: {}", pid, serverUrl); try { diff --git a/src/selenium4/java/com/nordstrom/automation/selenium/SeleniumConfig.java b/src/selenium4/java/com/nordstrom/automation/selenium/SeleniumConfig.java index e9338a87..efd0d164 100644 --- a/src/selenium4/java/com/nordstrom/automation/selenium/SeleniumConfig.java +++ b/src/selenium4/java/com/nordstrom/automation/selenium/SeleniumConfig.java @@ -29,7 +29,6 @@ import org.openqa.selenium.MutableCapabilities; import org.openqa.selenium.grid.config.ConfigException; import org.openqa.selenium.json.Json; -import org.openqa.selenium.net.PortProber; import com.nordstrom.automation.selenium.core.GridServer; import com.nordstrom.automation.selenium.core.GridUtility; @@ -44,9 +43,10 @@ public class SeleniumConfig extends AbstractSeleniumConfig { private static final String DEFAULT_GRID_LAUNCHER = "org.openqa.selenium.grid.Bootstrap"; - private static final String DEFAULT_HUB_PORT = "4446"; + private static final String DEFAULT_HUB_PORT = "4444"; private static final String DEFAULT_HUB_CONFIG = "hubConfig-s4.json"; private static final String DEFAULT_NODE_CONFIG = "nodeConfig-s4.json"; + private static final String EVENT_HOST = "tcp://*:"; /** * org.openqa.selenium.grid.Main @@ -391,6 +391,18 @@ protected Map getDefaults() { return defaults; } + @Override + public String getPublishUrl() { + int port = getAvailablePort(SeleniumSettings.PUBLISH_PORT); + return (port != -1) ? EVENT_HOST + port : null; + } + + @Override + public String getSubscribeUrl() { + int port = getAvailablePort(SeleniumSettings.SUBSCRIBE_PORT); + return (port != -1) ? EVENT_HOST + port : null; + } + /** * {@inheritDoc} */ @@ -450,7 +462,7 @@ public Path createNodeConfig(String capabilities, URL hubUrl) throws IOException // create relay configuration template if absent Map relayOptions = (Map) nodeConfig.computeIfAbsent("relay", k -> new HashMap<>()); relayOptions.computeIfAbsent("host", k -> HostUtils.getLocalHost()); - relayOptions.computeIfAbsent("port", k -> PortProber.findFreePort()); + relayOptions.computeIfAbsent("port", k -> SeleniumConfig.getConfig().getAppiumServerPort()); relayOptions.computeIfAbsent("configs", k -> new ArrayList<>()); // otherwise (not Appium) } else { diff --git a/src/selenium4/java/com/nordstrom/automation/selenium/core/LocalSeleniumGrid.java b/src/selenium4/java/com/nordstrom/automation/selenium/core/LocalSeleniumGrid.java index 349bc3e1..c5f3f20f 100644 --- a/src/selenium4/java/com/nordstrom/automation/selenium/core/LocalSeleniumGrid.java +++ b/src/selenium4/java/com/nordstrom/automation/selenium/core/LocalSeleniumGrid.java @@ -42,6 +42,8 @@ public class LocalSeleniumGrid extends SeleniumGrid { private static final String OPT_HOST = "--host"; private static final String OPT_PORT = "--port"; private static final String OPT_CONFIG = "--config"; + private static final String OPT_PUB_EVENTS = "--publish-events"; + private static final String OPT_SUB_EVENTS = "--subscribe-events"; /** * Constructor for models of local Selenium Grid instances from hub URL. @@ -139,6 +141,8 @@ public static SeleniumGrid create(SeleniumConfig config, final URL hubUrl) throw Objects.requireNonNull(config, "[config] must be non-null"); String launcherClassName = config.getString(SeleniumSettings.GRID_LAUNCHER.key()); + String publishUrl = config.getPublishUrl(); + String subscribeUrl = config.getSubscribeUrl(); String[] dependencyContexts = config.getDependencyContexts(); String workingDir = config.getString(SeleniumSettings.GRID_WORKING_DIR.key()); Path workingPath = (workingDir == null || workingDir.isEmpty()) ? null : Paths.get(workingDir); @@ -157,8 +161,8 @@ public static SeleniumGrid create(SeleniumConfig config, final URL hubUrl) throw Integer hubPort = (hubUrl != null) ? hubUrl.getPort() : config.getInteger(SeleniumSettings.HUB_PORT.key(), -1); Path outputPath = GridUtility.getOutputPath(config, true); - hubServer = create(config, launcherClassName, dependencyContexts, - true, hubPort, hubConfigPath, workingPath, outputPath); + hubServer = create(config, launcherClassName, dependencyContexts, true, hubPort, + publishUrl, subscribeUrl, hubConfigPath, workingPath, outputPath); } // store hub host and hub port in system properties for subsequent retrieval @@ -178,10 +182,10 @@ public static SeleniumGrid create(SeleniumConfig config, final URL hubUrl) throw nodeServers.add(nodeServer); // if this is an Appium Grid server if (nodeServer instanceof AppiumGridServer) { - // get path to relay configuration path from Appium process environment + // get path to relay configuration from Appium process environment Path nodeConfigPath = ((AppiumGridServer) nodeServer).getNodeConfigPath(); // add relay node for Appium Grid server to nodes list - nodeServers.add(create(config, launcherClassName, dependencyContexts, false, -1, nodeConfigPath, + nodeServers.add(createNode(config, launcherClassName, dependencyContexts, -1, nodeConfigPath, workingPath, GridUtility.getOutputPath(config, null))); LOGGER.debug("Adding local Grid relay for Appium server providing personalities: {}", nodeServer.getPersonalities().keySet()); @@ -218,6 +222,35 @@ public static SeleniumGrid create(SeleniumConfig config, final URL hubUrl) throw } } + /** + * Create an object that represents a Selenium Grid server with the specified arguments. + *

+ * NOTE: The created object defines a separate process for managing the local server, but does NOT + * start this process. + * + * @param config {@link SeleniumConfig} object + * @param launcherClassName fully-qualified name of {@code GridLauncher} class + * @param dependencyContexts fully-qualified names of context classes for Selenium Grid dependencies + * @param port port that Grid server should use; -1 to specify auto-configuration + * @param configPath {@link Path} to server configuration file + * @param workingPath {@link Path} of working directory for server process; {@code null} for default + * @param outputPath {@link Path} to output log file; {@code null} to decline log-to-file + * @param propertyNames optional array of property names to propagate to server process + * @return {@link LocalGridServer} object for managing the server process + * @throws GridServerLaunchFailedException if a Grid component process failed to start + * @see #activate() + * @see LocalGridServer#start() + * @see + * Getting Command-Line Help + */ + public static LocalGridServer createNode(final SeleniumConfig config, final String launcherClassName, + final String[] dependencyContexts, final Integer port, final Path configPath, + final Path workingPath, final Path outputPath, final String... propertyNames) { + + return create(config, launcherClassName, dependencyContexts, false, port, + null, null, configPath, workingPath, outputPath, propertyNames); + } + /** * Create an object that represents a Selenium Grid server with the specified arguments. *

@@ -229,19 +262,22 @@ public static SeleniumGrid create(SeleniumConfig config, final URL hubUrl) throw * @param dependencyContexts fully-qualified names of context classes for Selenium Grid dependencies * @param isHub role of Grid server being started ({@code true} = hub; {@code false} = node) * @param port port that Grid server should use; -1 to specify auto-configuration + * @param publishUrl URL for publishing Grid events + * @param subscribeUrl URL for subscribing to Grid events * @param configPath {@link Path} to server configuration file * @param workingPath {@link Path} of working directory for server process; {@code null} for default * @param outputPath {@link Path} to output log file; {@code null} to decline log-to-file * @param propertyNames optional array of property names to propagate to server process * @return {@link LocalGridServer} object for managing the server process - * @throws GridServerLaunchFailedException If a Grid component process failed to start + * @throws GridServerLaunchFailedException if a Grid component process failed to start * @see #activate() * @see LocalGridServer#start() * @see * Getting Command-Line Help */ public static LocalGridServer create(final SeleniumConfig config, final String launcherClassName, - final String[] dependencyContexts, final boolean isHub, final Integer port, final Path configPath, + final String[] dependencyContexts, final boolean isHub, final Integer port, + final String publishUrl, final String subscribeUrl, final Path configPath, final Path workingPath, final Path outputPath, final String... propertyNames) { List argsList = new ArrayList<>(); @@ -272,6 +308,16 @@ public static LocalGridServer create(final SeleniumConfig config, final String l argsList.add(OPT_PORT); argsList.add(portNum.toString()); + if (publishUrl != null) { + argsList.add(OPT_PUB_EVENTS); + argsList.add(publishUrl); + } + + if (subscribeUrl != null) { + argsList.add(OPT_SUB_EVENTS); + argsList.add(subscribeUrl); + } + // specify server configuration file argsList.add(OPT_CONFIG); argsList.add(configPath.toString()); diff --git a/src/selenium4/java/com/nordstrom/automation/selenium/core/ServerProcessKiller.java b/src/selenium4/java/com/nordstrom/automation/selenium/core/ServerProcessKiller.java index 600016f0..8e62ba8f 100644 --- a/src/selenium4/java/com/nordstrom/automation/selenium/core/ServerProcessKiller.java +++ b/src/selenium4/java/com/nordstrom/automation/selenium/core/ServerProcessKiller.java @@ -47,7 +47,7 @@ public static boolean killServerProcess(Process process, URL serverUrl) throws I } // get ID of listening process - String pid = ServerPidFinder.getPidOfServerAt(serverUrl.getPort()); + String pid = ServerPidFinder.getPidOfServerAt(serverUrl.getPort(), true); if (pid != null) { // get server process handle ('null' on failure) ProcessHandle handle = ProcessHandle.of(Long.parseLong(pid)).orElse(null); diff --git a/src/selenium4/java/com/nordstrom/automation/selenium/plugins/AbstractAppiumPlugin.java b/src/selenium4/java/com/nordstrom/automation/selenium/plugins/AbstractAppiumPlugin.java index f1dcc6d1..26f64d96 100644 --- a/src/selenium4/java/com/nordstrom/automation/selenium/plugins/AbstractAppiumPlugin.java +++ b/src/selenium4/java/com/nordstrom/automation/selenium/plugins/AbstractAppiumPlugin.java @@ -146,7 +146,7 @@ public LocalGridServer create(SeleniumConfig config, String launcherClassName, S Path appiumConfigPath = config.getAppiumConfigPath(); // if file path is specified if (appiumConfigPath != null) { - argsList.add("--config-file"); + argsList.add("--config"); argsList.add(appiumConfigPath.toString()); } diff --git a/src/selenium4/java/com/nordstrom/automation/selenium/utility/GridHubPortAllocator.java b/src/selenium4/java/com/nordstrom/automation/selenium/utility/GridHubPortAllocator.java new file mode 100644 index 00000000..3b133073 --- /dev/null +++ b/src/selenium4/java/com/nordstrom/automation/selenium/utility/GridHubPortAllocator.java @@ -0,0 +1,123 @@ +package com.nordstrom.automation.selenium.utility; + +import java.io.IOException; +import java.net.ServerSocket; + +import com.nordstrom.automation.selenium.core.ServerPidFinder; + +public class GridHubPortAllocator { + + public static class GridPorts { + public final int hubPort; + public final int eventBusPubPort; + public final int eventBusSubPort; + + public GridPorts(int hubPort, int pub, int sub) { + this.hubPort = hubPort; + this.eventBusPubPort = pub; + this.eventBusSubPort = sub; + } + + @Override + public String toString() { + return "hub=" + hubPort + + ", pub=" + eventBusPubPort + + ", sub=" + eventBusSubPort; + } + } + + /** + * Allocates a full Selenium Grid hub port set atomically. + */ + public static GridPorts allocate(int startHubPort) { + int hub = startHubPort; + + while (hub < 65530) { + + int pub = hub - 2; + int sub = hub - 1; + + if (pub < 1024) { + hub += 10; + continue; + } + + if (isFree(hub, pub, sub)) { + Reservation r = reserve(hub, pub, sub); + if (r.success) { + r.close(); // release locks, ports remain reserved by OS rules + return new GridPorts(hub, pub, sub); + } + } + + hub += 10; // spacing reduces collision probability + } + + throw new RuntimeException("No free Grid port set found"); + } + + // ------------------------- + // atomic reservation + // ------------------------- + + private static Reservation reserve(int hub, int pub, int sub) { + try { + ServerSocket h = new ServerSocket(hub); + ServerSocket p = new ServerSocket(pub); + ServerSocket s = new ServerSocket(sub); + return new Reservation(true, h, p, s); + } catch (IOException e) { + return new Reservation(false, null, null, null); + } + } + + public static GridPorts assigned(int hubPort) { + int pubPort = hubPort - 2; + int subPort = hubPort - 1; + + String hubPid = ServerPidFinder.getPidOfServerAt(hubPort, true); + String pubPid = ServerPidFinder.getPidOfServerAt(pubPort, false); + String subPid = ServerPidFinder.getPidOfServerAt(subPort, true); + + + } + + public static boolean isFree(int hub, int pub, int sub) { + return (isFree(hub) && isFree(pub) && isFree(sub)); + } + + public static boolean isFree(int port) { + try (ServerSocket socket = new ServerSocket(port)) { + socket.setReuseAddress(true); + return true; + } catch (IOException e) { + return false; + } + } + + private static class Reservation { + final boolean success; + final ServerSocket hub; + final ServerSocket pub; + final ServerSocket sub; + + Reservation(boolean success, ServerSocket hub, ServerSocket pub, ServerSocket sub) { + this.success = success; + this.hub = hub; + this.pub = pub; + this.sub = sub; + } + + void close() { + closeQuiet(hub); + closeQuiet(pub); + closeQuiet(sub); + } + + private void closeQuiet(ServerSocket s) { + if (s != null) { + try { s.close(); } catch (IOException ignored) {} + } + } + } +}