Java tutorial
/* * Copyright 2006 ThoughtWorks, Inc. * * 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.atlassian.selenium.browsers.firefox; import org.openqa.selenium.Capabilities; import org.openqa.selenium.Platform; import org.openqa.selenium.browserlaunchers.Sleeper; import org.openqa.selenium.browserlaunchers.LauncherUtils; import org.openqa.selenium.browserlaunchers.Proxies; import org.openqa.selenium.browserlaunchers.locators.BrowserInstallation; import org.openqa.selenium.browserlaunchers.locators.CombinedFirefoxLocator; import org.openqa.selenium.os.CommandLine; import org.openqa.selenium.remote.BrowserType; import org.openqa.selenium.server.ApplicationRegistry; import org.openqa.selenium.server.RemoteControlConfiguration; import org.openqa.selenium.server.browserlaunchers.AbstractBrowserLauncher; import org.openqa.selenium.server.browserlaunchers.BrowserOptions; import org.openqa.selenium.server.browserlaunchers.InvalidBrowserExecutableException; import org.openqa.selenium.server.browserlaunchers.ResourceExtractor; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.logging.Level; import java.util.logging.Logger; /** * This is an override of the {@link org.openqa.selenium.server.browserlaunchers.FirefoxChromeLauncher} * class from selenium so we can control the display system property * to get xvfb to work. */ public class DisplayAwareFirefoxChromeLauncher extends AbstractBrowserLauncher { private static final Logger log = Logger.getLogger(DisplayAwareFirefoxChromeLauncher.class.getName()); private File customProfileDir = null; private boolean closed = false; private BrowserInstallation browserInstallation; private CommandLine process = null; private boolean changeMaxConnections = false; public DisplayAwareFirefoxChromeLauncher(Capabilities browserOptions, RemoteControlConfiguration configuration, String sessionId, String browserString) throws InvalidBrowserExecutableException { this(browserOptions, configuration, sessionId, ApplicationRegistry.instance().browserInstallationCache() .locateBrowserInstallation(BrowserType.CHROME, browserString, new CombinedFirefoxLocator())); if (browserInstallation == null) { throw new InvalidBrowserExecutableException("The specified path to the browser executable is invalid."); } } public DisplayAwareFirefoxChromeLauncher(Capabilities browserOptions, RemoteControlConfiguration configuration, String sessionId, BrowserInstallation browserInstallation) { super(sessionId, configuration, browserOptions); if (browserInstallation == null) { throw new InvalidBrowserExecutableException("The specified path to the browser executable is invalid."); } this.browserInstallation = browserInstallation; } /* * (non-Javadoc) * * @see * org.openqa.selenium.server.browserlaunchers.AbstractBrowserLauncher#launch(java.lang.String) */ @Override protected void launch(String url) { final String profilePath; final String homePage; try { homePage = new ChromeUrlConvert().convert(url); profilePath = makeCustomProfile(homePage); populateCustomProfileDirectory(profilePath); log.info("Launching Firefox..."); process = prepareCommand(browserInstallation.launcherFilePath(), "-profile", profilePath); process.setEnvironmentVariable("NO_EM_RESTART", "1"); process.executeAsync(); } catch (IOException e) { throw new RuntimeException(e); } } private void populateCustomProfileDirectory(String profilePath) { /* * The first time we launch Firefox with an empty profile directory, Firefox will launch itself, * populate the profile directory, then kill/relaunch itself, so our process handle goes out of * date. So, the first time we launch Firefox, we'll start it up at an URL that will immediately * shut itself down. */ CommandLine command = prepareCommand(browserInstallation.launcherFilePath(), "-profile", profilePath, "-silent"); command.setDynamicLibraryPath(browserInstallation.libraryPath()); log.info("Preparing Firefox profile..."); command.execute(); try { waitForFullProfileToBeCreated(20 * 1000); } catch (RuntimeException e) { command.destroy(); throw e; } } protected CommandLine prepareCommand(String... commands) { CommandLine command = new CommandLine(commands); command.setEnvironmentVariable("MOZ_NO_REMOTE", "1"); // don't set the library path on Snow Leopard Platform platform = Platform.getCurrent(); if (!platform.is(Platform.MAC) || ((platform.is(Platform.MAC)) && platform.getMajorVersion() <= 10 && platform.getMinorVersion() <= 5)) { command.setDynamicLibraryPath(browserInstallation.libraryPath()); } if (System.getProperty("DISPLAY") != null) { command.setEnvironmentVariable("DISPLAY", System.getProperty("DISPLAY")); } return command; } protected void createCustomProfileDir() { customProfileDir = LauncherUtils.createCustomProfileDir(sessionId); } protected void copyDirectory(File sourceDir, File destDir) { LauncherUtils.copyDirectory(sourceDir, destDir); } protected File initProfileTemplate() { File firefoxProfileTemplate; String relativeProfile = BrowserOptions.getProfile(browserConfigurationOptions); if (relativeProfile == null) { relativeProfile = ""; } File profilesLocation = getConfiguration().getProfilesLocation(); if (profilesLocation != null && !"".equals(relativeProfile)) { firefoxProfileTemplate = getFileFromParent(profilesLocation, relativeProfile); if (!firefoxProfileTemplate.exists()) { throw new RuntimeException( "The profile specified '" + firefoxProfileTemplate.getAbsolutePath() + "' does not exist"); } } else { firefoxProfileTemplate = BrowserOptions.getFile(browserConfigurationOptions, "firefoxProfileTemplate"); } if (firefoxProfileTemplate != null) { copyDirectory(firefoxProfileTemplate, customProfileDir); } return firefoxProfileTemplate; } protected void extractProfileFromJar() throws IOException { ResourceExtractor.extractResourcePath(getClass(), "/customProfileDirCUSTFFCHROME", customProfileDir); } protected void copySingleFileWithOverwrite(File sourceFile, File destFile) { LauncherUtils.copySingleFileWithOverwrite(sourceFile, destFile, true); } protected File getFileFromParent(final File parent, String child) { return new File(parent, child); } protected void copyCert8db(final File firefoxProfileTemplate) { // Make sure that cert8.db of firefoxProfileTemplate is stored into customProfileDir if (firefoxProfileTemplate != null) { File sourceCertFile = getFileFromParent(firefoxProfileTemplate, "cert8.db"); if (sourceCertFile.exists()) { File destCertFile = new File(customProfileDir, "cert8.db"); copySingleFileWithOverwrite(sourceCertFile, destCertFile); } } } protected void generatePacAndPrefJs(String homePage) throws IOException { browserConfigurationOptions = Proxies.setProxyRequired(browserConfigurationOptions, false); if (browserConfigurationOptions.is("captureNetworkTraffic") || browserConfigurationOptions.is("addCustomRequestHeaders") || browserConfigurationOptions.is("trustAllSSLCertificates")) { browserConfigurationOptions = Proxies.setProxyEverything(browserConfigurationOptions, true); browserConfigurationOptions = Proxies.setProxyRequired(browserConfigurationOptions, true); } LauncherUtils.generatePacAndPrefJs(customProfileDir, getPort(), homePage, changeMaxConnections, getTimeout(), browserConfigurationOptions); } private String makeCustomProfile(String homePage) throws IOException { createCustomProfileDir(); File firefoxProfileTemplate = initProfileTemplate(); extractProfileFromJar(); copyCert8db(firefoxProfileTemplate); copyRunnerHtmlFiles(); changeMaxConnections = browserConfigurationOptions.is("changeMaxConnections"); generatePacAndPrefJs(homePage); return customProfileDir.getAbsolutePath(); } private void copyRunnerHtmlFiles() { String guid = "{503A0CD4-EDC8-489b-853B-19E0BAA8F0A4}"; File extensionDir = new File(customProfileDir, "extensions/" + guid); File htmlDir = new File(extensionDir, "chrome"); htmlDir.mkdirs(); LauncherUtils.extractHTAFile(htmlDir, getPort(), "/core/TestRunner.html", "TestRunner.html"); LauncherUtils.extractHTAFile(htmlDir, getPort(), "/core/TestPrompt.html", "TestPrompt.html"); LauncherUtils.extractHTAFile(htmlDir, getPort(), "/core/RemoteRunner.html", "RemoteRunner.html"); } public void close() { if (closed) { return; } FileLockRemainedException fileLockException = null; if (process != null) { try { killFirefoxProcess(); } catch (FileLockRemainedException flre) { fileLockException = flre; } } if (customProfileDir != null) { try { removeCustomProfileDir(); } catch (RuntimeException e) { if (fileLockException != null) { log.log(Level.SEVERE, "Couldn't delete custom Firefox profile directory", e); log.severe("Perhaps caused by this exception:"); log.log(Level.SEVERE, "Perhaps caused by this exception:", fileLockException); throw new RuntimeException("Couldn't delete custom Firefox " + "profile directory, presumably because task kill failed; " + "see error LOGGER!", e); } throw e; } } closed = true; } /** * Wrapper to allow for stubbed-out testing * */ protected void removeCustomProfileDir() throws RuntimeException { LauncherUtils.deleteTryTryAgain(customProfileDir, 6); } /** * Wrapper to allow for stubbed-out testing * */ protected void killFirefoxProcess() throws FileLockRemainedException { log.info("Killing Firefox..."); int exitValue = process.destroy(); if (exitValue == 0) { log.warning("Firefox seems to have ended on its own (did we kill the real browser???)"); } waitForFileLockToGoAway(0, 500); } /** * Firefox knows it's running by using a "parent.lock" file in the profile directory. Wait for * this file to go away (and stay gone) * * @param timeout max time to wait for the file to go away * @param timeToWait minimum time to wait to make sure the file is gone * @throws FileLockRemainedException */ private void waitForFileLockToGoAway(long timeout, long timeToWait) throws FileLockRemainedException { File lock = new File(customProfileDir, "parent.lock"); for (long start = System.currentTimeMillis(); System.currentTimeMillis() < start + timeout;) { Sleeper.sleepTight(500); if (!lock.exists() && makeSureFileLockRemainsGone(lock, timeToWait)) { return; } } if (lock.exists()) { throw new FileLockRemainedException("Lock file still present! " + lock.getAbsolutePath()); } } /** * When initializing the profile, Firefox rapidly starts, stops, restarts and stops again; we need * to wait a bit to make sure the file lock is really gone. * * @param lock the parent.lock file in the profile directory * @param timeToWait minimum time to wait to see if the file shows back up again. This is not a * timeout; we will always wait this amount of time or more. * @return true if the file stayed gone for the entire timeToWait; false if the file exists (or * came back) */ private boolean makeSureFileLockRemainsGone(File lock, long timeToWait) { for (long start = System.currentTimeMillis(); System.currentTimeMillis() < start + timeToWait;) { Sleeper.sleepTight(500); if (lock.exists()) { return false; } } return !lock.exists(); } /** * Wait for one of the Firefox-generated files to come into existence, then wait for Firefox to * exit * * @param timeout the maximum amount of time to wait for the profile to be created */ private void waitForFullProfileToBeCreated(long timeout) { // This will be a characteristic file in the profile File testFile = new File(customProfileDir, "extensions.ini"); long start = System.currentTimeMillis(); for (; System.currentTimeMillis() < start + timeout;) { Sleeper.sleepTight(500); if (testFile.exists()) { break; } } if (!testFile.exists()) { throw new RuntimeException("Timed out waiting for profile to be created!"); } // wait the rest of the timeout for the file lock to go away long subTimeout = timeout - (System.currentTimeMillis() - start); try { waitForFileLockToGoAway(subTimeout, 500); } catch (FileLockRemainedException e) { throw new RuntimeException("Firefox refused shutdown while preparing a profile", e); } } // visible for testing protected void setCustomProfileDir(File value) { customProfileDir = value; } // visible for testing protected void setCommandLine(CommandLine p) { process = p; } protected class FileLockRemainedException extends Exception { FileLockRemainedException(String message) { super(message); } } public static class ChromeUrlConvert { public String convert(String httpUrl) throws MalformedURLException { String query = LauncherUtils.getQueryString(httpUrl); String file = new File(new URL(httpUrl).getPath()).getName(); return "chrome://src/content/" + file + "?" + query; } } @Override // need to specify an absolute resultsUrl public void launchHTMLSuite(String suiteUrl, String browserURL) { // If navigating to TestPrompt, use the baked-in version instead. if (suiteUrl != null && suiteUrl.startsWith("TestPrompt.html?")) { suiteUrl = suiteUrl.replaceFirst("^TestPrompt\\.html\\?", "chrome://src/content/TestPrompt.html?"); } launch(LauncherUtils.getDefaultHTMLSuiteUrl(browserURL, suiteUrl, (!BrowserOptions.isSingleWindow(browserConfigurationOptions)), getPort())); } @Override // need to specify an absolute driverUrl public void launchRemoteSession(String browserURL) { launch(LauncherUtils.getDefaultRemoteSessionUrl(browserURL, sessionId, (!BrowserOptions.isSingleWindow(browserConfigurationOptions)), getPort(), browserConfigurationOptions.is("browserSideLog"))); } }