package com.codeborne.selenide.impl;
import com.codeborne.selenide.Configuration;
import com.codeborne.selenide.proxy.SelenideProxyServer;
import com.codeborne.selenide.webdriver.WebDriverFactory;
import org.openqa.selenium.*;
import org.openqa.selenium.internal.Killable;
import org.openqa.selenium.remote.UnreachableBrowserException;
import org.openqa.selenium.support.events.EventFiringWebDriver;
import org.openqa.selenium.support.events.WebDriverEventListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import static com.codeborne.selenide.Configuration.FileDownloadMode.PROXY;
import static com.codeborne.selenide.Configuration.*;
import static com.codeborne.selenide.impl.Describe.describe;
import static java.lang.Thread.currentThread;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.SEVERE;
public class WebDriverThreadLocalContainer implements WebDriverContainer {
private static final Logger log = Logger.getLogger(WebDriverThreadLocalContainer.class.getName());
protected WebDriverFactory factory = new WebDriverFactory();
protected List<WebDriverEventListener> listeners = new ArrayList<>();
protected Collection<Thread> ALL_WEB_DRIVERS_THREADS = new ConcurrentLinkedQueue<>();
protected Map<Long, WebDriver> THREAD_WEB_DRIVER = new ConcurrentHashMap<>(4);
protected Map<Long, SelenideProxyServer> THREAD_PROXY_SERVER = new ConcurrentHashMap<>(4);
protected Proxy proxy;
protected final AtomicBoolean cleanupThreadStarted = new AtomicBoolean(false);
protected void closeUnusedWebdrivers() {
for (Thread thread : ALL_WEB_DRIVERS_THREADS) {
if (!thread.isAlive()) {
log.info("Thread " + thread.getId() + " is dead. Let's close webdriver " + THREAD_WEB_DRIVER.get(thread.getId()));
closeWebDriver(thread);
}
}
}
@Override
public void addListener(WebDriverEventListener listener) {
listeners.add(listener);
}
@Override
public WebDriver setWebDriver(WebDriver webDriver) {
THREAD_WEB_DRIVER.put(currentThread().getId(), webDriver);
return webDriver;
}
@Override
public void setProxy(Proxy webProxy) {
proxy = webProxy;
}
protected boolean isBrowserStillOpen(WebDriver webDriver) {
try {
webDriver.getTitle();
return true;
} catch (UnreachableBrowserException e) {
log.log(FINE, "Browser is unreachable", e);
return false;
} catch (NoSuchWindowException e) {
log.log(FINE, "Browser window is not found", e);
return false;
} catch (NoSuchSessionException e) {
log.log(FINE, "Browser session is not found", e);
return false;
}
}
/**
* @return true iff webdriver is started in current thread
*/
@Override
public boolean hasWebDriverStarted() {
return THREAD_WEB_DRIVER.containsKey(currentThread().getId());
}
@Override
public WebDriver getWebDriver() {
WebDriver webDriver = THREAD_WEB_DRIVER.get(currentThread().getId());
if (webDriver != null) {
return webDriver;
}
log.info("No webdriver is bound to current thread: " + currentThread().getId() + " - let's create new webdriver");
return setWebDriver(createDriver());
}
@Override
public WebDriver getAndCheckWebDriver() {
WebDriver webDriver = THREAD_WEB_DRIVER.get(currentThread().getId());
if (webDriver != null) {
if (!reopenBrowserOnFail || isBrowserStillOpen(webDriver)) {
return webDriver;
}
else {
log.info("Webdriver has been closed meanwhile. Let's re-create it.");
closeWebDriver();
}
}
return setWebDriver(createDriver());
}
@Override
public SelenideProxyServer getProxyServer() {
return THREAD_PROXY_SERVER.get(currentThread().getId());
}
@Override
public void closeWebDriver() {
closeWebDriver(currentThread());
}
protected void closeWebDriver(Thread thread) {
ALL_WEB_DRIVERS_THREADS.remove(thread);
WebDriver webdriver = THREAD_WEB_DRIVER.remove(thread.getId());
SelenideProxyServer proxy = THREAD_PROXY_SERVER.remove(thread.getId());
if (webdriver != null && !holdBrowserOpen) {
log.info("Close webdriver: " + thread.getId() + " -> " + webdriver);
log.info("Close proxy server: " + thread.getId() + " -> " + proxy);
long start = System.currentTimeMillis();
Thread t = new Thread(new CloseBrowser(webdriver, proxy));
t.setDaemon(true);
t.start();
try {
t.join(closeBrowserTimeoutMs);
} catch (InterruptedException e) {
log.log(FINE, "Failed to close webdriver in " + closeBrowserTimeoutMs + " milliseconds", e);
}
long duration = System.currentTimeMillis() - start;
if (duration >= closeBrowserTimeoutMs) {
log.severe("Failed to close webdriver in " + closeBrowserTimeoutMs + " milliseconds");
}
else if (duration > 200) {
log.info("Closed webdriver in " + duration + " ms");
}
else {
log.fine("Closed webdriver in " + duration + " ms");
}
}
else if (proxy != null && !holdBrowserOpen) {
log.info("Close proxy server: " + thread.getId() + " -> " + proxy);
proxy.shutdown();
}
}
private static class CloseBrowser implements Runnable {
private final WebDriver webdriver;
private final SelenideProxyServer proxy;
private CloseBrowser(WebDriver webdriver, SelenideProxyServer proxy) {
this.webdriver = webdriver;
this.proxy = proxy;
}
@Override
public void run() {
try {
log.info("Trying to close the browser " + describe(webdriver) + " ...");
webdriver.quit();
}
catch (UnreachableBrowserException e) {
// It happens for Firefox. It's ok: browser is already closed.
log.log(FINE, "Browser is unreachable", e);
}
catch (WebDriverException cannotCloseBrowser) {
log.severe("Cannot close browser normally: " + Cleanup.of.webdriverExceptionMessage(cannotCloseBrowser));
}
finally {
killBrowser(webdriver);
}
if (proxy != null) {
log.info("Trying to shutdown " + proxy + " ...");
proxy.shutdown();
}
}
protected void killBrowser(WebDriver webdriver) {
if (webdriver instanceof Killable) {
try {
((Killable) webdriver).kill();
} catch (Exception e) {
log.log(SEVERE, "Failed to kill browser " + webdriver + ':', e);
}
}
}
}
@Override
public void clearBrowserCache() {
WebDriver webdriver = THREAD_WEB_DRIVER.get(currentThread().getId());
if (webdriver != null) {
webdriver.manage().deleteAllCookies();
}
}
@Override
public String getPageSource() {
return getWebDriver().getPageSource();
}
@Override
public String getCurrentUrl() {
return getWebDriver().getCurrentUrl();
}
@Override
public String getCurrentFrameUrl() {
return ((JavascriptExecutor) getWebDriver()).executeScript("return window.location.href").toString();
}
protected WebDriver createDriver() {
Proxy userProvidedProxy = proxy;
if (Configuration.fileDownload == PROXY) {
SelenideProxyServer selenideProxyServer = new SelenideProxyServer(proxy);
selenideProxyServer.start();
THREAD_PROXY_SERVER.put(currentThread().getId(), selenideProxyServer);
userProvidedProxy = selenideProxyServer.createSeleniumProxy();
}
WebDriver webdriver = factory.createWebDriver(userProvidedProxy);
log.info("Create webdriver in current thread " + currentThread().getId() + ": " +
describe(webdriver) + " -> " + webdriver);
return markForAutoClose(addListeners(webdriver));
}
protected WebDriver addListeners(WebDriver webdriver) {
if (listeners.isEmpty()) {
return webdriver;
}
EventFiringWebDriver wrapper = new EventFiringWebDriver(webdriver);
for (WebDriverEventListener listener : listeners) {
log.info("Add listener to webdriver: " + listener);
wrapper.register(listener);
}
return wrapper;
}
protected WebDriver markForAutoClose(WebDriver webDriver) {
ALL_WEB_DRIVERS_THREADS.add(currentThread());
if (!cleanupThreadStarted.get()) {
synchronized (this) {
if (!cleanupThreadStarted.get()) {
new UnusedWebdriversCleanupThread().start();
cleanupThreadStarted.set(true);
}
}
}
Runtime.getRuntime().addShutdownHook(new WebdriversFinalCleanupThread(currentThread()));
return webDriver;
}
protected class WebdriversFinalCleanupThread extends Thread {
private final Thread thread;
public WebdriversFinalCleanupThread(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
closeWebDriver(thread);
}
}
protected class UnusedWebdriversCleanupThread extends Thread {
public UnusedWebdriversCleanupThread() {
setDaemon(true);
setName("Webdrivers killer thread");
}
@Override
public void run() {
while (true) {
closeUnusedWebdrivers();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
}