org.kurento.test.grid.GridHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.kurento.test.grid.GridHandler.java

Source

/*
    
 * (C) Copyright 2015 Kurento (http://kurento.org/)
 *
 * 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 org.kurento.test.grid;

import static org.kurento.commons.PropertiesManager.getProperty;
import static org.kurento.test.config.TestConfiguration.SELENIUM_HUB_ADDRESS;
import static org.kurento.test.config.TestConfiguration.SELENIUM_HUB_ADDRESS_DEFAULT;
import static org.kurento.test.config.TestConfiguration.SELENIUM_HUB_PORT_DEFAULT;
import static org.kurento.test.config.TestConfiguration.SELENIUM_HUB_PORT_PROPERTY;
import static org.kurento.test.config.TestConfiguration.SELENIUM_NODES_FILE_LIST_PROPERTY;
import static org.kurento.test.config.TestConfiguration.SELENIUM_NODES_LIST_DEFAULT;
import static org.kurento.test.config.TestConfiguration.SELENIUM_NODES_LIST_PROPERTY;
import static org.kurento.test.config.TestConfiguration.SELENIUM_NODES_URL_PROPERTY;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Writer;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServlet;

import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClientBuilder;
import org.junit.Assert;
import org.kurento.test.base.PerformanceTest;
import org.kurento.test.browser.BrowserType;
import org.kurento.test.config.TestScenario;
import org.kurento.test.utils.Randomizer;
import org.kurento.test.utils.Shell;
import org.kurento.test.utils.SshConnection;
import org.openqa.grid.selenium.GridLauncher;
import org.openqa.jetty.http.HttpListener;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.net.NetworkUtils;
import org.openqa.selenium.support.events.WebDriverEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.io.CharStreams;
import com.google.gson.JsonElement;

import freemarker.template.Configuration;
import freemarker.template.Template;

/**
 * Singleton handler for Selenium Grid infrastructure.
 *
 * @author Boni Garcia (bgarcia@gsyc.es)
 * @since 5.1.1
 * @see <a href="http://www.seleniumhq.org/">Selenium</a>
 */
public class GridHandler {

    public static Logger log = LoggerFactory.getLogger(GridHandler.class);

    public static final String REMOTE_FOLDER = ".kurento-test";
    public static final String REMOTE_PID_FILE = "node-pid";
    public static final String IPS_REGEX = "(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";

    private static final int TIMEOUT_NODE = 300; // seconds
    private static final String LAUNCH_SH = "launch-node.sh";

    private static GridHandler instance = null;

    private GridHub hub;
    private String hubAddress = getProperty(SELENIUM_HUB_ADDRESS, SELENIUM_HUB_ADDRESS_DEFAULT);
    private int hubPort = getProperty(SELENIUM_HUB_PORT_PROPERTY, SELENIUM_HUB_PORT_DEFAULT);
    private CountDownLatch countDownLatch;
    private Map<String, GridNode> nodes = new ConcurrentHashMap<>();
    private List<String> nodeList;
    private boolean hubStarted = false;
    private boolean nodeListFiltered = false;

    protected GridHandler() {
        String nodesListProp = System.getProperty(SELENIUM_NODES_LIST_PROPERTY);
        String nodesListFileProp = System.getProperty(SELENIUM_NODES_FILE_LIST_PROPERTY);
        String nodesListUrlProp = System.getProperty(SELENIUM_NODES_URL_PROPERTY);

        if (nodesListUrlProp != null) {
            if (nodeList == null) {
                nodeList = new ArrayList<>();
                try {
                    log.trace("Reading node list from URL {}", nodesListUrlProp);
                    String contents = readContents(nodesListUrlProp);
                    Pattern p = Pattern.compile(IPS_REGEX);
                    Matcher m = p.matcher(contents);
                    while (m.find()) {
                        nodeList.add(m.group());
                    }
                } catch (IOException e) {
                    Assert.fail("Exception reading URL " + nodesListUrlProp + " : " + e.getMessage());
                }
            }

        } else if (nodesListFileProp != null) {
            log.trace("Reading node list from file {}", nodesListFileProp);
            try {
                nodeList = FileUtils.readLines(new File(nodesListFileProp), Charset.defaultCharset());
            } catch (IOException e) {
                Assert.fail("Exception reading node list file: " + e.getMessage());
            }

        } else if (nodesListProp != null) {
            log.trace("Reading node list from property {}", nodesListProp);
            nodeList = new ArrayList<>(Arrays.asList(nodesListProp.split(";")));

        } else {
            log.trace("Using default node list {}", SELENIUM_NODES_LIST_DEFAULT);
            InputStream inputStream = PerformanceTest.class.getClassLoader()
                    .getResourceAsStream(SELENIUM_NODES_LIST_DEFAULT);

            try {
                nodeList = CharStreams.readLines(new InputStreamReader(inputStream, Charsets.UTF_8));
            } catch (IOException e) {
                Assert.fail("Exception reading node-list.txt: " + e.getMessage());
            }
        }
    }

    public static synchronized GridHandler getInstance() {
        if (instance == null) {
            instance = new GridHandler();
        }
        return instance;
    }

    public synchronized void stopGrid() {
        log.debug("Stopping Selenium Grid");
        try {
            // Stop Hub
            if (hub != null) {
                log.debug("Stopping Hub");
                hub.stop();
                hubStarted = false;
            }

            // Stop Nodes
            if (nodes != null) {
                log.debug("Number of nodes: {}", nodes.size());

                for (GridNode node : nodes.values()) {
                    log.debug("Stopping Node {}", node.getHost());
                    stopNode(node);
                }
            }
            nodes.clear();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    public synchronized void startHub() {
        try {
            if (hubAddress != null && !hubStarted) {
                hub = new GridHub(hubPort);
                hub.start();
                hubStarted = true;
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void startNodes() {
        try {
            countDownLatch = new CountDownLatch(nodes.size());
            ExecutorService exec = Executors.newFixedThreadPool(nodes.size());

            for (final GridNode n : nodes.values()) {
                Thread t = new Thread() {
                    @Override
                    public void run() {
                        startNode(n);
                    }
                };
                exec.execute(t);
            }

            if (!countDownLatch.await(TIMEOUT_NODE, TimeUnit.SECONDS)) {
                Assert.fail("Timeout waiting nodes (" + TIMEOUT_NODE + " seconds)");
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void startNode(GridNode node) {
        try {
            countDownLatch = new CountDownLatch(1);
            log.debug("Launching node {}", node.getHost());
            node.startSsh();

            final String chromeDriverSource = System.getProperty("webdriver.chrome.driver");
            final Class<?>[] classpath = { GridLauncher.class, ImmutableList.class, HttpListener.class,
                    NetworkUtils.class, WebDriverException.class, LogFactory.class, HttpServlet.class,
                    ChromeDriver.class, FirefoxDriver.class, JsonElement.class, HttpEntity.class, HttpClient.class,
                    WebDriverEventListener.class, ExecuteWatchdog.class };

            // OverThere SCP need absolute path, so home path must be known
            String remoteHome = node.getHome();

            final String remoteFolder = remoteHome + "/" + REMOTE_FOLDER;
            final String remoteChromeDriver = remoteFolder + "/chromedriver";
            final String remoteScript = node.getTmpFolder() + "/" + LAUNCH_SH;
            final String remotePort = String.valueOf(node.getSshConnection().getFreePort());

            if (!node.getSshConnection().exists(remoteFolder) || node.isOverwrite()) {
                node.getSshConnection().execAndWaitCommand("mkdir", "-p", remoteFolder);
            }
            if (!node.getSshConnection().exists(remoteChromeDriver) || node.isOverwrite()) {
                node.getSshConnection().scp(chromeDriverSource, remoteChromeDriver);
                node.getSshConnection().execAndWaitCommand("chmod", "+x", remoteChromeDriver);
            }

            String cp = "";
            for (Class<?> clazz : classpath) {
                if (!cp.isEmpty()) {
                    cp += ":";
                }
                String jarSource = getJarPath(clazz).getAbsolutePath();
                String remoteSeleniumJar = remoteFolder + "/" + getJarPath(clazz).getName();
                cp += remoteSeleniumJar;

                if (!node.getSshConnection().exists(remoteSeleniumJar) || node.isOverwrite()) {
                    node.getSshConnection().scp(jarSource, remoteSeleniumJar);
                }
            }

            // Script is always overwritten
            createRemoteScript(node, remotePort, remoteScript, remoteFolder, remoteChromeDriver, cp,
                    node.getBrowserType(), node.getMaxInstances());

            // Launch node
            node.getSshConnection().execCommand(remoteScript);

            // Wait to be available for Hub
            waitForNode(node.getHost(), remotePort);

            // Set started flag to true
            node.setStarted(true);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    private File getJarPath(Class<?> aclass) {
        URL url;
        try {
            url = aclass.getProtectionDomain().getCodeSource().getLocation();
        } catch (SecurityException ex) {
            url = aclass.getResource(aclass.getSimpleName() + ".class");
        }
        try {
            return new File(url.toURI());
        } catch (URISyntaxException ex) {
            return new File(url.getPath());
        }
    }

    private void createRemoteScript(GridNode node, String remotePort, String remoteScript, String remoteFolder,
            String remoteChromeDriver, String classpath, BrowserType browser, int maxInstances) throws IOException {

        Map<String, Object> data = new HashMap<String, Object>();
        data.put("remotePort", String.valueOf(remotePort));
        data.put("maxInstances", String.valueOf(maxInstances));
        data.put("hubIp", hubAddress);
        data.put("hubPort", String.valueOf(hubPort));
        data.put("tmpFolder", node.getTmpFolder());
        data.put("remoteChromeDriver", remoteChromeDriver);
        data.put("classpath", classpath);
        data.put("pidFile", REMOTE_PID_FILE);
        data.put("browser", browser);

        // Create script for Node
        Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
        cfg.setClassForTemplateLoading(PerformanceTest.class, "/templates/");

        String tmpScript = node.getTmpFolder() + LAUNCH_SH;
        try {
            Template template = cfg.getTemplate(LAUNCH_SH + ".ftl");
            Writer writer = new FileWriter(new File(tmpScript));
            template.process(data, writer);
            writer.flush();
            writer.close();

        } catch (Exception e) {
            throw new RuntimeException("Exception while creating file from template", e);
        }

        // Copy script to remote node
        node.getSshConnection().scp(tmpScript, remoteScript);
        node.getSshConnection().execAndWaitCommand("chmod", "+x", remoteScript);
        Shell.runAndWait("rm", tmpScript);
    }

    public void copyRemoteVideo(GridNode node, String video) {
        try {
            // Copy video in remote host if necessary
            if (!node.getSshConnection().exists(node.getRemoteVideo(video)) || node.isOverwrite()) {
                node.getSshConnection().scp(video, node.getRemoteVideo(video));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private void waitForNode(String node, String port) {
        log.debug("Waiting for node {} to be ready...", node);
        int responseStatusCode = 0;
        HttpClient client = HttpClientBuilder.create().build();
        HttpGet httpGet = new HttpGet("http://" + node + ":" + port + "/wd/hub/static/resource/hub.html");

        // Wait for a max of TIMEOUT_NODE seconds
        long maxSystemTime = System.currentTimeMillis() + TIMEOUT_NODE * 1000;
        do {
            try {
                HttpResponse response = client.execute(httpGet);
                responseStatusCode = response.getStatusLine().getStatusCode();
            } catch (Exception e) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException ie) {
                    // Intentionally left blank
                }
                if (System.currentTimeMillis() > maxSystemTime) {
                    log.error("Timeout ({} sec) waiting for node {}", TIMEOUT_NODE, node);
                }
            }
        } while (responseStatusCode != HttpStatus.SC_OK);

        if (responseStatusCode == HttpStatus.SC_OK) {
            log.debug("Node {} ready (responseStatus {})", node, responseStatusCode);
            countDownLatch.countDown();
        }
    }

    public synchronized void filterValidNodes() {
        if (!nodeListFiltered) {
            log.debug("Node availables in the node list: {}", nodeList.size());
            int nodeListSize = nodeList.size();
            ExecutorService executor = Executors.newFixedThreadPool(nodeListSize);
            final CountDownLatch latch = new CountDownLatch(nodeListSize);

            for (final String nodeCandidate : nodeList) {
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        if (!nodeIsValid(nodeCandidate)) {
                            nodeList.remove(nodeCandidate);
                        }
                        latch.countDown();
                    }
                });
            }

            try {
                latch.await();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            nodeListFiltered = true;
            log.debug("Node availables in the node list after filtering: {}", nodeList.size());
        }
    }

    public boolean nodeIsValid(String nodeCandidate) {
        boolean valid = false;
        log.debug("Node candidate {}", nodeCandidate);

        if (SshConnection.ping(nodeCandidate)) {
            SshConnection remoteHost = new SshConnection(nodeCandidate);
            try {
                remoteHost.start();
                int xvfb = remoteHost.runAndWaitCommand("xvfb-run");
                if (xvfb != 2) {
                    log.debug("Node {} has no Xvfb", nodeCandidate);
                } else {
                    valid = true;
                }
            } catch (Exception e) {
                log.debug("Invalid credentials to access node {} ", nodeCandidate);
            } finally {
                remoteHost.stop();
            }

        } else {
            log.debug("Node {} seems to be down", nodeCandidate);
        }
        return valid;
    }

    public synchronized GridNode getRandomNodeFromList(String browserKey, BrowserType browserType,
            int browserPerInstance) {

        log.debug("getRandomNodeFromList for browser {}", browserKey);

        GridNode node = browserPerInstance > 1 ? existsNode(browserKey) : null;
        if (node == null) {
            try {

                String nodeCandidate = nodeList.get(Randomizer.getInt(0, nodeList.size()));
                log.debug("######## Creating node {} in host {}", browserKey, nodeCandidate);
                node = new GridNode(nodeCandidate, browserType, browserPerInstance);
                addNode(browserKey, node);
                nodeList.remove(nodeCandidate);
                log.debug(">>>> Using node {} for browser '{}'", node.getHost(), browserKey);

            } catch (IllegalArgumentException e) {
                throw new RuntimeException("No valid available node(s) to perform Selenim Grid test");
            }

        } else {
            log.debug(">>>> Re-using node {} for browser '{}'", node.getHost(), browserKey);
            node.setStarted(true);
        }
        return node;

    }

    private synchronized GridNode existsNode(String browserKey) {
        GridNode gridNode = null;
        int indexOfSeparator = browserKey.lastIndexOf(TestScenario.INSTANCES_SEPARATOR);

        if (indexOfSeparator != -1) {
            String browserPreffix = browserKey.substring(0, indexOfSeparator + 1);
            log.debug("browserPreffix {}", browserPreffix);

            for (String node : nodes.keySet()) {
                if (node.startsWith(browserPreffix)) {
                    gridNode = nodes.get(node);
                    break;
                }
            }
        }
        log.debug("Exists node {} = {}", browserKey, gridNode != null);

        return gridNode;
    }

    private void stopNode(GridNode node) throws IOException {
        if (node.getSshConnection().isStarted()) {
            node.getSshConnection().execCommand("kill", "-9", "-1");
            node.stopSsh();
        }
    }

    public void runParallel(List<GridNode> nodeList, Runnable myFunc)
            throws InterruptedException, ExecutionException {
        ExecutorService exec = Executors.newFixedThreadPool(nodes.size());
        List<Future<?>> results = new ArrayList<>();
        for (int i = 0; i < nodes.size(); i++) {
            results.add(exec.submit(myFunc));
        }
        for (Future<?> r : results) {
            r.get();
        }
    }

    public String getHubHost() {
        return hubAddress;
    }

    public int getHubPort() {
        return hubPort;
    }

    public GridNode getNode(String browserKey) {
        return nodes.get(browserKey);
    }

    public synchronized void addNode(String browserKey, GridNode node) {
        log.debug("Adding node {} ({}) to map", browserKey, node.getHost());
        nodes.put(browserKey, node);
    }

    public boolean useRemoteNodes() {
        return !nodes.isEmpty();
    }

    public void logNodeList() {
        String nodeListStr = "";
        for (GridNode node : nodes.values()) {
            nodeListStr += node.getHost() + " ";
        }
        log.debug("Node list: {}", nodeListStr);
    }

    public GridNode getFirstNode(String browserKey) {
        if (nodes.containsKey(browserKey)) {
            return nodes.get(browserKey);
        } else {
            return nodes.get(browserKey.substring(0, browserKey.indexOf("-") + 1) + 0);
        }
    }

    public synchronized boolean containsSimilarBrowserKey(String browserKey) {
        boolean constainsSimilarBrowser = false;
        int index = browserKey.indexOf("-");
        String browser = null;
        if (index != -1) {
            String prefix = browserKey.substring(0, browserKey.indexOf("-"));
            for (String key : nodes.keySet()) {
                constainsSimilarBrowser |= key.startsWith(prefix);
                if (constainsSimilarBrowser) {
                    browser = key;
                    break;
                }
            }
        }

        if (constainsSimilarBrowser && !nodes.keySet().contains(browserKey) && browser != null) {
            addNode(browserKey, nodes.get(browser));
        }
        return constainsSimilarBrowser;
    }

    public void setHubAddress(String hubAddress) {
        this.hubAddress = hubAddress;
    }

    public static String readContents(String address) throws IOException {
        StringBuilder contents = new StringBuilder(2048);
        BufferedReader br = null;
        try {
            URL url = new URL(address);
            br = new BufferedReader(new InputStreamReader(url.openStream()));
            String line = "";
            while (line != null) {
                line = br.readLine();
                contents.append(line);
            }
        } finally {
            if (br != null) {
                br.close();
            }
        }
        return contents.toString();
    }

    public List<String> getNodeList() {
        return nodeList;
    }

}