...57 private static final AtomicInteger THREAD_COUNTER = new AtomicInteger(1);58 private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool(59 new ThreadFactory() {60 public Thread newThread(Runnable r) {61 Thread t = new Thread(r, "UrlChecker-" + THREAD_COUNTER.incrementAndGet()); // Thread safety reviewed62 t.setDaemon(true);63 return t;64 }65 });66 private GridServer hubServer;67 private Map<String, GridServer> nodeServers = new HashMap<>();68 protected Map<String, String> personalities = new HashMap<>();69 70 protected static final Logger LOGGER = LoggerFactory.getLogger(SeleniumGrid.class);71 72 /**73 * Constructor for Selenium Grid from hub URL.74 * <p>75 * This is used to create an interface for an active grid - remote or local.76 * 77 * @param config {@link SeleniumConfig} object78 * @param hubUrl {@link URL} for grid hub host79 * @throws IOException if unable to acquire Grid details80 */81 public SeleniumGrid(SeleniumConfig config, URL hubUrl) throws IOException {82 LOGGER.debug("Mapping structure of grid at: {}", hubUrl);83 hubServer = new GridServer(hubUrl, GridRole.HUB);84 for (String nodeEndpoint : GridUtility.getGridProxies(hubUrl)) {85 URL nodeUrl = new URL(nodeEndpoint + GridServer.HUB_BASE);86 nodeServers.put(nodeEndpoint, new GridServer(nodeUrl, GridRole.NODE));87 addNodePersonalities(config, hubServer.getUrl(), nodeEndpoint);88 }89 addPluginPersonalities(config);90 LOGGER.debug("{}: Personalities => {}", hubServer.getUrl(), personalities.keySet());91 }92 93 /**94 * Constructor for Selenium Grid from server objects.95 * <p>96 * This is used to create an interface for a newly-created local Grid.97 * 98 * @param config {@link SeleniumConfig} object99 * @param hubServer {@link GridServer} object for hub host100 * @param nodeServers array of {@link GridServer} objects for node hosts101 * @throws IOException if unable to acquire Grid details102 */103 public SeleniumGrid(SeleniumConfig config, GridServer hubServer, GridServer... nodeServers) throws IOException {104 this.hubServer = Objects.requireNonNull(hubServer);105 if (Objects.requireNonNull(nodeServers).length == 0) {106 throw new IllegalArgumentException("[nodeServers] must be non-empty");107 }108 LOGGER.debug("Assembling graph of grid at: {}", hubServer.getUrl());109 for (GridServer nodeServer : nodeServers) {110 String nodeEndpoint = "http://" + nodeServer.getUrl().getAuthority();111 this.nodeServers.put(nodeEndpoint, nodeServer);112 addNodePersonalities(config, hubServer.getUrl(), nodeEndpoint);113 }114 addPluginPersonalities(config);115 LOGGER.debug("{}: Personalities => {}", hubServer.getUrl(), personalities.keySet());116 }117 118 /**119 * Add supported personalities of the specified Grid node.120 * <p>121 * <b>NOTE</b>: Names of node personalities are derived from the following capabilities122 * (in order of precedence):123 * 124 * <ul>125 * <li><b>automationName</b>: 'appium' automation name</li>126 * <li><b>browserName</b>: name of target browser</li>127 * </ul>128 * 129 * @param config {@link SeleniumConfig} object130 * @param hubUrl {@link URL} of Grid hub131 * @param nodeEndpoint node endpoint132 * @throws IOException if an I/O error occurs133 */134 @SuppressWarnings({"unchecked", "rawtypes"})135 private void addNodePersonalities(SeleniumConfig config, URL hubUrl, String nodeEndpoint) throws IOException {136 LOGGER.debug("{}: Adding personalities of node: {}", hubUrl, nodeEndpoint);137 for (Capabilities capabilities : GridUtility.getNodeCapabilities(config, hubUrl, nodeEndpoint)) {138 Map<String, Object> req = (Map<String, Object>) capabilities.getCapability("request");139 List<Map> capsList = (List<Map>) req.get("capabilities");140 if (capsList == null) {141 Map<String, Object> conf = (Map<String, Object>) req.get("configuration");142 capsList = (List<Map>) conf.get("capabilities");143 }144 for (Map<String, Object> capsItem : capsList) {145 String browserName = (String) capsItem.get("automationName");146 if (browserName == null) {147 browserName = (String) capsItem.get("browserName");148 }149 personalities.putAll(PluginUtils.getPersonalitiesForBrowser(browserName));150 }151 }152 }153 154 /**155 * Add supported personalities from configured driver plug-ins.156 * 157 * @param config {@link SeleniumConfig} object158 */159 private void addPluginPersonalities(SeleniumConfig config) {160 for (DriverPlugin driverPlugin : LocalSeleniumGrid.getDriverPlugins(config)) {161 if (personalities.containsKey(driverPlugin.getBrowserName())) {162 personalities.putAll(driverPlugin.getPersonalities());163 }164 }165 }166 167 /**168 * Create an object that represents the Selenium Grid with the specified hub endpoint.169 * <p>170 * If the endpoint is {@code null} or specifies an inactive {@code localhost} URL, this method launches a local171 * Grid instance and returns a {@link LocalSeleniumGrid} object.172 * 173 * @param config {@link SeleniumConfig} object174 * @param hubUrl {@link URL} of hub host175 * @return {@link SeleniumGrid} object for the specified hub endpoint176 * @throws IOException if an I/O error occurs177 * @throws InterruptedException if this thread was interrupted178 * @throws TimeoutException if host timeout interval exceeded179 */180 public static SeleniumGrid create(SeleniumConfig config, URL hubUrl) throws IOException, InterruptedException, TimeoutException {181 if ((hubUrl != null) && GridUtility.isHubActive(hubUrl)) {182 // ensure that hub port is available as a discrete setting183 System.setProperty(SeleniumSettings.HUB_PORT.key(), Integer.toString(hubUrl.getPort()));184 return new SeleniumGrid(config, hubUrl);185 } else if ((hubUrl == null) || GridUtility.isLocalHost(hubUrl)) {186 return LocalSeleniumGrid.launch(config, config.createHubConfig());187 }188 throw new IllegalStateException("Specified remote hub URL '" + hubUrl + "' isn't active");189 }190 191 /**192 * Shutdown the Selenium Grid represented by this object.193 * 194 * @param localOnly {@code true} to target only local Grid servers195 * @return {@code false} if non-local Grid server encountered; otherwise {@code true}196 * @throws InterruptedException if this thread was interrupted197 */198 public boolean shutdown(final boolean localOnly) throws InterruptedException {199 boolean result = true;200 Iterator<Entry<String, GridServer>> iterator = nodeServers.entrySet().iterator();201 202 while (iterator.hasNext()) {203 Entry<String, GridServer> serverEntry = iterator.next();204 if (serverEntry.getValue().shutdown(localOnly)) {205 iterator.remove();206 } else {207 result = false;208 }209 }210 211 if (hubServer.shutdown(localOnly)) {212 hubServer = null;213 } else {214 result = false;215 }216 217 return result;218 }219 220 /**221 * Get grid server object for the active hub.222 * 223 * @return {@link GridServer} object that represents the active hub server224 */225 public GridServer getHubServer() {226 return hubServer;227 }228 229 /**230 * Get the map of grid server objects for the attached nodes.231 * 232 * @return map of {@link GridServer} objects that represent the attached node servers233 */234 public Map<String, GridServer> getNodeServers() {235 return nodeServers;236 }237 238 /**239 * Get capabilities object for the specified browser personality.240 * 241 * @param config {@link SeleniumConfig} object242 * @param personality browser personality to retrieve243 * @return {@link Capabilities} object for the specified personality244 * @throws IllegalArgumentException if specified personality isn't supported by the active Grid245 */246 public Capabilities getPersonality(SeleniumConfig config, String personality) {247 String json = personalities.get(personality);248 if ((json == null) || json.isEmpty()) {249 String message = String.format("Specified personality '%s' not supported by active Grid", personality);250 String browserName = personality.split("\\.")[0];251 if ( ! browserName.equals(personality)) {252 LOGGER.warn("{}; revert to browser name '{}'", message, browserName);253 Capabilities[] capsList = config.getCapabilitiesForName(browserName);254 if (capsList.length > 0) {255 return capsList[0];256 }257 }258 throw new IllegalArgumentException(message);259 } else {260 return config.getCapabilitiesForJson(json)[0];261 }262 }263 /**264 * This class represents a single Selenium Grid server (hub or node).265 */266 public static class GridServer {267 private GridRole role;268 private URL serverUrl;269 protected String statusRequest;270 protected String shutdownRequest;271 272 public static final String GRID_CONSOLE = "/grid/console";273 public static final String HUB_BASE = "/wd/hub";274 public static final String NODE_STATUS = "/wd/hub/status";275 public static final String HUB_CONFIG = "/grid/api/hub/";276 public static final String NODE_CONFIG = "/grid/api/proxy";277 278 private static final String HUB_SHUTDOWN = "/lifecycle-manager?action=shutdown";279 private static final String NODE_SHUTDOWN = "/extra/LifecycleServlet?action=shutdown";280 private static final long SHUTDOWN_DELAY = 15;281 282 public GridServer(URL url, GridRole role) {283 this.role = role;284 this.serverUrl = url;285 if (isHub()) {286 statusRequest = HUB_CONFIG;287 shutdownRequest = HUB_SHUTDOWN;288 } else {289 statusRequest = NODE_STATUS;290 shutdownRequest = NODE_SHUTDOWN;291 }292 }293 294 /**295 * Determine if this Grid server is a hub host.296 * 297 * @return {@code true} if this server is a hub; otherwise {@code false}298 */299 public boolean isHub() {300 return (role == GridRole.HUB);301 }302 303 /**304 * Get the URL for this server.305 * 306 * @return {@link URL} object for this server307 */308 public URL getUrl() {309 return serverUrl;310 }311 312 public boolean isActive() {313 return GridUtility.isHostActive(serverUrl, statusRequest);314 }315 316 /**317 * Stop the Selenium Grid server represented by this object.318 * 319 * @param localOnly {@code true} to target only local Grid server320 * @return {@code false} if [localOnly] and server is remote; otherwise {@code true}321 * @throws InterruptedException if this thread was interrupted322 */323 public boolean shutdown(final boolean localOnly) throws InterruptedException {324 return shutdown(this, shutdownRequest, localOnly);325 }326 /**327 * Stop the specified Selenium Grid server.328 * 329 * @param gridServer {@link GridServer} object for hub or node330 * @param shutdownRequest Selenium server shutdown request331 * @param localOnly {@code true} to target only local Grid server332 * @return {@code false} if [localOnly] and server is remote; otherwise {@code true}333 * @throws InterruptedException if this thread was interrupted334 */335 public static boolean shutdown(final GridServer gridServer, final String shutdownRequest,336 final boolean localOnly) throws InterruptedException {337 338 URL serverUrl = gridServer.getUrl();339 340 if (localOnly && !GridUtility.isLocalHost(serverUrl)) {341 return false;342 }343 344 if (gridServer.isActive()) {345 if ( ! gridServer.isHub() && (gridServer instanceof LocalGridServer)) {346 ((LocalGridServer) gridServer).getProcess().destroy();347 } else {348 try {349 GridUtility.getHttpResponse(serverUrl, shutdownRequest);350 waitUntilUnavailable(SHUTDOWN_DELAY, TimeUnit.SECONDS, serverUrl);351 } catch (IOException | org.openqa.selenium.net.UrlChecker.TimeoutException e) {352 throw UncheckedThrow.throwUnchecked(e);353 }354 }355 }356 357 Thread.sleep(1000);358 return true;359 }360 }361 /**362 * Wait up to the specified interval for the indicated URL(s) to be available.363 * <p>364 * <b>NOTE</b>: This method was back-ported from the {@link org.openqa.selenium.net.UrlChecker UrlChecker} class in365 * Selenium 3 to compile under Java 7.366 * 367 * @param timeout timeout interval368 * @param unit granularity of specified timeout369 * @param urls URLs to poll for availability370 * @throws org.openqa.selenium.net.UrlChecker.TimeoutException if indicated URL is still available after specified371 * interval.372 */373 public static void waitUntilAvailable(long timeout, TimeUnit unit, final URL... urls)374 throws org.openqa.selenium.net.UrlChecker.TimeoutException {375 long start = System.nanoTime();376 try {377 Future<Void> callback = EXECUTOR.submit(new Callable<Void>() {378 public Void call() throws InterruptedException {379 HttpURLConnection connection = null;380 long sleepMillis = MIN_POLL_INTERVAL_MS;381 while (true) {382 if (Thread.interrupted()) {383 throw new InterruptedException();384 }385 for (URL url : urls) {386 try {387 connection = connectToUrl(url);388 if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {389 return null;390 }391 } catch (IOException e) {392 // Ok, try again.393 } finally {394 if (connection != null) {395 connection.disconnect();396 }397 }398 }399 MILLISECONDS.sleep(sleepMillis);400 sleepMillis = (sleepMillis >= MAX_POLL_INTERVAL_MS) ? sleepMillis : sleepMillis * 2;401 }402 }403 });404 callback.get(timeout, unit);405 } catch (java.util.concurrent.TimeoutException e) {406 throw new org.openqa.selenium.net.UrlChecker.TimeoutException(407 String.format("Timed out waiting for %s to be available after %d ms", Arrays.toString(urls),408 MILLISECONDS.convert(System.nanoTime() - start, NANOSECONDS)),409 e);410 } catch (InterruptedException e) {411 Thread.currentThread().interrupt();412 throw new RuntimeException(e);413 } catch (ExecutionException e) {414 throw new RuntimeException(e);415 }416 }417 /**418 * Wait up to the specified interval for the indicated URL to be unavailable.419 * <p>420 * <b>NOTE</b>: This method was back-ported from the {@link org.openqa.selenium.net.UrlChecker UrlChecker} class in421 * Selenium 3 to compile under Java 7.422 * 423 * @param timeout timeout interval424 * @param unit granularity of specified timeout425 * @param url URL to poll for availability426 * @throws org.openqa.selenium.net.UrlChecker.TimeoutException if indicated URL is still available after specified427 * interval.428 */429 public static void waitUntilUnavailable(long timeout, TimeUnit unit, final URL url)430 throws org.openqa.selenium.net.UrlChecker.TimeoutException {431 long start = System.nanoTime();432 try {433 Future<Void> callback = EXECUTOR.submit(new Callable<Void>() {434 public Void call() throws InterruptedException {435 HttpURLConnection connection = null;436 long sleepMillis = MIN_POLL_INTERVAL_MS;437 while (true) {438 try {439 connection = connectToUrl(url);440 if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {441 return null;442 }443 } catch (IOException e) {444 return null;445 } finally {446 if (connection != null) {447 connection.disconnect();448 }449 }450 MILLISECONDS.sleep(sleepMillis);451 sleepMillis = (sleepMillis >= MAX_POLL_INTERVAL_MS) ? sleepMillis452 : sleepMillis * 2;453 }454 }455 });456 callback.get(timeout, unit);457 } catch (TimeoutException e) {458 throw new org.openqa.selenium.net.UrlChecker.TimeoutException(String.format(459 "Timed out waiting for %s to become unavailable after %d ms", url,460 MILLISECONDS.convert(System.nanoTime() - start, NANOSECONDS)), e);461 } catch (RuntimeException e) {462 throw e;463 } catch (Exception e) {464 throw new RuntimeException(e);465 }466 }467 /**468 * Create a connection to the specified URL.469 * <p>470 * <b>NOTE</b>: This method was lifted from the {@link org.openqa.selenium.net.UrlChecker UrlChecker} class in the471 * Selenium API.472 * 473 * @param url URL for connection474 * @return connection to the specified URL475 * @throws IOException if an I/O exception occurs476 */477 private static HttpURLConnection connectToUrl(URL url) throws IOException {478 HttpURLConnection connection = (HttpURLConnection) url.openConnection();479 connection.setConnectTimeout(CONNECT_TIMEOUT_MS);480 connection.setReadTimeout(READ_TIMEOUT_MS);481 connection.connect();482 return connection;483 }484}...