Java tutorial
// 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.firefox.xpi; import static java.util.concurrent.TimeUnit.SECONDS; import static org.openqa.selenium.firefox.FirefoxOptions.FIREFOX_OPTIONS; import static org.openqa.selenium.firefox.FirefoxProfile.PORT_PREFERENCE; import com.google.auto.service.AutoService; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.io.ByteStreams; import org.openqa.selenium.Capabilities; import org.openqa.selenium.Platform; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.firefox.ClasspathExtension; import org.openqa.selenium.firefox.Extension; import org.openqa.selenium.firefox.FileExtension; import org.openqa.selenium.firefox.FirefoxBinary; import org.openqa.selenium.firefox.FirefoxDriver; import org.openqa.selenium.firefox.FirefoxDriverService; import org.openqa.selenium.firefox.FirefoxOptions; import org.openqa.selenium.firefox.FirefoxProfile; import org.openqa.selenium.io.FileHandler; import org.openqa.selenium.net.UrlChecker; import org.openqa.selenium.os.CommandLine; import org.openqa.selenium.remote.service.DriverService; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; import java.util.stream.Stream; public class XpiDriverService extends FirefoxDriverService { private static final String NO_FOCUS_LIBRARY_NAME = "x_ignore_nofocus.so"; private static final String PATH_PREFIX = "/" + XpiDriverService.class.getPackage().getName().replace(".", "/") + "/"; private final Lock lock = new ReentrantLock(); private final int port; private final FirefoxBinary binary; private final FirefoxProfile profile; private File profileDir; private XpiDriverService(File executable, int port, ImmutableList<String> args, ImmutableMap<String, String> environment, FirefoxBinary binary, FirefoxProfile profile, File logFile) throws IOException { super(executable, port, args, environment); Preconditions.checkState(port > 0, "Port must be set"); this.port = port; this.binary = binary; this.profile = profile; String firefoxLogFile = System.getProperty(FirefoxDriver.SystemProperty.BROWSER_LOGFILE); if (firefoxLogFile != null) { // System property has higher precedence if ("/dev/stdout".equals(firefoxLogFile)) { sendOutputTo(System.out); } else if ("/dev/stderr".equals(firefoxLogFile)) { sendOutputTo(System.err); } else if ("/dev/null".equals(firefoxLogFile)) { sendOutputTo(ByteStreams.nullOutputStream()); } else { // TODO: This stream is leaked. sendOutputTo(new FileOutputStream(firefoxLogFile)); } } else { if (logFile != null) { // TODO: This stream is leaked. sendOutputTo(new FileOutputStream(logFile)); } else { sendOutputTo(ByteStreams.nullOutputStream()); } } } @Override protected URL getUrl(int port) throws MalformedURLException { return new URL("http", "localhost", port, "/hub"); } @Override public void start() throws IOException { lock.lock(); try { profile.setPreference(PORT_PREFERENCE, port); addWebDriverExtension(profile); profileDir = profile.layoutOnDisk(); binary.setOutputWatcher(getOutputStream()); ImmutableMap.Builder<String, String> envBuilder = new ImmutableMap.Builder<String, String>() .putAll(getEnvironment()).put("XRE_PROFILE_PATH", profileDir.getAbsolutePath()) .put("MOZ_NO_REMOTE", "1").put("MOZ_CRASHREPORTER_DISABLE", "1") // Disable Breakpad .put("NO_EM_RESTART", "1"); // Prevent the binary from detaching from the console if (Platform.getCurrent().is(Platform.LINUX) && profile.shouldLoadNoFocusLib()) { modifyLinkLibraryPath(envBuilder, profileDir); } Map<String, String> env = envBuilder.build(); List<String> cmdArray = new ArrayList<>(getArgs()); cmdArray.addAll(binary.getExtraOptions()); cmdArray.add("-foreground"); process = new CommandLine(binary.getPath(), Iterables.toArray(cmdArray, String.class)); process.setEnvironmentVariables(env); process.updateDynamicLibraryPath(env.get(CommandLine.getLibraryPathPropertyName())); // On Snow Leopard, beware of problems the sqlite library if (!(Platform.getCurrent().is(Platform.MAC) && Platform.getCurrent().getMinorVersion() > 5)) { String firefoxLibraryPath = System.getProperty(FirefoxDriver.SystemProperty.BROWSER_LIBRARY_PATH, binary.getFile().getAbsoluteFile().getParentFile().getAbsolutePath()); process.updateDynamicLibraryPath(firefoxLibraryPath); } process.copyOutputTo(getActualOutputStream()); process.executeAsync(); waitUntilAvailable(); } finally { lock.unlock(); } } private void modifyLinkLibraryPath(ImmutableMap.Builder<String, String> envBuilder, File profileDir) { // Extract x_ignore_nofocus.so from x86, amd64 directories inside // the jar into a real place in the filesystem and change LD_LIBRARY_PATH // to reflect that. String existingLdLibPath = System.getenv("LD_LIBRARY_PATH"); // The returned new ld lib path is terminated with ':' String newLdLibPath = extractAndCheck(profileDir, NO_FOCUS_LIBRARY_NAME, PATH_PREFIX + "x86", PATH_PREFIX + "amd64"); if (existingLdLibPath != null && !existingLdLibPath.equals("")) { newLdLibPath += existingLdLibPath; } envBuilder.put("LD_LIBRARY_PATH", newLdLibPath); // Set LD_PRELOAD to x_ignore_nofocus.so - this will be taken automagically // from the LD_LIBRARY_PATH envBuilder.put("LD_PRELOAD", NO_FOCUS_LIBRARY_NAME); } private String extractAndCheck(File profileDir, String noFocusSoName, String jarPath32Bit, String jarPath64Bit) { // 1. Extract x86/x_ignore_nofocus.so to profile.getLibsDir32bit // 2. Extract amd64/x_ignore_nofocus.so to profile.getLibsDir64bit // 3. Create a new LD_LIB_PATH string to contain: // profile.getLibsDir32bit + ":" + profile.getLibsDir64bit Set<String> pathsSet = new HashSet<>(); pathsSet.add(jarPath32Bit); pathsSet.add(jarPath64Bit); StringBuilder builtPath = new StringBuilder(); for (String path : pathsSet) { try { FileHandler.copyResource(profileDir, getClass(), path + File.separator + noFocusSoName); } catch (IOException e) { if (Boolean.getBoolean("webdriver.development")) { System.err.println("Exception unpacking required library, but in development mode. Continuing"); } else { throw new WebDriverException(e); } } // End catch. String outSoPath = profileDir.getAbsolutePath() + File.separator + path; File file = new File(outSoPath, noFocusSoName); if (!file.exists()) { throw new WebDriverException("Could not locate " + path + ": " + "native events will not work."); } builtPath.append(outSoPath).append(":"); } return builtPath.toString(); } private OutputStream getActualOutputStream() throws FileNotFoundException { String firefoxLogFile = System.getProperty(FirefoxDriver.SystemProperty.BROWSER_LOGFILE); if (firefoxLogFile != null) { if ("/dev/stdout".equals(firefoxLogFile)) { return System.out; } return new FileOutputStream(firefoxLogFile); } return getOutputStream(); } @Override protected void waitUntilAvailable() throws MalformedURLException { try { // Use a longer timeout, because 45 seconds was the default timeout in the predecessor to // XpiDriverService. This has to wait for Firefox to start, not just a service, and some users // may be running tests on really slow machines. URL status = new URL(getUrl(port).toString() + "/status"); new UrlChecker().waitUntilAvailable(45, SECONDS, status); } catch (UrlChecker.TimeoutException e) { throw new WebDriverException("Timed out waiting 45 seconds for Firefox to start.", e); } } @Override public void stop() { lock.lock(); try { if (process != null) { process.destroy(); } profile.cleanTemporaryModel(); profile.clean(profileDir); } finally { lock.unlock(); } } private void addWebDriverExtension(FirefoxProfile profile) { if (profile.containsWebDriverExtension()) { return; } profile.addExtension("webdriver", loadCustomExtension().orElse(loadDefaultExtension())); } private Optional<Extension> loadCustomExtension() { String xpiProperty = System.getProperty(FirefoxDriver.SystemProperty.DRIVER_XPI_PROPERTY); if (xpiProperty != null) { File xpi = new File(xpiProperty); return Optional.of(new FileExtension(xpi)); } return Optional.empty(); } private static Extension loadDefaultExtension() { return new ClasspathExtension(FirefoxProfile.class, "/" + XpiDriverService.class.getPackage().getName().replace(".", "/") + "/webdriver.xpi"); } /** * Configures and returns a new {@link XpiDriverService} using the default configuration. In * this configuration, the service will use the firefox executable identified by the * {@link FirefoxDriver.SystemProperty#BROWSER_BINARY} system property on a free port. * * @return A new XpiDriverService using the default configuration. */ public static XpiDriverService createDefaultService() { try { return new Builder().build(); } catch (WebDriverException e) { throw new IllegalStateException(e.getMessage(), e.getCause()); } } @SuppressWarnings("unchecked") static XpiDriverService createDefaultService(Capabilities caps) { Builder builder = new Builder().usingAnyFreePort(); Stream.<Supplier<FirefoxProfile>>of(() -> (FirefoxProfile) caps.getCapability(FirefoxDriver.PROFILE), () -> { try { return FirefoxProfile.fromJson((String) caps.getCapability(FirefoxDriver.PROFILE)); } catch (IOException ex) { throw new RuntimeException(ex); } }, ((FirefoxOptions) caps)::getProfile, () -> (FirefoxProfile) ((Map<String, Object>) caps.getCapability(FIREFOX_OPTIONS)).get("profile"), () -> { try { return FirefoxProfile .fromJson((String) ((Map<String, Object>) caps.getCapability(FIREFOX_OPTIONS)) .get("profile")); } catch (IOException ex) { throw new RuntimeException(ex); } }, () -> { Map<String, Object> options = (Map<String, Object>) caps.getCapability(FIREFOX_OPTIONS); FirefoxProfile toReturn = new FirefoxProfile(); ((Map<String, Object>) options.get("prefs")).forEach((key, value) -> { if (value instanceof Boolean) { toReturn.setPreference(key, (Boolean) value); } if (value instanceof Integer) { toReturn.setPreference(key, (Integer) value); } if (value instanceof String) { toReturn.setPreference(key, (String) value); } }); return toReturn; }).map(supplier -> { try { return supplier.get(); } catch (Exception e) { return null; } }).filter(Objects::nonNull).findFirst().ifPresent(builder::withProfile); Object binary = caps.getCapability(FirefoxDriver.BINARY); if (binary != null) { FirefoxBinary actualBinary; if (binary instanceof FirefoxBinary) { actualBinary = (FirefoxBinary) binary; } else if (binary instanceof String) { actualBinary = new FirefoxBinary(new File(String.valueOf(binary))); } else { throw new IllegalArgumentException("Expected binary to be a string or a binary: " + binary); } builder.withBinary(actualBinary); } return builder.build(); } public static Builder builder() { return new Builder(); } @AutoService(DriverService.Builder.class) public static class Builder extends FirefoxDriverService.Builder<XpiDriverService, XpiDriverService.Builder> { private FirefoxBinary binary = null; private FirefoxProfile profile = null; @Override protected boolean isLegacy() { return true; } @Override public int score(Capabilities capabilites) { if (capabilites.is(FirefoxDriver.MARIONETTE)) { return 0; } int score = 0; if (capabilites.getCapability(FirefoxDriver.BINARY) != null) { score++; } if (capabilites.getCapability(FirefoxDriver.PROFILE) != null) { score++; } return score; } public Builder withBinary(FirefoxBinary binary) { this.binary = Preconditions.checkNotNull(binary); return this; } public Builder withProfile(FirefoxProfile profile) { this.profile = Preconditions.checkNotNull(profile); return this; } @Override protected FirefoxDriverService.Builder withOptions(FirefoxOptions options) { FirefoxProfile profile = options.getProfile(); if (profile == null) { profile = new FirefoxProfile(); options.setCapability(FirefoxDriver.PROFILE, profile); } withBinary(options.getBinary()); withProfile(profile); return this; } @Override protected File findDefaultExecutable() { if (binary == null) { return new FirefoxBinary().getFile(); } return binary.getFile(); } @Override protected ImmutableList<String> createArgs() { return ImmutableList.of("-foreground"); } @Override protected XpiDriverService createDriverService(File exe, int port, ImmutableList<String> args, ImmutableMap<String, String> environment) { try { return new XpiDriverService(exe, port, args, environment, binary == null ? new FirefoxBinary() : binary, profile == null ? new FirefoxProfile() : profile, getLogFile()); } catch (IOException e) { throw new WebDriverException(e); } } } }