org.openqa.selenium.server.BrowserSessionFactory Maven / Gradle / Ivy
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you 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 org.openqa.selenium.server;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.server.browserlaunchers.BrowserLauncher;
import org.openqa.selenium.remote.SessionId;
import org.openqa.selenium.remote.server.log.LoggingManager;
import org.openqa.selenium.remote.server.log.PerSessionLogHandler;
import org.openqa.selenium.server.browserlaunchers.BrowserLauncherFactory;
import org.openqa.selenium.server.browserlaunchers.InvalidBrowserExecutableException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Manages browser sessions, their creation, and their closure.
*
* Maintains a cache of unused and available browser sessions in case the server is reusing
* sessions. Also manages the creation and finalization of all browser sessions.
*
* @author [email protected] (Jennifer Bevan)
*/
public class BrowserSessionFactory {
private static final long DEFAULT_CLEANUP_INTERVAL = 300000; // 5 minutes.
private static final long DEFAULT_MAX_IDLE_SESSION_TIME = 600000; // 10 minutes
private static Logger log = Logger.getLogger(BrowserSessionFactory.class.getName());
// cached, unused, already-launched browser sessions.
protected final Set availableSessions =
Collections.synchronizedSet(new HashSet());
// active browser sessions.
protected final Set activeSessions =
Collections.synchronizedSet(new HashSet());
private final BrowserLauncherFactory browserLauncherFactory;
private final Timer cleanupTimer;
private final long maxIdleSessionTime;
private final boolean doCleanup;
public BrowserSessionFactory(BrowserLauncherFactory blf) {
this(blf, DEFAULT_CLEANUP_INTERVAL, DEFAULT_MAX_IDLE_SESSION_TIME, true);
}
/**
* Constructor for testing purposes.
*
* @param blf an injected BrowserLauncherFactory.
* @param cleanupInterval the time between idle available session cleaning sweeps.
* @param maxIdleSessionTime the max time in ms for an available session to be idle.
* @param doCleanup whether or not the idle session cleanup thread should run.
*/
protected BrowserSessionFactory(BrowserLauncherFactory blf,
long cleanupInterval, long maxIdleSessionTime, boolean doCleanup) {
browserLauncherFactory = blf;
this.maxIdleSessionTime = maxIdleSessionTime;
this.doCleanup = doCleanup;
cleanupTimer = new Timer(/* daemon= */true);
if (doCleanup) {
cleanupTimer.schedule(new CleanupTask(), 0, cleanupInterval);
}
}
/**
* Gets a new browser session, using the SeleniumServer static fields to populate parameters.
*
* @param browserString browser name string
* @param startURL starting url
* @param extensionJs per-session user extension Javascript
* @param configuration Remote Control configuration. Cannot be null.
* @param browserConfigurations capabilities of requested browser
* @return the BrowserSessionInfo for the new browser session.
* @throws RemoteCommandException remote command exception if new session fails
*/
public BrowserSessionInfo getNewBrowserSession(String browserString, String startURL,
String extensionJs, Capabilities browserConfigurations,
RemoteControlConfiguration configuration)
throws RemoteCommandException {
return getNewBrowserSession(browserString, startURL, extensionJs,
browserConfigurations,
configuration.reuseBrowserSessions(),
configuration.isEnsureCleanSession(), configuration);
}
/**
* Gets a new browser session
*
* @param browserString browser name string
* @param startURL starting url
* @param extensionJs per-session user extension Javascript
* @param configuration Remote Control configuration. Cannot be null.
* @param useCached if a cached session should be used if one is available
* @param ensureClean if a clean session (e.g. no previous cookies) is required.
* @param browserConfigurations capabilities of requested browser
* @return the BrowserSessionInfo for the new browser session.
* @throws RemoteCommandException remote command exception if new session fails
*/
protected BrowserSessionInfo getNewBrowserSession(String browserString, String startURL,
String extensionJs, Capabilities browserConfigurations,
boolean useCached, boolean ensureClean, RemoteControlConfiguration configuration)
throws RemoteCommandException {
BrowserSessionInfo sessionInfo = null;
browserString = validateBrowserString(browserString, configuration);
if (configuration.getProxyInjectionModeArg()) {
InjectionHelper.setBrowserSideLogEnabled(configuration.isBrowserSideLogEnabled());
InjectionHelper.init();
}
if (useCached) {
log.info("grabbing available session...");
sessionInfo = grabAvailableSession(browserString, startURL);
}
// couldn't find one in the cache, or not reusing sessions.
if (null == sessionInfo) {
log.info("creating new remote session");
sessionInfo = createNewRemoteSession(browserString, startURL, extensionJs,
browserConfigurations, ensureClean, configuration);
}
assert null != sessionInfo;
if (false/* ensureClean */) {
// need to add this to the launcher API.
// sessionInfo.launcher.hideCurrentSessionData();
}
return sessionInfo;
}
/**
* Ends all browser sessions.
*
* Active and available but inactive sessions are ended.
* @param configuration remote control configuration
*/
protected void endAllBrowserSessions(RemoteControlConfiguration configuration) {
boolean done = false;
Set allSessions = new HashSet<>();
while (!done) {
// to avoid concurrent modification exceptions...
synchronized (activeSessions) {
for (BrowserSessionInfo sessionInfo : activeSessions) {
allSessions.add(sessionInfo);
}
}
synchronized (availableSessions) {
for (BrowserSessionInfo sessionInfo : availableSessions) {
allSessions.add(sessionInfo);
}
}
for (BrowserSessionInfo sessionInfo : allSessions) {
endBrowserSession(true, sessionInfo.sessionId, configuration);
}
done = (0 == activeSessions.size() && 0 == availableSessions.size());
allSessions.clear();
if (doCleanup) {
cleanupTimer.cancel();
}
}
}
/**
* Ends a browser session, using SeleniumServer static fields to populate parameters.
*
* @param sessionId the id of the session to be ended
* @param configuration Remote Control configuration. Cannot be null.
*/
public void endBrowserSession(String sessionId, RemoteControlConfiguration configuration) {
endBrowserSession(false, sessionId, configuration, configuration.isEnsureCleanSession());
}
/**
* Ends a browser session, using SeleniumServer static fields to populate parameters.
*
* @param forceClose if the session should not be reused
* @param sessionId the id of the session to be ended
* @param configuration Remote Control configuration. Cannot be null.
*/
public void endBrowserSession(boolean forceClose, String sessionId,
RemoteControlConfiguration configuration) {
endBrowserSession(forceClose, sessionId, configuration, configuration.isEnsureCleanSession());
}
/**
* Ends a browser session.
*
* @param forceClose if the session should not be reused
* @param sessionId the id of the session to be ended
* @param configuration Remote Control configuration. Cannot be null.
* @param ensureClean if clean sessions (e.g. no leftover cookies) are required.
*/
protected void endBrowserSession(boolean forceClose, String sessionId,
RemoteControlConfiguration configuration,
boolean ensureClean) {
BrowserSessionInfo sessionInfo = lookupInfoBySessionId(sessionId, activeSessions);
if (null != sessionInfo) {
activeSessions.remove(sessionInfo);
try {
if (forceClose || !configuration.reuseBrowserSessions()) {
shutdownBrowserAndClearSessionData(sessionInfo);
} else {
if (null != sessionInfo.session) { // optional field
sessionInfo.session.reset(sessionInfo.baseUrl);
}
// mark what time this session was ended
sessionInfo.lastClosedAt = System.currentTimeMillis();
availableSessions.add(sessionInfo);
}
} finally {
LoggingManager.perSessionLogHandler().removeSessionLogs(new SessionId(sessionId));
if (ensureClean) {
// need to add this to the launcher API.
// sessionInfo.launcher.restoreOriginalSessionData();
}
}
} else {
// look for it in the available sessions.
sessionInfo = lookupInfoBySessionId(sessionId, availableSessions);
if (null != sessionInfo && (forceClose || !configuration.reuseBrowserSessions())) {
try {
availableSessions.remove(sessionInfo);
shutdownBrowserAndClearSessionData(sessionInfo);
} finally {
LoggingManager.perSessionLogHandler().removeSessionLogs(new SessionId(sessionId));
if (ensureClean) {
// sessionInfo.launcher.restoreOriginalSessionData();
}
}
}
}
}
/**
* Shuts down this browser session's launcher and clears out its session data (if session is not
* null).
*
* @param sessionInfo the browser session to end.
*/
protected void shutdownBrowserAndClearSessionData(BrowserSessionInfo sessionInfo) {
try {
sessionInfo.launcher.close(); // can throw RuntimeException
} finally {
if (null != sessionInfo.session) {
FrameGroupCommandQueueSet.clearQueueSet(sessionInfo.sessionId);
}
}
}
/**
* Rewrites the given browser string based on server settings.
*
* @param inputString the input browser string
* @return a possibly-modified browser string.
* @throws IllegalArgumentException if inputString is null.
*/
private String validateBrowserString(String inputString, RemoteControlConfiguration configuration)
throws IllegalArgumentException {
String browserString = inputString;
if (configuration.getForcedBrowserMode() != null) {
browserString = configuration.getForcedBrowserMode();
log.info("overriding browser mode w/ forced browser mode setting: " + browserString);
}
if (configuration.getProxyInjectionModeArg() && browserString.equals("*iexplore")) {
log.warning("running in proxy injection mode, but you used a *iexplore browser string; this is "
+
"almost surely inappropriate, so I'm changing it to *piiexplore...");
browserString = "*piiexplore";
} else if (configuration.getProxyInjectionModeArg() && (browserString.equals("*firefox")
|| browserString.equals("*firefox2") || browserString.equals("*firefox3"))) {
log.warning("running in proxy injection mode, but you used a " + browserString +
" browser string; this is " +
"almost surely inappropriate, so I'm changing it to *pifirefox...");
browserString = "*pifirefox";
}
if (null == browserString) {
throw new IllegalArgumentException("browser string may not be null");
}
return browserString;
}
/**
* Retrieves an available, unused session from the cache.
*
* @param browserString the necessary browser for a suitable session
* @param baseUrl the necessary baseUrl for a suitable session
* @return the session info of the cached session, null if none found.
*/
protected BrowserSessionInfo grabAvailableSession(String browserString,
String baseUrl) {
BrowserSessionInfo sessionInfo = null;
synchronized (availableSessions) {
sessionInfo = lookupInfoByBrowserAndUrl(browserString, baseUrl,
availableSessions);
if (null != sessionInfo) {
availableSessions.remove(sessionInfo);
}
}
if (null != sessionInfo) {
activeSessions.add(sessionInfo);
}
return sessionInfo;
}
/**
* Isolated dependency
*
* @param sessionId session id
* @param port port
* @param configuration Remote Control Configuration
* @return a new FrameGroupCommandQueueSet instance
*/
protected FrameGroupCommandQueueSet makeQueueSet(String sessionId, int port,
RemoteControlConfiguration configuration) {
return FrameGroupCommandQueueSet.makeQueueSet(sessionId,
configuration.getPortDriversShouldContact(), configuration);
}
/**
* Isolated dependency
*
* @param sessionId session id
* @return an existing FrameGroupCommandQueueSet instance
*/
protected FrameGroupCommandQueueSet getQueueSet(String sessionId) {
return FrameGroupCommandQueueSet.getQueueSet(sessionId);
}
/**
* Creates and tries to open a new session.
*
* @param browserString browser name string
* @param startURL starting url
* @param extensionJs per-session user extension javascript
* @param configuration Remote Control configuration. Cannot be null.
* @param browserConfiguration capabilities of requested browser
* @param ensureClean if a clean session is required
* @return the BrowserSessionInfo of the new session.
* @throws RemoteCommandException if the browser failed to launch and request work in the required
* amount of time.
*/
protected BrowserSessionInfo createNewRemoteSession(String browserString, String startURL,
String extensionJs, Capabilities browserConfiguration, boolean ensureClean,
RemoteControlConfiguration configuration)
throws RemoteCommandException {
final FrameGroupCommandQueueSet queueSet;
final BrowserSessionInfo sessionInfo;
final BrowserLauncher launcher;
String sessionId;
sessionId = UUID.randomUUID().toString().replace("-", "");
if ("*webdriver".equals(browserString) && browserConfiguration != null) {
Object id = browserConfiguration.getCapability("webdriver.remote.sessionid");
if (id != null && id instanceof String) {
sessionId = (String) id;
}
}
queueSet = makeQueueSet(sessionId, configuration.getPortDriversShouldContact(), configuration);
queueSet.setExtensionJs(extensionJs);
try {
launcher =
browserLauncherFactory.getBrowserLauncher(browserString, sessionId, configuration,
browserConfiguration);
} catch (InvalidBrowserExecutableException e) {
throw new RemoteCommandException(e.getMessage(), "");
}
sessionInfo = new BrowserSessionInfo(sessionId, browserString, startURL, launcher, queueSet);
SessionIdTracker.setLastSessionId(sessionId);
log.info("Allocated session " + sessionId + " for " + startURL + ", launching...");
final PerSessionLogHandler perSessionLogHandler = LoggingManager.perSessionLogHandler();
perSessionLogHandler.attachToCurrentThread(new SessionId(sessionId));
try {
launcher.launchRemoteSession(startURL);
queueSet.waitForLoad(configuration.getTimeoutInSeconds() * 1000L);
// TODO DGF log4j only
// NDC.push("sessionId="+sessionId);
FrameGroupCommandQueueSet queue = getQueueSet(sessionId);
queue.doCommand("setContext", sessionId, "");
activeSessions.add(sessionInfo);
return sessionInfo;
} catch (Exception e) {
/*
* At this point the session might not have been added to neither available nor active
* sessions. This session is unlikely to be of any practical use so we need to make sure we
* close the browser and clear all session data.
*/
log.log(Level.SEVERE,
"Failed to start new browser session, shutdown browser and clear all session data", e);
shutdownBrowserAndClearSessionData(sessionInfo);
throw new RemoteCommandException("Error while launching browser", "", e);
} finally {
perSessionLogHandler.detachFromCurrentThread();
}
}
/**
* Adds a browser session that was not created by this factory to the set of active sessions.
*
* Allows for creation of unmanaged sessions (i.e. no FrameGroupCommandQueueSet) for task such as
* running the HTML tests (see HTMLLauncher.java). All fields other than session are required to
* be non-null.
*
* @param sessionInfo the session info to register.
* @return true if session was registered
*/
protected boolean registerExternalSession(BrowserSessionInfo sessionInfo) {
boolean result = false;
if (BrowserSessionInfo.isValid(sessionInfo)) {
activeSessions.add(sessionInfo);
result = true;
}
return result;
}
/**
* Removes a previously registered external browser session from the list of active sessions.
*
* @param sessionInfo the session to remove.
*/
protected void deregisterExternalSession(BrowserSessionInfo sessionInfo) {
activeSessions.remove(sessionInfo);
}
/**
* Looks up a session in the named set by session id
*
* @param sessionId the session id to find
* @param set the Set to inspect
* @return the matching BrowserSessionInfo or null if not found.
*/
protected BrowserSessionInfo lookupInfoBySessionId(String sessionId,
Set set) {
BrowserSessionInfo result = null;
synchronized (set) {
for (BrowserSessionInfo info : set) {
if (info.sessionId.equals(sessionId)) {
result = info;
break;
}
}
}
return result;
}
/**
* Looks up a session in the named set by browser string and base URL
*
* @param browserString the browser string to match
* @param baseUrl the base URL to match.
* @param set the Set to inspect
* @return the matching BrowserSessionInfo or null if not found.
*/
protected BrowserSessionInfo lookupInfoByBrowserAndUrl(String browserString,
String baseUrl, Set set) {
BrowserSessionInfo result = null;
synchronized (set) {
for (BrowserSessionInfo info : set) {
if (info.browserString.equals(browserString)
&& info.baseUrl.equals(baseUrl)) {
result = info;
break;
}
}
}
return result;
}
protected void removeIdleAvailableSessions() {
long now = System.currentTimeMillis();
synchronized (availableSessions) {
Iterator iter = availableSessions.iterator();
while (iter.hasNext()) {
BrowserSessionInfo info = iter.next();
if (now - info.lastClosedAt > maxIdleSessionTime) {
iter.remove();
shutdownBrowserAndClearSessionData(info);
}
}
}
}
/**
* for testing only
* @param sessionId session id
* @return true if it has one
*/
protected boolean hasActiveSession(String sessionId) {
BrowserSessionInfo info = lookupInfoBySessionId(sessionId, activeSessions);
return (null != info);
}
/**
* for testing only
* @param sessionId session id
* @return true if it has one
*/
protected boolean hasAvailableSession(String sessionId) {
BrowserSessionInfo info = lookupInfoBySessionId(sessionId, availableSessions);
return (null != info);
}
/**
* for testing only
* @param sessionInfo browser session info
*/
protected void addToAvailableSessions(BrowserSessionInfo sessionInfo) {
availableSessions.add(sessionInfo);
}
/**
* Collection class to hold the objects associated with a browser session.
*
* @author [email protected] (Jennifer Bevan)
*/
public static class BrowserSessionInfo {
public BrowserSessionInfo(String sessionId, String browserString,
String baseUrl, BrowserLauncher launcher,
FrameGroupCommandQueueSet session) {
this.sessionId = sessionId;
this.browserString = browserString;
this.baseUrl = baseUrl;
this.launcher = launcher;
this.session = session; // optional field; may be null.
lastClosedAt = 0;
}
public final String sessionId;
public final String browserString;
public final String baseUrl;
public final BrowserLauncher launcher;
public final FrameGroupCommandQueueSet session;
public long lastClosedAt;
/**
* Browser sessions require the session id, the browser, the base URL, and the launcher. They
* don't actually require the session to be set up as a FrameGroupCommandQueueSet.
*
* @param sessionInfo the sessionInfo to validate.
* @return true if all fields excepting session are non-null.
*/
protected static boolean isValid(BrowserSessionInfo sessionInfo) {
boolean result = (null != sessionInfo.sessionId
&& null != sessionInfo.browserString
&& null != sessionInfo.baseUrl
&& null != sessionInfo.launcher);
return result;
}
}
/**
* TimerTask that looks for unused sessions in the availableSessions collection.
*
* @author [email protected] (Jennifer Bevan)
*/
protected class CleanupTask extends TimerTask {
@Override
public void run() {
removeIdleAvailableSessions();
}
}
}