code: // 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.manager;
  
  import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
  import static org.openqa.selenium.Platform.MAC;
  import static org.openqa.selenium.Platform.WINDOWS;
  
  import com.google.common.io.CharStreams;
  import java.io.File;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.InputStreamReader;
  import java.nio.charset.StandardCharsets;
  import java.nio.file.Files;
  import java.nio.file.Path;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.List;
  import java.util.Map;
  import java.util.logging.Level;
  import java.util.logging.Logger;
  import org.openqa.selenium.Beta;
  import org.openqa.selenium.Capabilities;
  import org.openqa.selenium.Platform;
  import org.openqa.selenium.Proxy;
  import org.openqa.selenium.WebDriverException;
  import org.openqa.selenium.json.Json;
  import org.openqa.selenium.json.JsonException;
  
  /**
   * This implementation is still in beta, and may change.
   *
   * <p>The Selenium-Manager binaries are distributed in a JAR file
   * (org.openqa.selenium:selenium-manager) for the Java binding language. Since these binaries are
   * compressed within these JAR, we need to serialize the proper binary for the current platform
   * (Windows, macOS, or Linux) as an executable file. To implement this we use a singleton pattern,
   * since this way, we have a single instance in the JVM, and we reuse the resulting binary for all
   * the calls to the Selenium Manager singleton during all the Java process lifetime, deleting the
   * binary (stored as a local temporal file) on runtime shutdown.
   */
  @Beta
  public class SeleniumManager {
  
    private static final Logger LOG = Logger.getLogger(SeleniumManager.class.getName());
  
    private static final String SELENIUM_MANAGER = "selenium-manager";
    private static final String EXE = ".exe";
    private static final String INFO = "INFO";
    private static final String WARN = "WARN";
    private static final String DEBUG = "DEBUG";
  
    private static SeleniumManager manager;
  
    private File binary;
  
    /** Wrapper for the Selenium Manager binary. */
    private SeleniumManager() {
      Runtime.getRuntime()
          .addShutdownHook(
              new Thread(
                  () -> {
                    if (binary != null && binary.exists()) {
                      try {
                        Files.delete(binary.toPath());
                      } catch (IOException e) {
                        LOG.warning(
                            String.format(
                                "%s deleting temporal file: %s",
                                e.getClass().getSimpleName(), e.getMessage()));
                      }
                    }
                  }));
    }
  
    public static SeleniumManager getInstance() {
      if (manager == null) {
        manager = new SeleniumManager();
      }
      return manager;
    }
  
    /**
     * Executes a process with the given arguments.
     *
     * @param command the file and arguments to execute.
     * @return the standard output of the execution.
     */
    private static String runCommand(String... command) {
      LOG.fine(String.format("Executing Process: %s", Arrays.toString(command)));
      String output;
      int code;
      try {
        Process process = new ProcessBuilder(command).redirectErrorStream(true).start();
        process.waitFor();
        code = process.exitValue();
        output =
            CharStreams.toString(
                new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new WebDriverException(
            "Interrupted while running command: " + Arrays.toString(command), e);
      } catch (Exception e) {
        throw new WebDriverException("Failed to run command: " + Arrays.toString(command), e);
      }
      SeleniumManagerJsonOutput jsonOutput = null;
      JsonException failedToParse = null;
      String dump = output;
      if (!output.isEmpty()) {
        try {
          jsonOutput = new Json().toType(output, SeleniumManagerJsonOutput.class);
          jsonOutput.logs.forEach(
              logged -> {
                if (logged.level.equalsIgnoreCase(WARN)) {
                  LOG.warning(logged.message);
                }
                if (logged.level.equalsIgnoreCase(DEBUG) || logged.level.equalsIgnoreCase(INFO)) {
                  LOG.fine(logged.message);
                }
              });
          dump = jsonOutput.result.message;
        } catch (JsonException e) {
          failedToParse = e;
        }
      }
      if (code != 0) {
        throw new WebDriverException(
            "Command failed with code: "
                + code
                + ", executed: "
                + Arrays.toString(command)
                + "\n"
                + dump,
            failedToParse);
      } else if (failedToParse != null) {
        throw new WebDriverException(
            "Failed to parse json output, executed: " + Arrays.toString(command) + "\n" + dump,
            failedToParse);
      }
      return jsonOutput.result.message;
    }
  
    /**
     * Determines the correct Selenium Manager binary to use.
     *
     * @return the path to the Selenium Manager binary.
     */
    private synchronized File getBinary() {
      if (binary == null) {
        try {
          Platform current = Platform.getCurrent();
          String folder = "linux";
          String extension = "";
          if (current.is(WINDOWS)) {
            extension = EXE;
            folder = "windows";
          } else if (current.is(MAC)) {
            folder = "macos";
          }
          String binaryPath = String.format("%s/%s%s", folder, SELENIUM_MANAGER, extension);
          try (InputStream inputStream = this.getClass().getResourceAsStream(binaryPath)) {
            Path tmpPath = Files.createTempDirectory(SELENIUM_MANAGER + System.nanoTime());
            File tmpFolder = tmpPath.toFile();
            tmpFolder.deleteOnExit();
            binary = new File(tmpFolder, SELENIUM_MANAGER + extension);
            Files.copy(inputStream, binary.toPath(), REPLACE_EXISTING);
          }
          binary.setExecutable(true);
        } catch (Exception e) {
          throw new WebDriverException("Unable to obtain Selenium Manager Binary", e);
        }
      }
      LOG.fine(String.format("Selenium Manager binary found at: %s", binary));
  
      return binary;
    }
  
    /**
     * Returns the browser binary path when present in the vendor options
     *
     * @param options browser options used to start the session
     * @return the browser binary path when present, only Chrome/Firefox/Edge
     */
    private String getBrowserBinary(Capabilities options) {
      List<String> vendorOptionsCapabilities =
          Arrays.asList("moz:firefoxOptions", "goog:chromeOptions", "ms:edgeOptions");
      for (String vendorOptionsCapability : vendorOptionsCapabilities) {
        if (options.asMap().containsKey(vendorOptionsCapability)) {
          try {
            @SuppressWarnings("unchecked")
            Map<String, Object> vendorOptions =
                (Map<String, Object>) options.getCapability(vendorOptionsCapability);
            return (String) vendorOptions.get("binary");
          } catch (Exception e) {
            LOG.warning(
                String.format(
                    "Exception while retrieving the browser binary path. %s: %s",
                    options, e.getMessage()));
          }
        }
      }
      return null;
    }
  
    /**
     * Determines the location of the correct driver.
     *
     * @param options Browser Options instance.
     * @return the location of the driver.
     */
    public String getDriverPath(Capabilities options) {
      LOG.fine("Applicable driver not found; attempting to install with Selenium Manager (Beta)");
      File binaryFile = getBinary();
      if (binaryFile == null) {
        return null;
      }
      List<String> commandList = new ArrayList<>();
      commandList.add(binaryFile.getAbsolutePath());
      commandList.add("--browser");
      commandList.add(options.getBrowserName());
      commandList.add("--output");
      commandList.add("json");
  
      if (!options.getBrowserVersion().isEmpty()) {
        commandList.add("--browser-version");
        commandList.add(options.getBrowserVersion());
      }
  
      String browserBinary = getBrowserBinary(options);
      if (browserBinary != null && !browserBinary.isEmpty()) {
        commandList.add("--browser-path");
        commandList.add(browserBinary);
      }
  
      if (getLogLevel().intValue() <= Level.FINE.intValue()) {
        commandList.add("--debug");
      }
  
      Proxy proxy = (Proxy) options.getCapability("proxy");
      if (proxy != null) {
        if (proxy.getSslProxy() != null) {
          commandList.add("--proxy");
          commandList.add(proxy.getSslProxy());
        } else if (proxy.getHttpProxy() != null) {
          commandList.add("--proxy");
          commandList.add(proxy.getHttpProxy());
        }
      }
  
      String path = runCommand(commandList.toArray(new String[0]));
      LOG.fine(String.format("Using driver at location: %s", path));
      return path;
    }
  
    private Level getLogLevel() {
      Level level = LOG.getLevel();
      if (level == null && LOG.getParent() != null) {
        level = LOG.getParent().getLevel();
      }
      if (level == null) {
        return Level.INFO;
      }
      return level;
    }
  }
  