All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.infotel.seleniumrobot.grid.aspects.SessionSlotActions Maven / Gradle / Ivy

The newest version!
package com.infotel.seleniumrobot.grid.aspects;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.openqa.selenium.MutableCapabilities;
import org.openqa.selenium.NoSuchSessionException;
import org.openqa.selenium.Platform;
import org.openqa.selenium.SessionNotCreatedException;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.chrome.ChromeDriverService;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.edge.EdgeDriverService;
import org.openqa.selenium.edge.EdgeOptions;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.firefox.FirefoxProfile;
import org.openqa.selenium.firefox.ProfilesIni;
import org.openqa.selenium.grid.data.CreateSessionRequest;
import org.openqa.selenium.grid.node.ActiveSession;
import org.openqa.selenium.grid.node.local.SessionSlot;
import org.openqa.selenium.internal.Either;
import org.openqa.selenium.io.Zip;
import org.openqa.selenium.remote.Browser;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.remote.SessionId;

import com.infotel.seleniumrobot.grid.config.LaunchConfig;
import com.infotel.seleniumrobot.grid.servlets.client.NodeClient;
import com.infotel.seleniumrobot.grid.tasks.CleanNodeTask;
import com.infotel.seleniumrobot.grid.tasks.DiscoverBrowserAndDriverPidsTask;
import com.infotel.seleniumrobot.grid.tasks.KillTask;
import com.infotel.seleniumrobot.grid.tasks.video.StopVideoCaptureTask;
import com.infotel.seleniumrobot.grid.utils.Utils;
import com.seleniumtests.browserfactory.BrowserInfo;
import com.seleniumtests.browserfactory.SeleniumRobotCapabilityType;
import com.seleniumtests.customexception.ConfigurationException;
import com.seleniumtests.driver.BrowserType;
import com.seleniumtests.util.osutility.OSUtility;
import com.seleniumtests.util.osutility.OSUtilityFactory;

import kong.unirest.UnirestException;

@Aspect
public class SessionSlotActions {

	private static Logger logger = LogManager.getLogger(SessionSlotActions.class);

	public static final String SE_IE_OPTIONS = "se:ieOptions";
	public static final String EDGE_PATH = "edgePath";
	public static final String ALL_ACCESS = "allAccess";
	public static final int DEFAULT_LOCK_TIMEOUT = 30;
	private static Map> pidsToKill = Collections.synchronizedMap(new HashMap<>());
	
	// all PIDs corresponding to browser / driver that already exist on the node
	private static Map> preexistingBrowserAndDriverPids = Collections.synchronizedMap(new HashMap<>());
	
	// all PIDs corresponding to browser / driver that have been created by the session
	private static Map> currentBrowserAndDriverPids = Collections.synchronizedMap(new HashMap<>());

	private Lock newTestSessionLock;
	private int lockTimeout;
	private NodeClient nodeStatusClient;
	
	public SessionSlotActions() {
		this(DEFAULT_LOCK_TIMEOUT, null);
	}
	
	/**
	 * Constructor to be used in tests
	 * @param lockTimeout
	 * @param nodeStatusClient
	 */
	public SessionSlotActions(int lockTimeout, NodeClient nodeStatusClient) {
		this.lockTimeout = lockTimeout;
		newTestSessionLock = new ReentrantLock();
		this.nodeStatusClient = nodeStatusClient;
	}
	
	private NodeClient getNodeStatusClient() {
		if (nodeStatusClient == null) {
			nodeStatusClient = new NodeClient(LaunchConfig.getCurrentNodeConfig().getServerOptions().getExternalUri());
		} 
		return nodeStatusClient;
	}
	
//	@Around("execution(public * org.openqa.selenium.grid.node.local.SessionSlot..* (..)) ")
//	public Object logLocalNode(ProceedingJoinPoint joinPoint) throws Throwable {
//		System.out.println("coucou2: " + joinPoint.getSignature());
//		Object reply = joinPoint.proceed(joinPoint.getArgs());
//		
//		return reply;
//	} 
	
	@Around("execution(public * org.openqa.selenium.grid.node.local.SessionSlot.apply (..)) ")
	public Object onNewSession(ProceedingJoinPoint joinPoint) throws Throwable {
		
		CreateSessionRequest initialSessionRequest = (CreateSessionRequest) joinPoint.getArgs()[0];
		SessionSlot slot = (SessionSlot)joinPoint.getThis();
		CreateSessionRequest sessionRequest = beforeStartSession(initialSessionRequest, slot);
		
		try {
			Either result = (Either) joinPoint.proceed(new Object[] {sessionRequest});
			if (result.isLeft() && result.left() instanceof SessionNotCreatedException) {
				OSUtility.resetInstalledBrowsersWithVersion();
				sessionRequest = beforeStartSession(initialSessionRequest, slot);
				return joinPoint.proceed(new Object[] {sessionRequest});
			} else {
				return result;
			}

		} finally {
			try {
				afterStartSession(slot.getSession().getId(), slot);
			} catch (Exception e) {
				// do not rethrow an exception raised in finally block
			}
			
		}
	} 
	
	@Around("execution(public * org.openqa.selenium.grid.node.local.SessionSlot.stop (..)) ")
	public Object onStopSession(ProceedingJoinPoint joinPoint) throws Throwable {

		SessionSlot slot = (SessionSlot)joinPoint.getThis();
		SessionId sessionId;
		try {
			sessionId = slot.getSession().getId();
		} catch (NoSuchSessionException e) {
			sessionId = null;
		}
		
		beforeStopSession(sessionId, slot);
		try {
			return joinPoint.proceed(joinPoint.getArgs());
		} finally {
			afterStopSession(sessionId);
		}
	} 
	
	public CreateSessionRequest beforeStartSession(CreateSessionRequest sessionRequest, SessionSlot slot) {
		
		if (!getNodeStatusClient().isBusyOnOtherSlot(null)) {
			cleanNode();
		}
		
		// 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
		boolean clearLock = false;
		try {
			// unlock should occur in "afterStartSession", if something goes wrong in the calling method, 'afterStartSession' may never be called
			// unlock after 30 secs to avoid deadlocks
			// 30 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 = new DiscoverBrowserAndDriverPidsTask(slot.getStereotype().getBrowserName(), slot.getStereotype().getBrowserVersion())
							.withExistingPids(new ArrayList<>())
							.execute()
							.getProcessPids();

			setPreexistingBrowserAndDriverPids(slot, existingPids);
			
			
		} catch (Exception e) {
			// do nothing if something goes wrong
		}
		

		boolean mobilePlatform = false;
		Map requestedCaps = new HashMap<>(sessionRequest.getDesiredCapabilities().asMap());
		Map slotCaps = slot.getStereotype().asMap();

		// add driver path if it's present in node capabilities, so that they can be transferred to node
		String browserName = (String)slotCaps.get(CapabilityType.BROWSER_NAME);
		if (browserName != null) {
			if (browserName.toLowerCase().contains(Browser.CHROME.browserName().toLowerCase())) {
				updateChromeCapabilities(requestedCaps, slotCaps);
			
			} else if (browserName.toLowerCase().contains(Browser.FIREFOX.browserName().toLowerCase())) {
				updateFirefoxCapabilities(requestedCaps, slotCaps);
				
			} else if (browserName.toLowerCase().contains(Browser.IE.browserName().toLowerCase())) {
				updateInternetExplorerCapabilities(requestedCaps, slotCaps);

			} else if (browserName.toLowerCase().contains(Browser.EDGE.browserName().toLowerCase())) {
				updateEdgeCapabilities(requestedCaps, slotCaps);
			}
		}
		
		// 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_NAME) != null && ((Platform)requestedCaps.get(CapabilityType.PLATFORM_NAME)).family() != null) {
			Platform pf = (Platform)requestedCaps.remove(CapabilityType.PLATFORM_NAME);
			requestedCaps.remove(CapabilityType.PLATFORM_NAME);
			requestedCaps.put(CapabilityType.PLATFORM_NAME, pf.family().toString());
		}
		
		return new CreateSessionRequest(sessionRequest.getDownstreamDialects(), new MutableCapabilities(requestedCaps), sessionRequest.getMetadata());

	}

	
	/**
	 * Before quitting driver, get list of all pids created: driver pid, browser pids and all sub processes created by browser
	 * @param session
	 */
	public void beforeStopSession(SessionId sessionId, SessionSlot slot) {
		
		// stop video capture if it has not already been done
		try {
			new StopVideoCaptureTask(sessionId.toString()).execute();
		} catch (Exception e) {
			
		}
		
		// TODO: appium may now be handled outside of seleniumGrid
		// kill appium. Node will handle the existence of appium itself
//		try {
//			nodeClient.stopAppium(session.getInternalKey());
//		} catch (UnirestException | NullPointerException e) {
//			
//		}
		
		try {
			// search all PIDS corresponding to driver and browser, for this session
			@SuppressWarnings("unchecked")
			List pids = new DiscoverBrowserAndDriverPidsTask(slot.getStereotype().getBrowserName(), slot.getStereotype().getBrowserVersion())
				.withParentsPids(getCurrentBrowserAndDriverPids(sessionId) == null ? new ArrayList<>(): getCurrentBrowserAndDriverPids(sessionId))
				.execute()
				.getProcessPids();

			setPidsToKill(sessionId, pids);
			removeCurrentBrowserPids(sessionId);
		} catch (Exception e) {
			logger.error("cannot get list of pids to kill: " + e.getMessage());
		}
	}

	
	/**
	 * Kill all processes identified in beforeStopSession method
	 * @param session
	 */
	@SuppressWarnings("unchecked")
	public void afterStopSession(SessionId sessionId) {
		if (sessionId == null) {
			return;
		}
		
		for (Long pid: getPidsToKill(sessionId)) {
			try {
				new KillTask().withPid(pid)
					.execute();
			} catch (Exception e) {
				logger.error(String.format("cannot kill pid %d: %s", pid, e.getMessage()));
			}
		}
		
		// avoid keeping PIDs for terminated sessions
		pidsToKill.remove(sessionId);
		
		if (!getNodeStatusClient().isBusyOnOtherSlot(sessionId.toString())) {
			cleanNode();
		}
	}
	

	/**
	 * 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
	 */

	
	/**
	 * Deduce, from the existing pid list for our driver (e.g: chromedriver), the driver we have created
	 */
	public void afterStartSession(SessionId sessionId, SessionSlot slot) {
		// lock should here still be locked
		List existingPids = getPreexistingBrowserAndDriverPids(sessionId);
		try {
			
			// store the newly created browser/driver pids in the session
			if (existingPids != null) {
				List browserPid = new DiscoverBrowserAndDriverPidsTask(slot.getStereotype().getBrowserName(), slot.getStereotype().getBrowserVersion())
						.withExistingPids(existingPids)
						.execute()
						.getProcessPids();
				setCurrentBrowserAndDriverPids(sessionId, browserPid);
			} else {
				setCurrentBrowserAndDriverPids(sessionId, new ArrayList<>());
			}
					
		} catch (Exception e) {
			
		} finally {
			removePreexistingPidsForSession(sessionId);
			if (((ReentrantLock)newTestSessionLock).isLocked()) {
				try {
					newTestSessionLock.unlock();
				} catch (IllegalMonitorStateException e) {}
			}
		}
	}

	/* Help tests */
	public void removePreexistingPidsForSession(SessionId sessionId) {
		preexistingBrowserAndDriverPids.remove(sessionId);
	}

	public void setCurrentBrowserAndDriverPids(SessionId sessionId, List browserPid) {
		currentBrowserAndDriverPids.put(sessionId, browserPid);
	}
	
	public List getCurrentBrowserAndDriverPids(SessionId sessionId) {
		return currentBrowserAndDriverPids.get(sessionId);
	}

	public void removeCurrentBrowserPids(SessionId sessionId) {
		currentBrowserAndDriverPids.remove(sessionId);
	}

	public List getPreexistingBrowserAndDriverPids(SessionId sessionId) {
		return preexistingBrowserAndDriverPids.get(sessionId);
	}

	public void setPreexistingBrowserAndDriverPids(SessionSlot slot, List existingPids) {
		preexistingBrowserAndDriverPids.put(slot.getSession().getId(), existingPids);
	}

	public void setPidsToKill(SessionId sessionId, List pids) {
		pidsToKill.put(sessionId, pids);
	}

	public List getPidsToKill(SessionId sessionId) {
		return pidsToKill.getOrDefault(sessionId, new ArrayList<>());
	}
	
	/**
	 * @param requestedCaps
	 * @param slotCaps
	 */
	@SuppressWarnings("unchecked")
	private void updateInternetExplorerCapabilities(Map requestedCaps, Map slotCaps) {

		if (requestedCaps.containsKey(SeleniumRobotCapabilityType.EDGE_IE_MODE) 
				&& (boolean) requestedCaps.get(SeleniumRobotCapabilityType.EDGE_IE_MODE) 
				&& slotCaps.get(EDGE_PATH) != null) {

			// put in both location as Selenium3 does not handle edge chromium properly
			requestedCaps.putIfAbsent(SE_IE_OPTIONS, new HashMap<>());
			((Map) requestedCaps.get(SE_IE_OPTIONS)).put("ie.edgechromium", true);
			((Map) requestedCaps.get(SE_IE_OPTIONS)).put("ie.edgepath", slotCaps.get(EDGE_PATH));
		}
		
		// remove custom capabilities
		requestedCaps.remove(SeleniumRobotCapabilityType.TEST_NAME);
		requestedCaps.remove(SeleniumRobotCapabilityType.BETA_BROWSER);
		requestedCaps.remove(SeleniumRobotCapabilityType.EDGE_IE_MODE);
		
		// remove se:CONFIG_UUID for IE (issue #15) (moved from CustomDriverProvider)
		requestedCaps.remove("se:CONFIG_UUID");
		
	}
	
	private String firefoxProfileToJson(FirefoxProfile profile) throws IOException {
	    File file = profile.layoutOnDisk();
	    try {
	      return Zip.zip(file);
	    } finally {
	      profile.clean(file);
	    }
	  }
	
	/**
	 * Update capabilites for firefox, depending on what is requested
	 * @param requestedCaps
	 * @param slotCaps
	 */
	private void updateFirefoxCapabilities(Map requestedCaps, Map slotCaps) {

		// in case "firefoxProfile" capability is set, add the '--user-data-dir' option. If value is 'default', search the default user profile
		if (requestedCaps.get(SeleniumRobotCapabilityType.FIREFOX_PROFILE) != null) {
			try {
				// get some options of the current profile
				FirefoxProfile profile = FirefoxProfile.fromJson((String) ((Map) requestedCaps
						.get(FirefoxOptions.FIREFOX_OPTIONS))
						.get("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(SeleniumRobotCapabilityType.FIREFOX_PROFILE).equals(BrowserInfo.DEFAULT_BROWSER_PRODFILE)) {
					newProfile = new ProfilesIni().getProfile("default");
				} else {
					newProfile = new FirefoxProfile(new File((String) requestedCaps.get(SeleniumRobotCapabilityType.FIREFOX_PROFILE)));
				}
				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);
		        
		        Map newFfOptions = new HashMap<>();
		        for (Entry entry: ((Map)requestedCaps.get(FirefoxOptions.FIREFOX_OPTIONS)).entrySet()) {
		        	newFfOptions.put(entry.getKey().toString(), entry.getValue());
		        }
		        newFfOptions.put("profile", firefoxProfileToJson(newProfile));
		        
//		        Map ffOptions = .stream()
		        ((Map) requestedCaps)
						.put(FirefoxOptions.FIREFOX_OPTIONS, Collections.unmodifiableMap(newFfOptions));
				
			} catch (Exception e) {
				logger.error("Cannot change firefox profile", e);
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	private void updateChromeCapabilities(Map requestedCaps, Map slotCaps) {
		
		if (requestedCaps.get(ChromeOptions.CAPABILITY) == null) {
			requestedCaps.put(ChromeOptions.CAPABILITY, new HashMap());
		}
		
		// in case "sr:chromeProfile" capability is set, add the '--user-data-dir' option. If value is 'default', search the default user profile
		if (requestedCaps.get(SeleniumRobotCapabilityType.CHROME_PROFILE) != null) {
			if (requestedCaps.get(SeleniumRobotCapabilityType.CHROME_PROFILE).equals(BrowserInfo.DEFAULT_BROWSER_PRODFILE)) {
				((Map>)requestedCaps.get(ChromeOptions.CAPABILITY)).get("args").add("--user-data-dir=" + slotCaps.get(LaunchConfig.DEFAULT_PROFILE_PATH));
			} else {
				((Map>)requestedCaps.get(ChromeOptions.CAPABILITY)).get("args").add("--user-data-dir=" + requestedCaps.get(SeleniumRobotCapabilityType.CHROME_PROFILE));
			}
		}
		
		if (slotCaps.get(ChromeOptions.CAPABILITY) != null && ((Map)(slotCaps.get(ChromeOptions.CAPABILITY))).get("binary") != null) {
			((Map)requestedCaps.get(ChromeOptions.CAPABILITY)).put("binary", ((Map)(slotCaps.get(ChromeOptions.CAPABILITY))).get("binary"));
		}	
		
		// get driver from chrome version
		// driver is not set anymore on startup so that automatic chrome update don't lead to error using an old driver
		List chromeBrowsers = OSUtility.getInstalledBrowsersWithVersion(true).get(BrowserType.CHROME);
		Optional browserInfo = chromeBrowsers.stream().filter(b -> b.getBeta() == (Boolean)requestedCaps.getOrDefault(SeleniumRobotCapabilityType.BETA_BROWSER, false)).findFirst();
		if (browserInfo.isPresent()) {
			String driverPath = Utils.getDriverDir().toString().replace(File.separator, "/") + "/";
			String ext = OSUtilityFactory.getInstance().getProgramExtension();
			System.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY, driverPath + browserInfo.get().getDriverFileName() + ext);
		} else {
			throw new SessionNotCreatedException("No chrome browser / driver supports requested caps");
		}
	}
	
	@SuppressWarnings("unchecked")
	private void updateEdgeCapabilities(Map requestedCaps, Map slotCaps) {
	
		if (requestedCaps.get(EdgeOptions.CAPABILITY) == null) {
			requestedCaps.put(EdgeOptions.CAPABILITY, new HashMap());
		}
		
		// in case "edgeProfile" capability is set, add the '--user-data-dir' option. If value is 'default', search the default user profile
		if (requestedCaps.get(SeleniumRobotCapabilityType.EDGE_PROFILE) != null) {
			if (requestedCaps.get(SeleniumRobotCapabilityType.EDGE_PROFILE).equals(BrowserInfo.DEFAULT_BROWSER_PRODFILE)) {
				((Map>)requestedCaps.get(EdgeOptions.CAPABILITY)).get("args").add("--user-data-dir=" + slotCaps.get(LaunchConfig.DEFAULT_PROFILE_PATH));
			} else {
				((Map>)requestedCaps.get(EdgeOptions.CAPABILITY)).get("args").add("--user-data-dir=" + requestedCaps.get(SeleniumRobotCapabilityType.EDGE_PROFILE));
			}
		}
		
		if (slotCaps.get(EdgeOptions.CAPABILITY) != null && ((Map)(slotCaps.get(EdgeOptions.CAPABILITY))).get("binary") != null) {
			((Map)requestedCaps.get(EdgeOptions.CAPABILITY)).put("binary", ((Map)(slotCaps.get(EdgeOptions.CAPABILITY))).get("binary"));
		}
		
		// get driver from edge version
		// driver is not set anymore on startup so that automatic edge update don't lead to error using an old driver
		List edgeBrowsers = OSUtility.getInstalledBrowsersWithVersion(true).get(BrowserType.EDGE);
		Optional browserInfo = edgeBrowsers.stream().filter(b -> b.getBeta() == (Boolean)requestedCaps.getOrDefault(SeleniumRobotCapabilityType.BETA_BROWSER, false)).findFirst();
		if (browserInfo.isPresent()) {
			String driverPath = Utils.getDriverDir().toString().replace(File.separator, "/") + "/";
			String ext = OSUtilityFactory.getInstance().getProgramExtension();
			System.setProperty(EdgeDriverService.EDGE_DRIVER_EXE_PROPERTY, driverPath + browserInfo.get().getDriverFileName() + ext);
		} else {
			throw new SessionNotCreatedException("No edge browser / driver supports requested caps");
		}
	
	}
	
	/**
	 * 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
	 * Be sure the node is not used anymore with "isBusy" or isBusyOnOtherSlots
	 */
	public void cleanNode() {
		
		boolean locked = newTestSessionLock.tryLock();
		if (locked) {
			
			
			try {
				new CleanNodeTask().execute();
			} 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) {
				
			}
		}
	}

	public static Map> getPreexistingBrowserAndDriverPids() {
		return preexistingBrowserAndDriverPids;
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy