Index: server-coreless/src/test/java/org/openqa/selenium/server/BrowserSessionFactoryTest.java =================================================================== --- server-coreless/src/test/java/org/openqa/selenium/server/BrowserSessionFactoryTest.java (revision 0) +++ server-coreless/src/test/java/org/openqa/selenium/server/BrowserSessionFactoryTest.java (revision 0) @@ -0,0 +1,204 @@ +package org.openqa.selenium.server; + +import junit.framework.TestCase; + +import org.openqa.selenium.server.BrowserSessionFactory.BrowserSessionInfo; +import org.openqa.selenium.server.browserlaunchers.BrowserLauncher; +import org.openqa.selenium.server.FrameGroupCommandQueueSet; + +import java.util.HashSet; +import java.util.Set; + +public class BrowserSessionFactoryTest extends TestCase { + + private static final String SESSION_ID_1 = "testLookupByBrowserAndUrl1"; + private static final String BROWSER_1 = "*firefox"; + private static final String BASEURL1 = "http://www.google.com"; + + private static final String SESSION_ID_2 = "testLookupByBrowserAndUrl2"; + private static final String BROWSER2 = "*firefox"; + private static final String BASEURL2 = "http://maps.google.com"; + + public void testIsValidWithInvalidSessionInfo() { + BrowserSessionInfo info = new BrowserSessionInfo("id1", "*firefox", + null, null, null); + } + + public void testLookupByBrowserAndUrl() { + BrowserSessionFactory factory = getTestSessionFactory(); + Set infos = getTestSessionSet(); + BrowserSessionInfo result = factory.lookupInfoByBrowserAndUrl( + BROWSER_1, BASEURL1, infos); + assertEquals(SESSION_ID_1, result.sessionId); + } + + public void testLookupByBrowserAndUrlWithNoMatch() { + BrowserSessionFactory factory = getTestSessionFactory(); + Set infos = getTestSessionSet(); + BrowserSessionInfo result = factory.lookupInfoByBrowserAndUrl( + BROWSER_1, "fooey", infos); + assertNull(result); + } + + public void testLookupBySessionId() { + BrowserSessionFactory factory = getTestSessionFactory(); + Set infos = getTestSessionSet(); + BrowserSessionInfo result = factory.lookupInfoBySessionId( + SESSION_ID_2, infos); + assertEquals(BASEURL2, result.baseUrl); + } + + public void testLookupBySessionIdWithNoMatch() { + BrowserSessionFactory factory = getTestSessionFactory(); + Set infos = getTestSessionSet(); + BrowserSessionInfo result = factory.lookupInfoBySessionId( + "fooey", infos); + assertNull(result); + } + + public void testRegisterValidExternalSession() { + BrowserSessionFactory factory = getTestSessionFactory(); + BrowserSessionInfo info1 = getTestSession1(); + factory.registerExternalSession(info1); + assertTrue(factory.hasActiveSession(info1.sessionId)); + } + + public void testRegisterInValidExternalSession() { + BrowserSessionFactory factory = getTestSessionFactory(); + BrowserSessionInfo info = new BrowserSessionInfo(SESSION_ID_1, "*firefox", + null, null, null); + factory.registerExternalSession(info); + assertFalse(factory.hasActiveSession(info.sessionId)); + } + + public void testGrabAvailableSession() { + BrowserSessionFactory factory = getTestSessionFactory(); + factory.addToAvailableSessions(getTestSession1()); + assertTrue(factory.hasAvailableSession(SESSION_ID_1)); + assertFalse(factory.hasActiveSession(SESSION_ID_1)); + BrowserSessionInfo result = factory.grabAvailableSession(BROWSER_1, BASEURL1); + assertEquals(SESSION_ID_1, result.sessionId); + assertFalse(factory.hasAvailableSession(SESSION_ID_1)); + assertTrue(factory.hasActiveSession(SESSION_ID_1)); + } + + public void testEndSessionWithNoCaching() { + BrowserSessionFactory factory = getTestSessionFactory(); + factory.registerExternalSession(getTestSession1()); + assertTrue(factory.hasActiveSession(SESSION_ID_1)); + factory.endBrowserSession(SESSION_ID_1, false); + assertFalse(factory.hasActiveSession(SESSION_ID_1)); + assertFalse(factory.hasAvailableSession(SESSION_ID_1)); + } + + public void testEndSessionWithCaching() { + BrowserSessionFactory factory = getTestSessionFactory(); + factory.registerExternalSession(getTestSession1()); + assertTrue(factory.hasActiveSession(SESSION_ID_1)); + long closingTime = System.currentTimeMillis(); + factory.endBrowserSession(SESSION_ID_1, true); + assertFalse(factory.hasActiveSession(SESSION_ID_1)); + assertTrue(factory.hasAvailableSession(SESSION_ID_1)); + BrowserSessionInfo info = factory.lookupInfoBySessionId(SESSION_ID_1, + factory.availableSessions); + assertTrue(info.lastClosedAt >= closingTime); + } + + public void testEndAllBrowserSessions() { + BrowserSessionFactory factory = getTestSessionFactory(); + factory.registerExternalSession(getTestSession1()); + factory.addToAvailableSessions(getTestSession2()); + factory.endAllBrowserSessions(); + assertFalse(factory.hasActiveSession(SESSION_ID_1)); + assertFalse(factory.hasAvailableSession(SESSION_ID_2)); + assertFalse(factory.hasAvailableSession(SESSION_ID_1)); + } + + public void testRemoveIdleAvailableSessions() { + BrowserSessionFactory factory = getTestSessionFactory(); + factory.addToAvailableSessions(getTestSession1()); + assertTrue(factory.hasAvailableSession(SESSION_ID_1)); + factory.removeIdleAvailableSessions(); + assertFalse(factory.hasAvailableSession(SESSION_ID_1)); + } + + public void testRemoveIdleAvailableSessionsViaCleanup() { + BrowserSessionFactory factory = new BrowserSessionFactory(null, 5, 0, true); + BrowserSessionInfo info1 = getTestSession1(); + info1.lastClosedAt = 0; // very idle. + factory.addToAvailableSessions(info1); + FrameGroupCommandQueueSet.sleepForAtLeast(5); + assertFalse(factory.hasAvailableSession(SESSION_ID_1)); + } + + private Set getTestSessionSet() { + Set infos = new HashSet(); + BrowserSessionInfo info1 = getTestSession1(); + infos.add(info1); + BrowserSessionInfo info2 = getTestSession2(); + infos.add(info2); + return infos; + } + + private BrowserSessionInfo getTestSession1() { + DummyLauncher mockLauncher1 = new DummyLauncher(); + BrowserSessionInfo info1 = new BrowserSessionInfo( + SESSION_ID_1, BROWSER_1, BASEURL1, mockLauncher1, null); + return info1; + } + + private BrowserSessionInfo getTestSession2() { + DummyLauncher mockLauncher2 = new DummyLauncher(); + BrowserSessionInfo info2 = new BrowserSessionInfo( + SESSION_ID_2, BROWSER2, BASEURL2, mockLauncher2, null); + return info2; + } + + private BrowserSessionFactory getTestSessionFactory() { + return new BrowserSessionFactory(null, 0, 0, false); + } + + /** + * A teeny tiny no-op launcher to get a non-null launcher for testing. + * + * @author jbevan@google.com (Jennifer Bevan) + */ + private static class DummyLauncher implements BrowserLauncher { + + private boolean closed; + + public DummyLauncher() { + closed = true; + } + + /** noop */ + public void close() { + closed = true; + } + + /** noop */ + public Process getProcess() { + return null; + } + + /** noop */ + public void launchHTMLSuite(String startURL, String suiteUrl, + boolean multiWindow, String defaultLogLevel) { + closed = false; + } + + /** noop */ + public void launchRemoteSession(String url, boolean multiWindow) { + closed = false; + } + + protected boolean isClosed() { + return closed; + } + + protected void setOpen() { + closed = false; + } + } + +} Index: server-coreless/src/test/java/org/openqa/selenium/UnitTestSuite.java =================================================================== --- server-coreless/src/test/java/org/openqa/selenium/UnitTestSuite.java (revision 2199) +++ server-coreless/src/test/java/org/openqa/selenium/UnitTestSuite.java (working copy) @@ -3,6 +3,8 @@ import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; + +import org.openqa.selenium.server.BrowserSessionFactoryTest; import org.openqa.selenium.server.ClasspathResourceLocatorTest; import org.openqa.selenium.server.CommandHolderTest; import org.openqa.selenium.server.CommandQueueTest; @@ -30,6 +32,7 @@ suite.addTestSuite(CommandHolderTest.class); suite.addTestSuite(CommandResultHolderTest.class); suite.addTestSuite(CommandQueueTest.class); + suite.addTestSuite(BrowserSessionFactoryTest.class); suite.addTestSuite(SeleniumServerTest.class); suite.addTestSuite(ClasspathResourceLocatorTest.class); suite.addTestSuite(FrameGroupCommandQueueTest.class); Index: server-coreless/src/main/java/org/openqa/selenium/server/htmlrunner/HTMLLauncher.java =================================================================== --- server-coreless/src/main/java/org/openqa/selenium/server/htmlrunner/HTMLLauncher.java (revision 2199) +++ server-coreless/src/main/java/org/openqa/selenium/server/htmlrunner/HTMLLauncher.java (working copy) @@ -19,6 +19,7 @@ import org.openqa.selenium.server.SeleniumCommandTimedOutException; import org.openqa.selenium.server.SeleniumServer; import org.openqa.selenium.server.StaticContentHandler; +import org.openqa.selenium.server.BrowserSessionFactory.BrowserSessionInfo; import org.openqa.selenium.server.browserlaunchers.AsyncExecute; import org.openqa.selenium.server.browserlaunchers.BrowserLauncher; import org.openqa.selenium.server.browserlaunchers.BrowserLauncherFactory; @@ -48,7 +49,7 @@ * @param browserURL - the start URL for the browser * @param suiteURL - the relative URL to the HTML suite * @param outputFile - The file to which we'll output the HTML results - * @param timeoutInMs - the amount of time (in milliseconds) to wait for the browser to finish + * @param timeoutInSeconds - the amount of time (in seconds) to wait for the browser to finish * @param multiWindow TODO * @return PASS or FAIL * @throws IOException if we can't write the output file @@ -66,7 +67,7 @@ * @param outputFile - The file to which we'll output the HTML results * @param multiWindow TODO * @param defaultLogLevel TODO - * @param timeoutInMs - the amount of time (in milliseconds) to wait for the browser to finish + * @param timeoutInSeconds - the amount of time (in seconds) to wait for the browser to finish * @return PASS or FAIL * @throws IOException if we can't write the output file */ @@ -84,7 +85,11 @@ BrowserLauncherFactory blf = new BrowserLauncherFactory(); String sessionId = Long.toString(System.currentTimeMillis() % 1000000); BrowserLauncher launcher = blf.getBrowserLauncher(browser, sessionId); - server.registerBrowserLauncher(sessionId, launcher); + BrowserSessionInfo sessionInfo = new BrowserSessionInfo(sessionId, + browser, browserURL, launcher, null); + server.registerBrowserSession(sessionInfo); + + // JB: -- aren't these URLs in the wrong order according to declaration? launcher.launchHTMLSuite(suiteURL, browserURL, multiWindow, defaultLogLevel); long now = System.currentTimeMillis(); long end = now + timeoutInMs; @@ -92,6 +97,7 @@ AsyncExecute.sleepTight(500); } launcher.close(); + server.deregisterBrowserSession(sessionInfo); if (results == null) { throw new SeleniumCommandTimedOutException(); } @@ -110,7 +116,7 @@ * @param browserURL - the start URL for the browser * @param suiteFile - a file containing the HTML suite to run * @param outputFile - The file to which we'll output the HTML results - * @param timeoutInMs - the amount of time (in milliseconds) to wait for the browser to finish + * @param timeoutInSeconds - the amount of time (in seconds) to wait for the browser to finish * @param multiWindow - whether to run the browser in multiWindow or else framed mode * @return PASSED or FAIL * @throws IOException if we can't write the output file Index: server-coreless/src/main/java/org/openqa/selenium/server/FrameGroupCommandQueueSet.java =================================================================== --- server-coreless/src/main/java/org/openqa/selenium/server/FrameGroupCommandQueueSet.java (revision 2199) +++ server-coreless/src/main/java/org/openqa/selenium/server/FrameGroupCommandQueueSet.java (working copy) @@ -17,8 +17,10 @@ */ +import java.io.File; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -33,6 +35,7 @@ import org.apache.commons.logging.Log; import org.mortbay.log.LogFactory; +import org.openqa.selenium.server.browserlaunchers.LauncherUtils; /** @@ -61,6 +64,7 @@ private FrameAddress currentFrameAddress = null; private String currentUniqueId = null; + private final Set tempFilesForSession = Collections.synchronizedSet(new HashSet()); private Map uniqueIdToCommandQueue = new ConcurrentHashMap(); static private final Map queueSets = new ConcurrentHashMap(); @@ -72,9 +76,11 @@ private AtomicInteger millisecondDelayBetweenOperations; /** - * A unique string denoting a session with a browser. In most cases this session begins with the - * selenium server configuring and starting a browser process, and ends with a selenium server killing - * that process. + * A unique string denoting a session with a browser. + * + * In most cases this session begins with the + * selenium server configuring and starting a browser process, and ends + * with a selenium server killing that process. */ private final String sessionId; /** @@ -693,6 +699,7 @@ * */ public void endOfLife() { + removeTemporaryFiles(); for (CommandQueue frameQ : uniqueIdToCommandQueue.values()) { frameQ.endOfLife(); } @@ -769,7 +776,7 @@ // orphanedQueues.clear(); // } - public void reset() { + public void reset(String baseUrl) { log.debug("resetting frame group"); if (SeleniumServer.isProxyInjectionMode()) { // shut down all but the primary top level connection @@ -798,15 +805,39 @@ uniqueIdToCommandQueue.remove(frameAddress); } } + removeTemporaryFiles(); selectWindow(DEFAULT_SELENIUM_WINDOW_NAME); - String defaultUrl = "http://localhost:" + SeleniumServer.getPortDriversShouldContact() - + "/selenium-server/core/InjectedRemoteRunner.html"; + // String defaultUrl = "http://localhost:" + StringBuilder openUrl = new StringBuilder(); + if (SeleniumServer.isProxyInjectionMode()) { + openUrl.append("http://localhost:"); + openUrl.append(SeleniumServer.getPortDriversShouldContact()); + openUrl.append("/selenium-server/core/InjectedRemoteRunner.html"); + } else { + openUrl.append(LauncherUtils.stripStartURL(baseUrl)); + openUrl.append("/selenium-server/core/RemoteRunner.html"); + } try { - doCommand("open", defaultUrl, ""); // will close out subframes + doCommand("open", openUrl.toString(), ""); // will close out subframes } catch (RemoteCommandException rce) { log.debug("RemoteCommandException in reset: " + rce.getMessage()); } } + + protected void removeTemporaryFiles() { + for (File file : tempFilesForSession) { + boolean deleteSuccessful = file.delete(); + if (!deleteSuccessful) { + log.warn("temp file for session " + sessionId + + " not deleted " + file.getAbsolutePath()); + } + } + tempFilesForSession.clear(); + } + + protected void addTemporaryFile(File tf) { + tempFilesForSession.add(tf); + } private boolean queueMatchesFrameAddress(CommandQueue queue, String currentLocalFrameAddress, String newFrameAddressExpression) { boolean result; Index: server-coreless/src/main/java/org/openqa/selenium/server/SeleniumDriverResourceHandler.java =================================================================== --- server-coreless/src/main/java/org/openqa/selenium/server/SeleniumDriverResourceHandler.java (revision 2199) +++ server-coreless/src/main/java/org/openqa/selenium/server/SeleniumDriverResourceHandler.java (working copy) @@ -34,8 +34,6 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -58,6 +56,7 @@ import org.mortbay.http.handler.ResourceHandler; import org.mortbay.log.LogFactory; import org.mortbay.util.StringUtil; +import org.openqa.selenium.server.BrowserSessionFactory.BrowserSessionInfo; import org.openqa.selenium.server.browserlaunchers.AsyncExecute; import org.openqa.selenium.server.browserlaunchers.BrowserLauncher; import org.openqa.selenium.server.browserlaunchers.BrowserLauncherFactory; @@ -77,17 +76,16 @@ public class SeleniumDriverResourceHandler extends ResourceHandler { static Log log = LogFactory.getLog(SeleniumDriverResourceHandler.class); static Log browserSideLog = LogFactory.getLog(SeleniumDriverResourceHandler.class.getName()+".browserSideLog"); - private final Map launchers = new HashMap(); - private final Map> sessionIdToListOfTempFiles = new HashMap>(); - + private SeleniumServer server; private static String lastSessionId = null; private Map domainsBySessionId = new HashMap(); - private Map sessionIdsToBrowserStrings = - Collections.synchronizedMap(new HashMap()); private StringBuffer logMessagesBuffer = new StringBuffer(); + private BrowserLauncherFactory browserLauncherFactory = new BrowserLauncherFactory(); - + private final BrowserSessionFactory browserSessionFactory = + new BrowserSessionFactory(browserLauncherFactory); + public SeleniumDriverResourceHandler(SeleniumServer server) { this.server = server; @@ -419,7 +417,8 @@ results = "OK," + logMessagesBuffer.toString(); logMessagesBuffer.setLength(0); } else if ("testComplete".equals(cmd)) { - results = endBrowserSession(sessionId, SeleniumServer.reusingBrowserSessions()); + browserSessionFactory.endBrowserSession(sessionId); + results = "OK"; } else if ("shutDown".equals(cmd) || "shutDownSeleniumServer".equals(cmd)) { results = null; shutDown(res); @@ -427,8 +426,7 @@ FrameGroupCommandQueueSet queue = FrameGroupCommandQueueSet.getQueueSet(sessionId); try { File downloadedFile = downloadFile(values.get(1)); - List tempFilesForSession = getTempFiles(sessionId); - tempFilesForSession.add(downloadedFile); + queue.addTemporaryFile(downloadedFile); results = queue.doCommand("type", values.get(0), downloadedFile.getAbsolutePath()); } catch (Exception e) { results = e.toString(); @@ -515,8 +513,9 @@ String browser = values.get(0); String newSessionId = generateNewSessionId(); BrowserLauncher simpleLauncher = browserLauncherFactory.getBrowserLauncher(browser, newSessionId); - server.registerBrowserLauncher(newSessionId, simpleLauncher); String baseUrl = "http://localhost:" + server.getPort(); + server.registerBrowserSession(new BrowserSessionInfo( + newSessionId, browser, baseUrl, simpleLauncher, null)); simpleLauncher.launchHTMLSuite("TestPrompt.html?thisIsSeleniumServer=true", baseUrl, false, "info"); results = "OK"; } @@ -553,6 +552,7 @@ throw new RuntimeException("Malformed URL <" + urlString + ">, " + e.getMessage()); } File outputFile = FileUtils.getFileUtils().createTempFile("se-",".file",null); + outputFile.deleteOnExit(); // to be on the safe side. Project p = new Project(); p.addBuildListener(new AntJettyLoggerBuildListener(log)); Get g = new Get(); @@ -629,47 +629,6 @@ } - private String endBrowserSession(String sessionId, boolean cacheUnused) { - if (cacheUnused) { - FrameGroupCommandQueueSet.getQueueSet(sessionId).reset(); - } - else { - BrowserLauncher launcher = getLauncher(sessionId); - List tempFilesForSession = getTempFiles(sessionId); - if (launcher == null) { - return "ERROR: No launcher found for sessionId " + sessionId; - } - if (tempFilesForSession == null) { - return "ERROR: Can't find temp file storage for sessionId " + sessionId; //TODO invariant violated, never should have null, as we set up a new ArrayList when creating the session - } - try { - launcher.close(); - } catch (RuntimeException re) { - throw re; - } finally { - // recover memory - synchronized(launchers) { - launchers.remove(sessionId); - } - FrameGroupCommandQueueSet.clearQueueSet(sessionId); - } - - try { - sessionIdToListOfTempFiles.remove(sessionId); - } catch(RuntimeException re) { - throw re; - } finally { - for (File file : tempFilesForSession) { - boolean deleteSuccessful = file.delete(); - if (! deleteSuccessful) { - log.warn("temp file not deleted " + file.getAbsolutePath()); - } - } - } - } - return "OK"; - } - private void warnIfApparentDomainChange(String sessionId, String url) { if (url.startsWith("http://")) { String urlDomain = url.replaceFirst("^(http://[^/]+, url)/.*", "$1"); @@ -691,65 +650,13 @@ protected String getNewBrowserSession(String browserString, String startURL) throws RemoteCommandException { - - browserString = validateBrowserString(browserString); - - if (SeleniumServer.isProxyInjectionMode()) { - InjectionHelper.init(); - } - - String sessionId = UUID.randomUUID().toString().replace("-", ""); - setLastSessionId(sessionId); - FrameGroupCommandQueueSet queueSet = FrameGroupCommandQueueSet.makeQueueSet(sessionId); - BrowserLauncher launcher = browserLauncherFactory.getBrowserLauncher(browserString, sessionId); - registerBrowserLauncher(sessionId, launcher); - sessionIdsToBrowserStrings.put(sessionId, browserString); - log.info("Allocated session " + sessionId + " for " + startURL + ", launching..."); - - boolean multiWindow = server.isMultiWindow(); - launcher.launchRemoteSession(startURL, multiWindow); - - try { - queueSet.waitForLoad(SeleniumServer.getTimeoutInSeconds() * 1000l); - - // TODO DGF log4j only - // NDC.push("sessionId="+sessionId); - FrameGroupCommandQueueSet queue = FrameGroupCommandQueueSet.getQueueSet(sessionId); - queue.doCommand("setContext", sessionId, ""); - return sessionId; - } catch (RemoteCommandException rce) { - log.debug("Failed to start new browser session: " + rce.getMessage()); - synchronized(launchers) { - endBrowserSession(sessionId, false); - } - sessionIdsToBrowserStrings.remove(sessionId); - throw rce; - } + BrowserSessionInfo sessionInfo = + browserSessionFactory.getNewBrowserSession( + browserString, startURL, server.isMultiWindow()); + setLastSessionId(sessionInfo.sessionId); + return sessionInfo.sessionId; } - private String validateBrowserString(String inputString) throws IllegalArgumentException { - String browserString = inputString; - if (SeleniumServer.getForcedBrowserMode()!=null) { - browserString = SeleniumServer.getForcedBrowserMode(); - log.info("overriding browser mode w/ forced browser mode setting: " + browserString); - } - if (SeleniumServer.isProxyInjectionMode() && browserString.equals("*iexplore")) { - log.warn("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 (SeleniumServer.isProxyInjectionMode() && browserString.equals("*firefox")) { - log.warn("running in proxy injection mode, but you used a *firefox 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; - } - /** Perl and Ruby hang forever when they see "Connection: close" in the HTTP headers. * They see that and they think that Jetty will close the socket connection, but * Jetty doesn't appear to do that reliably when we're creating a process while @@ -787,46 +694,38 @@ } } } - - /** Retrieves a launcher for the specified sessionId, or null if there is no such launcher. */ - private BrowserLauncher getLauncher(String sessionId) { - synchronized (launchers) { - return launchers.get(sessionId); - } + + /** + * Registers the given browser session among the active sessions + * to handle. + * + * Usually externally created browser sessions are managed themselves, + * but registering them allows the shutdown procedures to be simpler. + * + * @param sessionInfo the externally created browser session to register. + */ + public void registerBrowserSession(BrowserSessionInfo sessionInfo) { + browserSessionFactory.registerExternalSession(sessionInfo); } - /** Retrieves the temp files for the specified sessionId, or null if there are no such files. */ - private List getTempFiles(String sessionId) { - synchronized (sessionIdToListOfTempFiles) { - List list = sessionIdToListOfTempFiles.get(sessionId); - if (list == null) { // first time we call it - list = new ArrayList(); - sessionIdToListOfTempFiles.put(sessionId, list); - } - return sessionIdToListOfTempFiles.get(sessionId); - } + /** + * De-registers the given browser session from among the active sessions. + * + * When an externally managed but registered session is closed, + * this method should be called to keep the set of active sessions + * up to date. + * + * @param sessionInfo the session to deregister. + */ + public void deregisterBrowserSession(BrowserSessionInfo sessionInfo) { + browserSessionFactory.deregisterExternalSession(sessionInfo); } - - public void registerBrowserLauncher(String sessionId, BrowserLauncher launcher) { - synchronized (launchers) { - launchers.put(sessionId, launcher); - } - } /** Kills all running browsers */ public void stopAllBrowsers() { - synchronized(launchers) { - List sessions = new ArrayList(launchers.keySet()); - for (String sessionId : sessions) { - endBrowserSession(sessionId, false); - } - } + browserSessionFactory.endAllBrowserSessions(); } - public Map getLaunchers() { - return launchers; - } - /** Sets all the don't-cache headers on the HttpResponse */ private void setNoCacheHeaders(HttpResponse res) { res.setField(HttpFields.__CacheControl, "no-cache"); Index: server-coreless/src/main/java/org/openqa/selenium/server/SeleniumServer.java =================================================================== --- server-coreless/src/main/java/org/openqa/selenium/server/SeleniumServer.java (revision 2199) +++ server-coreless/src/main/java/org/openqa/selenium/server/SeleniumServer.java (working copy) @@ -44,8 +44,8 @@ import org.mortbay.http.handler.SecurityHandler; import org.mortbay.jetty.Server; import org.mortbay.log.LogFactory; +import org.openqa.selenium.server.BrowserSessionFactory.BrowserSessionInfo; import org.openqa.selenium.server.browserlaunchers.AsyncExecute; -import org.openqa.selenium.server.browserlaunchers.BrowserLauncher; import org.openqa.selenium.server.htmlrunner.HTMLLauncher; import org.openqa.selenium.server.htmlrunner.HTMLResultsListener; import org.openqa.selenium.server.htmlrunner.SeleniumHTMLRunnerResultsHandler; @@ -478,12 +478,6 @@ "valid in combination with -proxyInjectionMode"); System.exit(1); } - if (!isProxyInjectionMode() && reusingBrowserSessions()) { - usage("-reusingBrowserSessions only valid in combination with -proxyInjectionMode" + - " (because of the need for multiple domain support, which only -proxyInjectionMode" + - " provides)."); - System.exit(1); - } } private static String getArg(String[] args, int i) { @@ -935,15 +929,7 @@ } } } - - /** - * Returns a map of session IDs and their associated browser launchers for all active sessions. - * @return - */ - public Map getBrowserLaunchers() { - return driver.getLaunchers(); - } - + public int getPort() { return port; } @@ -969,11 +955,16 @@ return staticContentHandler.getResource(path).getInputStream(); } - /** Registers a running browser with a specific sessionID */ - public void registerBrowserLauncher(String sessionId, BrowserLauncher launcher) { - driver.registerBrowserLauncher(sessionId, launcher); + /** Registers a running browser session */ + public void registerBrowserSession(BrowserSessionInfo sessionInfo) { + driver.registerBrowserSession(sessionInfo); } + /** De-registers a previously registered running browser session */ + public void deregisterBrowserSession(BrowserSessionInfo sessionInfo) { + driver.deregisterBrowserSession(sessionInfo); + } + /** * Get the number of threads that the server will use to configure the embedded Jetty instance. * Index: server-coreless/src/main/java/org/openqa/selenium/server/BrowserSessionFactory.java =================================================================== --- server-coreless/src/main/java/org/openqa/selenium/server/BrowserSessionFactory.java (revision 0) +++ server-coreless/src/main/java/org/openqa/selenium/server/BrowserSessionFactory.java (revision 0) @@ -0,0 +1,487 @@ +package org.openqa.selenium.server; + +import org.apache.commons.logging.Log; +import org.mortbay.log.LogFactory; +import org.openqa.selenium.server.browserlaunchers.BrowserLauncher; +import org.openqa.selenium.server.browserlaunchers.BrowserLauncherFactory; + +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; + +/** + * 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 jbevan@google.com (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 + + static Log log = LogFactory.getLog(BrowserSessionFactory.class); + + // 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; + + 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; + 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 + * @param startURL + * @param multiWindow if a new session should be started in multiWindow mode + * @return the BrowserSessionInfo for the new browser session. + * @throws RemoteCommandException + */ + public BrowserSessionInfo getNewBrowserSession(String browserString, String startURL, + boolean multiWindow) throws RemoteCommandException { + return getNewBrowserSession(browserString, startURL, multiWindow, + SeleniumServer.reusingBrowserSessions(), + SeleniumServer.isEnsureCleanSession()); + } + + /** + * Gets a new browser session + * + * @param browserString + * @param startURL + * @param multiWindow if a new session should be started in multiWindow mode + * @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. + * @return the BrowserSessionInfo for the new browser session. + * @throws RemoteCommandException + */ + protected BrowserSessionInfo getNewBrowserSession(String browserString, + String startURL, boolean multiWindow, boolean useCached, boolean ensureClean) + throws RemoteCommandException { + + BrowserSessionInfo sessionInfo = null; + browserString = validateBrowserString(browserString); + + if (SeleniumServer.isProxyInjectionMode()) { + 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, + multiWindow, ensureClean); + } + + assert null != sessionInfo; + if (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. + */ + protected void endAllBrowserSessions() { + 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(sessionInfo.sessionId, false); + } + done = (0 == activeSessions.size() && 0 == availableSessions.size()); + allSessions.clear(); + } + } + + /** + * Ends a browser session, using SeleniumServer static fields to populate + * parameters. + * + * @param sessionId the id of the session to be ended + */ + public void endBrowserSession(String sessionId) { + endBrowserSession(sessionId, + SeleniumServer.reusingBrowserSessions(), + SeleniumServer.isEnsureCleanSession()); + } + + /** + * Ends a browser session, using SeleniumServer static fields to populate + * parameters. + * + * @param sessionId the id of the session to be ended + * @param cacheUnused if the session should be made available for reuse. + */ + public void endBrowserSession(String sessionId, boolean cacheUnused) { + endBrowserSession(sessionId, cacheUnused, + SeleniumServer.isEnsureCleanSession()); + } + + /** + * Ends a browser session. + * + * @param sessionId the id of the session to be ended + * @param cacheUnused if this session should be made available for reuse + * @param ensureClean if clean sessions (e.g. no leftover cookies) are required. + */ + protected void endBrowserSession(String sessionId, boolean cacheUnused, + boolean ensureClean) { + BrowserSessionInfo sessionInfo = lookupInfoBySessionId(sessionId, activeSessions); + if (null != sessionInfo) { + activeSessions.remove(sessionInfo); + try { + if (cacheUnused) { + 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); + } else { + endBrowserSession(sessionInfo); + } + } finally { + 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 && !cacheUnused) { + try { + availableSessions.remove(sessionInfo); + endBrowserSession(sessionInfo); + } finally { + 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 endBrowserSession(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) throws IllegalArgumentException { + String browserString = inputString; + if (SeleniumServer.getForcedBrowserMode()!=null) { + browserString = SeleniumServer.getForcedBrowserMode(); + log.info("overriding browser mode w/ forced browser mode setting: " + browserString); + } + if (SeleniumServer.isProxyInjectionMode() && browserString.equals("*iexplore")) { + log.warn("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 (SeleniumServer.isProxyInjectionMode() && browserString.equals("*firefox")) { + log.warn("running in proxy injection mode, but you used a *firefox 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; + } + + /** + * Creates and tries to open a new session. + * + * @param browserString + * @param startURL + * @param multiWindow if new session should be opened -multiWindow + * @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, boolean multiWindow, boolean ensureClean) + throws RemoteCommandException { + String sessionId = UUID.randomUUID().toString().replace("-", ""); + FrameGroupCommandQueueSet queueSet = FrameGroupCommandQueueSet.makeQueueSet(sessionId); + BrowserLauncher launcher = browserLauncherFactory.getBrowserLauncher(browserString, sessionId); + BrowserSessionInfo sessionInfo = new BrowserSessionInfo(sessionId, + browserString, startURL, launcher, queueSet); + log.info("Allocated session " + sessionId + " for " + startURL + ", launching..."); + + launcher.launchRemoteSession(startURL, multiWindow); + try { + queueSet.waitForLoad(SeleniumServer.getTimeoutInSeconds() * 1000l); + + // TODO DGF log4j only + // NDC.push("sessionId="+sessionId); + FrameGroupCommandQueueSet queue = FrameGroupCommandQueueSet.getQueueSet(sessionId); + queue.doCommand("setContext", sessionId, ""); + + activeSessions.add(sessionInfo); + return sessionInfo; + } catch (RemoteCommandException rce) { + log.debug("Failed to start new browser session: " + rce.getMessage()); + endBrowserSession(sessionId, false, ensureClean); + throw rce; + } + } + + /** + * 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. + */ + 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(); + } + } + } + } + + /** for testing only */ + protected boolean hasActiveSession(String sessionId) { + BrowserSessionInfo info = lookupInfoBySessionId(sessionId, activeSessions); + return (null != info); + } + + /** for testing only */ + protected boolean hasAvailableSession(String sessionId) { + BrowserSessionInfo info = lookupInfoBySessionId(sessionId, availableSessions); + return (null != info); + } + + /** for testing only */ + protected void addToAvailableSessions(BrowserSessionInfo sessionInfo) { + availableSessions.add(sessionInfo); + } + + /** + * Collection class to hold the objects associated with a browser session. + * + * @author jbevan@google.com (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 jbevan@google.com (Jennifer Bevan) + */ + protected class CleanupTask extends TimerTask { + @Override + public void run() { + removeIdleAvailableSessions(); + } + } + +}