
com.infotel.seleniumrobot.grid.CustomRemoteProxy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of seleniumRobot-grid Show documentation
Show all versions of seleniumRobot-grid Show documentation
Selenium grid extension for mobile testing
/**
* Copyright 2017 www.infotel.com
*
* Licensed 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 com.infotel.seleniumrobot.grid;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
import org.bouncycastle.crypto.tls.UDPTransport;
import org.json.JSONException;
import org.openqa.grid.common.RegistrationRequest;
import org.openqa.grid.common.SeleniumProtocol;
import org.openqa.grid.internal.GridRegistry;
import org.openqa.grid.internal.RemoteProxy;
import org.openqa.grid.internal.TestSession;
import org.openqa.grid.internal.TestSlot;
import org.openqa.grid.selenium.proxy.DefaultRemoteProxy;
import org.openqa.grid.web.servlet.handler.RequestType;
import org.openqa.grid.web.servlet.handler.SeleniumBasedRequest;
import org.openqa.selenium.Platform;
import org.openqa.selenium.chrome.ChromeDriverService;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.edge.EdgeDriverService;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxProfile;
import org.openqa.selenium.firefox.GeckoDriverService;
import org.openqa.selenium.firefox.ProfilesIni;
import org.openqa.selenium.ie.InternetExplorerDriverService;
import org.openqa.selenium.remote.BrowserType;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.server.jmx.ManagedService;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import com.infotel.seleniumrobot.grid.config.LaunchConfig;
import com.infotel.seleniumrobot.grid.servlets.client.MobileNodeServletClient;
import com.infotel.seleniumrobot.grid.servlets.client.NodeStatusServletClient;
import com.infotel.seleniumrobot.grid.servlets.client.NodeTaskServletClient;
import com.infotel.seleniumrobot.grid.servlets.server.FileServlet;
import com.infotel.seleniumrobot.grid.servlets.server.StatusServlet;
import com.infotel.seleniumrobot.grid.utils.GridStatus;
import com.seleniumtests.browserfactory.BrowserInfo;
import com.seleniumtests.browserfactory.SeleniumRobotCapabilityType;
import com.seleniumtests.customexception.ConfigurationException;
import io.appium.java_client.remote.MobileCapabilityType;
import kong.unirest.UnirestException;
/**
* Custom proxy that handles the following features
* - Set path to driver into node system properties so that driver can be found when browser starts (method: beforeSession)
* - kill browser and driver processes after the test ends. Pids are detected by difference between list of pids before we create the session and after session has been created
* (method: beforeStartSession & afterStartSession)
* - prepare tests for mobile: application file URL added to capabilities and device capabilities updated to reflect a real device instead of a generic one (e.g: Android)
* - start / stop appium for mobile testing (method: afterSession)
* - restart hub / node when max number of test sessions has been reached (method: afterSession)
* - manage the hub and node status (ACTIVE or INACTIVE). No new test session will be created on a node if it's inactive. Currently running session will go to end (method: hasCapability)
*
* @author S047432
*
*/
@ManagedService(description = "Selenium Custom Grid Hub TestSlot")
public class CustomRemoteProxy extends DefaultRemoteProxy {
public static final String ALL_ACCESS = "allAccess";
public static final String PREEXISTING_DRIVER_PIDS = "preexistingDriverPids";
public static final String CURRENT_DRIVER_PIDS = "currentDriverPids";
public static final String PIDS_TO_KILL = "pidsToKill";
public static final int DEFAULT_LOCK_TIMEOUT = 30;
private static Integer hubTestSessionCount = 0;
private static LocalDateTime lowActivityBeginning = null;
private boolean upgradeAttempted = false;
private int lockTimeout;
private int testSessionsCount = 0;
private NodeTaskServletClient nodeClient;
private NodeStatusServletClient nodeStatusClient;
private MobileNodeServletClient mobileServletClient;
private Lock newTestSessionLock;
private static final Logger logger = Logger.getLogger(CustomRemoteProxy.class);
public CustomRemoteProxy(RegistrationRequest request, GridRegistry registry) {
super(request, registry);
init(new NodeTaskServletClient(getRemoteHost().getHost(), getRemoteHost().getPort()),
new NodeStatusServletClient(getRemoteHost().getHost(), getRemoteHost().getPort()),
new MobileNodeServletClient(getRemoteHost().getHost(), getRemoteHost().getPort()),
DEFAULT_LOCK_TIMEOUT);
}
// for test only
public CustomRemoteProxy(RegistrationRequest request,
GridRegistry registry,
NodeTaskServletClient nodeClient,
NodeStatusServletClient nodeStatusClient,
MobileNodeServletClient mobileServletClient,
int lockTimeout) {
super(request, registry);
init(nodeClient, nodeStatusClient, mobileServletClient, lockTimeout);
}
private void init(NodeTaskServletClient nodeClient,
NodeStatusServletClient nodeStatusClient,
MobileNodeServletClient mobileServletClient,
int lockTimeout) {
this.nodeClient = nodeClient;
this.nodeStatusClient = nodeStatusClient;
this.mobileServletClient = mobileServletClient;
newTestSessionLock = new ReentrantLock();
this.lockTimeout = lockTimeout;
}
@Override
public void beforeCommand(TestSession session, HttpServletRequest request, HttpServletResponse response) {
super.beforeCommand(session, request, response);
// for new session request, add driver path to capabilities so that they can be read by node
String body = ((SeleniumBasedRequest)request).getBody();
try {
JsonObject map = new JsonParser().parse(body).getAsJsonObject();
boolean bodyChanged = false;
if (map.has("capabilities")) {
map.getAsJsonObject("capabilities").remove("desiredCapabilities");
map.getAsJsonObject("capabilities").add("desiredCapabilities", new JsonParser().parse(new Gson().toJson(session.getRequestedCapabilities())).getAsJsonObject());
bodyChanged = true;
}
if (map.has("desiredCapabilities")) {
map.remove("desiredCapabilities");
map.add("desiredCapabilities", new JsonParser().parse(new Gson().toJson(session.getRequestedCapabilities())).getAsJsonObject());
bodyChanged = true;
}
if (bodyChanged) {
((SeleniumBasedRequest)request).setBody(map.toString());
}
} catch (JsonSyntaxException | IllegalStateException | UnsupportedEncodingException e) {
}
// get PID before we create driver
// use locking so that only one session is created at a time
if(((SeleniumBasedRequest)request).getRequestType() == RequestType.START_SESSION) {
beforeStartSession(session);
}
else if(((SeleniumBasedRequest)request).getRequestType() == RequestType.STOP_SESSION) {
beforeStopSession(session);
}
}
/**
* Do something after the START_SESSION and STOP_SESSION commands has been sent
*/
@Override
public void afterCommand(TestSession session, HttpServletRequest request, HttpServletResponse response) {
super.afterCommand(session, request, response);
if(((SeleniumBasedRequest)request).getRequestType() == RequestType.START_SESSION) {
afterStartSession(session);
}
else if(((SeleniumBasedRequest)request).getRequestType() == RequestType.STOP_SESSION && session.get(PIDS_TO_KILL) != null) {
afterStopSession(session);
}
}
/**
* Do actions before test session creation
* - add device name when doing mobile testing. This will allow to add missing caps, for example when client requests an android device without specifying it precisely
* - make file available as HTTP URL instead of FILE URL. These files must have already been uploaded to grid hub
*
*/
@Override
public void beforeSession(TestSession session) {
// add firefox & chrome binary to caps
super.beforeSession(session);
boolean mobilePlatform = false;
Map requestedCaps = session.getRequestedCapabilities();
// update capabilities for mobile. Mobile tests are identified by the use of 'platformName' capability
// this will allow to add missing caps, for example when client requests an android device without specifying it precisely
String platformName = (String)requestedCaps.getOrDefault(MobileCapabilityType.PLATFORM_NAME, "nonmobile");
if (platformName.toLowerCase().contains("ios") || platformName.toLowerCase().contains("android")) {
mobilePlatform = true;
try {
DesiredCapabilities caps = mobileServletClient.updateCapabilities(new DesiredCapabilities(requestedCaps));
requestedCaps.putAll(caps.asMap());
} catch (IOException | URISyntaxException e) {
}
try {
String appiumUrl = nodeClient.startAppium(session.getInternalKey());
requestedCaps.put("appiumUrl", appiumUrl);
} catch (UnirestException e) {
throw new ConfigurationException("Could not start appium: " + e.getMessage());
}
}
// replace all capabilities whose value begins with 'file:' by the remote HTTP URL
// we assume that these files have been previously uploaded on hub and thus available
for (Entry entry: session.getRequestedCapabilities().entrySet()) {
if (entry.getValue() instanceof String && ((String)entry.getValue()).startsWith(FileServlet.FILE_PREFIX)) {
requestedCaps.put(entry.getKey(), String.format("http://%s:%s/grid/admin/FileServlet/%s",
getConfig().getHubHost(),
getConfig().getHubPort(),
((String)entry.getValue()).replace(FileServlet.FILE_PREFIX, "")));
}
}
// set marionette mode depending on firefox version
Map nodeCapabilities = session.getSlot().getCapabilities();
if (nodeCapabilities.get(CapabilityType.BROWSER_NAME) != null
&& nodeCapabilities.get(CapabilityType.BROWSER_NAME).equals(BrowserType.FIREFOX.toString().toLowerCase())
&& nodeCapabilities.get(CapabilityType.BROWSER_VERSION) != null) {
if (Float.parseFloat((String)nodeCapabilities.get(CapabilityType.BROWSER_VERSION)) < 47.9) {
requestedCaps.put("marionette", false);
} else {
requestedCaps.put("marionette", true);
}
}
// add driver path if it's present in node capabilities, so that they can be transferred to node
if (nodeCapabilities.get(CapabilityType.BROWSER_NAME) != null) {
try {
if (nodeCapabilities.get(CapabilityType.BROWSER_NAME).toString().toLowerCase().contains(BrowserType.CHROME.toLowerCase()) && nodeCapabilities.get(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY) != null) {
updateChromeCapabilities(requestedCaps, nodeCapabilities);
} else if (nodeCapabilities.get(CapabilityType.BROWSER_NAME).toString().toLowerCase().contains(BrowserType.FIREFOX.toLowerCase()) && nodeCapabilities.get(GeckoDriverService.GECKO_DRIVER_EXE_PROPERTY) != null) {
updateFirefoxCapabilities(requestedCaps, nodeCapabilities);
} else if (nodeCapabilities.get(CapabilityType.BROWSER_NAME).toString().toLowerCase().contains(BrowserType.IE.toLowerCase()) && nodeCapabilities.get(InternetExplorerDriverService.IE_DRIVER_EXE_PROPERTY) != null) {
requestedCaps.put(InternetExplorerDriverService.IE_DRIVER_EXE_PROPERTY, nodeCapabilities.get(InternetExplorerDriverService.IE_DRIVER_EXE_PROPERTY).toString());
nodeClient.setProperty(InternetExplorerDriverService.IE_DRIVER_EXE_PROPERTY, nodeCapabilities.get(InternetExplorerDriverService.IE_DRIVER_EXE_PROPERTY).toString());
} else if (nodeCapabilities.get(CapabilityType.BROWSER_NAME).toString().toLowerCase().contains(BrowserType.EDGE.toLowerCase()) && nodeCapabilities.get(EdgeDriverService.EDGE_DRIVER_EXE_PROPERTY) != null) {
requestedCaps.put(EdgeDriverService.EDGE_DRIVER_EXE_PROPERTY, nodeCapabilities.get(EdgeDriverService.EDGE_DRIVER_EXE_PROPERTY).toString());
nodeClient.setProperty(EdgeDriverService.EDGE_DRIVER_EXE_PROPERTY, nodeCapabilities.get(EdgeDriverService.EDGE_DRIVER_EXE_PROPERTY).toString());
}
} catch (UnirestException e) {
throw new ConfigurationException("Could not transfer driver path to node, abord: " + e.getMessage());
}
}
// remove se:CONFIG_UUID for IE (issue #15) (moved from CustomDriverProvider)
if (BrowserType.IE.equals(requestedCaps.get(CapabilityType.BROWSER_NAME))) {
requestedCaps.remove("se:CONFIG_UUID");
}
// issue #54: set the platform to family platform, not the more precise one as this fails. Platform value may be useless now as test slot has been selected
if (!mobilePlatform && requestedCaps.get(CapabilityType.PLATFORM) != null && ((Platform)requestedCaps.get(CapabilityType.PLATFORM)).family() != null) {
Platform pf = (Platform)requestedCaps.remove(CapabilityType.PLATFORM);
requestedCaps.put(CapabilityType.PLATFORM, pf.family());
requestedCaps.remove(CapabilityType.PLATFORM_NAME);
requestedCaps.put(CapabilityType.PLATFORM_NAME, pf.family().toString());
}
}
/**
* Update capabilites for firefox, depending on what is requested
* @param requestedCaps
* @param nodeCapabilities
*/
private void updateFirefoxCapabilities(Map requestedCaps, Map nodeCapabilities) {
requestedCaps.put(GeckoDriverService.GECKO_DRIVER_EXE_PROPERTY, nodeCapabilities.get(GeckoDriverService.GECKO_DRIVER_EXE_PROPERTY).toString());
// in case "firefoxProfile" capability is set, add the '--user-data-dir' option. If value is 'default', search the default user profile
if (requestedCaps.get("firefoxProfile") != null) {
try {
// get some options of the current profile
FirefoxProfile profile = FirefoxProfile.fromJson((String) requestedCaps.get("firefox_profile"));
String userAgent = profile.getStringPreference("general.useragent.override", null);
String ntlmTrustedUris = profile.getStringPreference("network.automatic-ntlm-auth.trusted-uris", null);
FirefoxProfile newProfile;
if (requestedCaps.get("firefoxProfile").equals(BrowserInfo.DEFAULT_BROWSER_PRODFILE)) {
newProfile = new ProfilesIni().getProfile("default");
} else {
newProfile = new FirefoxProfile(new File((String) requestedCaps.get("firefoxProfile")));
}
if (userAgent != null) {
newProfile.setPreference("general.useragent.override", userAgent);
}
if (ntlmTrustedUris != null) {
newProfile.setPreference("network.automatic-ntlm-auth.trusted-uris", ntlmTrustedUris);
}
newProfile.setPreference("capability.policy.default.Window.QueryInterface", ALL_ACCESS);
newProfile.setPreference("capability.policy.default.Window.frameElement.get", ALL_ACCESS);
newProfile.setPreference("capability.policy.default.HTMLDocument.compatMode.get", ALL_ACCESS);
newProfile.setPreference("capability.policy.default.Document.compatMode.get", ALL_ACCESS);
newProfile.setPreference("dom.max_chrome_script_run_time", 0);
newProfile.setPreference("dom.max_script_run_time", 0);
requestedCaps.put(FirefoxDriver.PROFILE, newProfile.toJson());
} catch (Exception e) {
logger.error("Cannot change firefox profile", e);
}
}
// issue #60: if "firefox_binary" is set (case of custom / portable browsers), add it to requested caps, else, session is not started
if (nodeCapabilities.get(FirefoxDriver.BINARY) != null) {
requestedCaps.put(FirefoxDriver.BINARY, nodeCapabilities.get(FirefoxDriver.BINARY));
}
nodeClient.setProperty(GeckoDriverService.GECKO_DRIVER_EXE_PROPERTY, nodeCapabilities.get(GeckoDriverService.GECKO_DRIVER_EXE_PROPERTY).toString());
}
private void updateChromeCapabilities(Map requestedCaps, Map nodeCapabilities) {
requestedCaps.put(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY, nodeCapabilities.get(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY).toString());
if (requestedCaps.get(ChromeOptions.CAPABILITY) == null) {
requestedCaps.put(ChromeOptions.CAPABILITY, new HashMap());
}
// in case "chromeProfile" capability is set, add the '--user-data-dir' option. If value is 'default', search the default user profile
if (requestedCaps.get("chromeProfile") != null) {
if (requestedCaps.get("chromeProfile").equals(BrowserInfo.DEFAULT_BROWSER_PRODFILE)) {
((Map>)requestedCaps.get(ChromeOptions.CAPABILITY)).get("args").add("--user-data-dir=" + nodeCapabilities.get("defaultProfilePath"));
} else {
((Map>)requestedCaps.get(ChromeOptions.CAPABILITY)).get("args").add("--user-data-dir=" + requestedCaps.get("chromeProfile"));
}
}
// issue #60: if "chrome_binary" is set (case of custom / portable browsers), add it to requested caps, else, session is not started
if (nodeCapabilities.get("chrome_binary") != null ) {
((Map)requestedCaps.get(ChromeOptions.CAPABILITY)).put("binary", nodeCapabilities.get("chrome_binary"));
}
nodeClient.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY, nodeCapabilities.get(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY).toString());
}
/**
* Clean node after session
* - stop video capture
* - stop appium in case of mobile test
*/
@SuppressWarnings("unchecked")
@Override
public void afterSession(TestSession session) {
try {
try {
nodeClient.stopVideoCapture(session.getExternalKey().getKey());
} catch (UnirestException | NullPointerException e) {
}
// kill appium. Node will handle the existence of appium itself
try {
nodeClient.stopAppium(session.getInternalKey());
} catch (UnirestException | NullPointerException e) {
}
// kill remaining pids
if (session.get(PIDS_TO_KILL) != null) {
for (Long pid: (List) session.get(PIDS_TO_KILL)) {
try {
nodeClient.killProcessByPid(pid);
} catch (UnirestException e) {
logger.error(String.format("cannot kill pid %d: %s", pid, e.getMessage()));
}
}
}
} catch (Exception e) {
logger.warn("error while terminating session: " + e.getMessage(), e);
}
// count session to see if we should restart the node
disableNodeIfMaxSessionsReached();
// count sessions to see if we should restart the hub
disableHubIfMaxSessionsReached();
}
/**
* Clean node when no test session is active. It's like a garbage collector when pid killing was not able to remove all drivers / browser processes
* Cleaning will
* - remove all drivers
* - remove all browsers
* - clean temp directory as it seems that some browsers write to it
*/
public void cleanNode() {
if (isBusy()) {
return;
}
boolean locked = newTestSessionLock.tryLock();
if (locked) {
try {
nodeClient.cleanNode();
} catch (Exception e) {
logger.warn("error while cleaning node: " + e.getMessage());
}
// do not crash thread in case the lock has changed between the acquiring and releasing
// this could happen if cleaning takes more than 'lockTimeout' seconds while a new session is being created. In that case, a new lock is created
// and releasing this one will lead to an IllegalMonitorStateException
try {
newTestSessionLock.unlock();
} catch (IllegalMonitorStateException e) {
}
}
}
/**
* Mark node as inactive if a maximum number of sessions has been set and this maximum has been reached
*/
public void disableNodeIfMaxSessionsReached() {
testSessionsCount++;
Integer maxNodeTestCount = LaunchConfig.getCurrentLaunchConfig().getMaxNodeTestCount();
if (maxNodeTestCount != null && maxNodeTestCount > 0 && testSessionsCount > maxNodeTestCount) {
try {
nodeStatusClient.setStatus(GridStatus.INACTIVE);
} catch (Exception e) {
logger.warn(String.format("could not mark node %s as inactive: %s", getRemoteHost().toString(), e.getMessage()));
}
}
}
/**
* Mark hub as inactive if
* - a max number of session has been set and this maximum has been reached
* AND
* - hub is not used or max 10% of test slots are used
* AND
* - this low activity remains for a minute
*
* OR if a max number of session has been set and 2 times the maximum has been reached
*/
public void disableHubIfMaxSessionsReached() {
incrementHubTestSessionCount();
Integer maxHubTestCount = LaunchConfig.getCurrentLaunchConfig().getMaxHubTestCount();
if (maxHubTestCount != null && maxHubTestCount > 0) {
// if more than 10% of the test slots are in use, we are not in low activity
// at this stage, the currently finishing session is still count (=> remove it from count)
double currentActivity = (getUsedTestSlots() - 1) * 1.0 / getHubTotalTestSlots();
if (currentActivity < 0.1 && getLowActivityBeginning() == null) {
setLowActivityBeginning();
} else if (currentActivity >= 0.1) {
resetLowActivityBeginning();
}
if (getHubTestSessionCount() > 2 * maxHubTestCount
|| getHubTestSessionCount() > maxHubTestCount // a max number of session has been set and this maximum has been reached
&& getLowActivityBeginning() != null // hub is not used or max 10% of test slots are used
&& getLowActivityBeginning().isBefore(LocalDateTime.now().minusMinutes(1)) // this low activity remains for a minute
) {
setHubStatus(GridStatus.INACTIVE);
}
}
}
/**
* Returns the total number of test sessions that can be run behind the hub
* Sum of concurrent test sessions for each node
* @return
*/
public int getHubTotalTestSlots() {
int testSlotsCount = 0;
for (RemoteProxy proxy : getRegistry().getAllProxies().getSorted()) {
testSlotsCount += proxy.getMaxNumberOfConcurrentTestSessions();
}
return testSlotsCount;
}
/**
* Returns the number of used slots behind the hub
* @return
*/
public int getUsedTestSlots() {
int testSlotsCount = 0;
for (RemoteProxy proxy : getRegistry().getAllProxies().getSorted()) {
testSlotsCount += proxy.getTotalUsed();
}
return testSlotsCount;
}
@Override
public void beforeRelease(TestSession session) {
// get all pids before session is released in case some processes remain
beforeStopSession(session);
super.beforeRelease(session);
}
public boolean isProxyAlive() {
return super.isAlive();
}
@Override
public boolean isAlive() {
// move mouse to avoid computer session locking (on windows for example)
try {
nodeClient.keepAlive();
} catch (UnirestException e) {
}
boolean alive = isProxyAlive();
if (alive) {
// clean node
cleanNode();
// stop node if it's set to inactive, not busy and testSessionCount is greater than max declared
stopNodeWithMaxSessionsReached();
}
// stop hub if it's set to inactive, not busy and testSessionCount is greater than max declared
stopHubWithMaxSessionsReached();
return alive;
}
/**
* Stops the node in case max number of session is reached and node is not busy/active
*/
public void stopNodeWithMaxSessionsReached() {
Integer maxNodeTestCount = LaunchConfig.getCurrentLaunchConfig().getMaxNodeTestCount();
try {
if (maxNodeTestCount != null
&& maxNodeTestCount > 0
&& testSessionsCount > maxNodeTestCount
&& !isBusy()
&& "inactive".equalsIgnoreCase(nodeStatusClient.getStatus().getString("status"))) {
nodeClient.stopNode();
}
} catch (Exception e) {
if (e instanceof UnirestException && e.getMessage().contains("Connection reset")) {
return;
}
logger.warn(String.format("could not stop node %s: %s", getRemoteHost().toString(), e.getMessage()));
}
}
/**
* stop hub if it's set to inactive, not busy and testSessionCount is greater than max declared
*/
public void stopHubWithMaxSessionsReached() {
Integer maxHubTestCount = LaunchConfig.getCurrentLaunchConfig().getMaxHubTestCount();
try {
if (maxHubTestCount != null
&& maxHubTestCount > 0
&& getHubTestSessionCount() > maxHubTestCount
&& getUsedTestSlots() == 0
&& getHubStatus() == GridStatus.INACTIVE) {
logger.info("stopping hub");
getRegistry().getHub().stop();
}
} catch (Exception e) {
logger.warn(String.format("could not stop hub: %s", e.getMessage()));
}
}
/**
* Returns the status (ACTIVE or INACTIVE) of the hub
* @return
*/
public synchronized GridStatus getHubStatus() {
try {
return GridStatus.fromString(getRegistry().getHub().getConfiguration().custom.get(StatusServlet.STATUS));
} catch (IllegalArgumentException | NullPointerException e) {
return null;
}
}
public synchronized void setHubStatus(GridStatus status) {
getRegistry().getHub().getConfiguration().custom.put(StatusServlet.STATUS, status.toString());
}
@Override
public boolean hasCapability(Map requestedCapability) {
GridStatus hubStatus = getHubStatus();
if (hubStatus != null && GridStatus.INACTIVE == hubStatus) {
logger.info("Node does not accept sessions anymore, waiting to upgrade");
return false;
}
// check this node is able to handle new sessions
try {
if (GridStatus.INACTIVE.toString().equals(nodeStatusClient.getStatus().get(StatusServlet.STATUS))) {
return false;
}
} catch (JSONException | UnirestException e) {
return false;
}
return super.hasCapability(requestedCapability);
}
public boolean isUpgradeAttempted() {
return upgradeAttempted;
}
public void setUpgradeAttempted(boolean upgradeAttempted) {
this.upgradeAttempted = upgradeAttempted;
}
/**
* Get list of pids corresponding to our driver, before creating it
* This will allow to know the driver pid we have created for this session
*/
private void beforeStartSession(TestSession session) {
try {
// unlock should occur in "afterCommand", if something goes wrong in the calling method, 'afterCommand' will never be called
// unlock after 60 secs to avoid deadlocks
// 60 secs is the delay after which we consider that the driver is created
boolean locked = newTestSessionLock.tryLock(lockTimeout, TimeUnit.SECONDS);
// timeout reached for the previous lock. We consider that the lock will never be released, so create a new one
if (!locked) {
newTestSessionLock = new ReentrantLock();
newTestSessionLock.tryLock(lockTimeout, TimeUnit.SECONDS);
}
List existingPids = nodeClient.getDriverPids((String) session.getRequestedCapabilities().get(CapabilityType.BROWSER_NAME),
(String) session.getRequestedCapabilities().get(CapabilityType.BROWSER_VERSION),
new ArrayList<>());
session.put(PREEXISTING_DRIVER_PIDS, existingPids);
} catch (Exception e) {
newTestSessionLock.unlock();
}
}
/**
* Deduce, from the existing pid list for our driver (e.g: chromedriver), the driver we have created
*/
private void afterStartSession(TestSession session) {
// lock should here still be locked
@SuppressWarnings("unchecked")
List existingPids = (List) session.get(PREEXISTING_DRIVER_PIDS);
try {
// store the newly created browser/driver pids in the session
if (existingPids != null) {
List browserPid = nodeClient.getDriverPids((String) session.getRequestedCapabilities().get(CapabilityType.BROWSER_NAME),
(String) session.getRequestedCapabilities().get(CapabilityType.BROWSER_VERSION),
existingPids);
session.put(CURRENT_DRIVER_PIDS, browserPid);
} else {
session.put(CURRENT_DRIVER_PIDS, new ArrayList<>());
}
} catch (UnirestException e) {
} finally {
if (((ReentrantLock)newTestSessionLock).isLocked()) {
try {
newTestSessionLock.unlock();
} catch (IllegalMonitorStateException e) {}
}
}
}
/**
* Before quitting driver, get list of all pids created: driver pid, browser pids and all sub processes created by browser
* @param session
*/
private void beforeStopSession(TestSession session) {
try {
@SuppressWarnings("unchecked")
List pidsToKill = nodeClient.getBrowserAndDriverPids((String) session.getRequestedCapabilities().get(CapabilityType.BROWSER_NAME),
(String) session.getRequestedCapabilities().get(CapabilityType.BROWSER_VERSION),
session.get(CURRENT_DRIVER_PIDS) == null ? new ArrayList<>(): (List) session.get(CURRENT_DRIVER_PIDS));
session.put(PIDS_TO_KILL, pidsToKill);
} catch (UnirestException e) {
logger.error("cannot get list of pids to kill: " + e.getMessage());
}
}
/**
* Kill all processes identified in beforeStopSession method
* @param session
*/
@SuppressWarnings("unchecked")
private void afterStopSession(TestSession session) {
for (Long pid: (List) session.get(PIDS_TO_KILL)) {
try {
nodeClient.killProcessByPid(pid);
} catch (UnirestException e) {
logger.error(String.format("cannot kill pid %d: %s", pid, e.getMessage()));
}
}
}
public synchronized TestSession getNewSession(Map requestedCapability) {
int configuredMaxSessions = config.maxSession;
try {
if (requestedCapability.containsKey(SeleniumRobotCapabilityType.ATTACH_SESSION_ON_NODE)) {
// if we are on the right proxy, the one where we want to attach this new session, allow more sessions temporary
if (requestedCapability.get(SeleniumRobotCapabilityType.ATTACH_SESSION_ON_NODE).equals(remoteHost.toString())) {
config.maxSession = getTotalUsed() + 1;
// if node do not match, do not go further
} else {
return null;
}
}
return super.getNewSession(requestedCapability);
} catch (Throwable e) {
return null;
} finally {
config.maxSession = configuredMaxSessions;
}
}
@Override
public TestSlot createTestSlot(SeleniumProtocol protocol, Map capabilities) {
// create test slot with a copy of this proxy
RemoteProxy proxyCopy = new CustomRemoteProxyWrapper(this);
String platformName = capabilities.getOrDefault(MobileCapabilityType.PLATFORM_NAME, "nonmobile").toString();
if (platformName.toLowerCase().contains("ios") || platformName.toLowerCase().contains("android")) {
((CustomRemoteProxyWrapper)proxyCopy).setMobileSlot(true);
} else {
((CustomRemoteProxyWrapper)proxyCopy).setMobileSlot(false);
}
return new TestSlot(proxyCopy, protocol, capabilities);
}
public NodeStatusServletClient getNodeStatusClient() {
return nodeStatusClient;
}
public NodeTaskServletClient getNodeClient() {
return nodeClient;
}
public static synchronized Integer getHubTestSessionCount() {
return CustomRemoteProxy.hubTestSessionCount;
}
public static synchronized void incrementHubTestSessionCount() {
CustomRemoteProxy.hubTestSessionCount++;
}
public static synchronized LocalDateTime getLowActivityBeginning() {
return lowActivityBeginning;
}
public static synchronized void resetLowActivityBeginning() {
CustomRemoteProxy.lowActivityBeginning = null;
}
public static synchronized void setLowActivityBeginning() {
CustomRemoteProxy.lowActivityBeginning = LocalDateTime.now();
}
public int getTestSessionsCount() {
return testSessionsCount;
}
public void setTestSessionsCount(int testSessionsCount) {
this.testSessionsCount = testSessionsCount;
}
// for test
public static synchronized void setHubTestSessionCount(Integer hubTestSessionCount) {
CustomRemoteProxy.hubTestSessionCount = hubTestSessionCount;
}
// for test
public static synchronized void setLowActivityBeginning(LocalDateTime lowActivityBeginning) {
CustomRemoteProxy.lowActivityBeginning = lowActivityBeginning;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy