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()})
*
* 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
+ * 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
+ * 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
* 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