hudson.plugins.selenium.PluginImpl.java Source code

Java tutorial

Introduction

Here is the source code for hudson.plugins.selenium.PluginImpl.java

Source

/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF 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 hudson.plugins.selenium;

import hudson.DescriptorExtensionList;
import hudson.Extension;
import hudson.Plugin;
import hudson.console.HyperlinkNote;
import hudson.model.Action;
import hudson.model.Api;
import hudson.model.Computer;
import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.model.Failure;
import hudson.model.Label;
import hudson.model.TaskListener;
import hudson.plugins.selenium.callables.hub.StopHubCallable;
import hudson.plugins.selenium.configuration.ConfigurationDescriptor;
import hudson.plugins.selenium.configuration.CustomRCConfiguration;
import hudson.plugins.selenium.configuration.CustomWDConfiguration;
import hudson.plugins.selenium.configuration.SeleniumNodeConfiguration;
import hudson.plugins.selenium.configuration.browser.selenium.ChromeBrowser;
import hudson.plugins.selenium.configuration.browser.selenium.FirefoxBrowser;
import hudson.plugins.selenium.configuration.browser.selenium.IEBrowser;
import hudson.plugins.selenium.configuration.browser.selenium.SeleniumBrowser;
import hudson.plugins.selenium.configuration.browser.webdriver.WebDriverBrowser;
import hudson.plugins.selenium.configuration.global.SeleniumGlobalConfiguration;
import hudson.plugins.selenium.configuration.global.hostname.HostnameResolver;
import hudson.plugins.selenium.configuration.global.hostname.HostnameResolverDescriptor;
import hudson.plugins.selenium.configuration.global.hostname.JenkinsRootHostnameResolver;
import hudson.plugins.selenium.configuration.global.matcher.MatchAllMatcher;
import hudson.plugins.selenium.configuration.global.matcher.SeleniumConfigurationMatcher;
import hudson.plugins.selenium.configuration.global.matcher.SeleniumConfigurationMatcher.MatcherDescriptor;
import hudson.plugins.selenium.process.SeleniumProcessUtils;
import hudson.remoting.Channel;
import hudson.security.Permission;
import hudson.security.PermissionGroup;
import hudson.security.PermissionScope;
import hudson.util.StreamTaskListener;
import jenkins.model.Jenkins;
import jenkins.security.MasterToSlaveCallable;
import net.sf.json.JSONObject;
import org.acegisecurity.AccessDeniedException;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.framework.io.LargeText;
import org.openqa.grid.internal.Registry;
import org.openqa.grid.internal.RemoteProxy;
import org.openqa.grid.internal.TestSlot;
import org.springframework.util.StringUtils;

import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.ServerSocket;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Starts Selenium Grid server in another JVM.
 *
 * @author Kohsuke Kawaguchi
 * @author Richard Lavoie
 * @author Ryan Thomas Correia Ortega
 */
@ExportedBean
public class PluginImpl extends Plugin implements Action, Serializable, Describable<PluginImpl> {

    private static final long serialVersionUID = 1L;

    private static final String SEPARATOR = ",";

    private static final Logger LOGGER = Logger.getLogger(PluginImpl.class.getName());

    private static final PermissionGroup SELENIUM_GROUP = new PermissionGroup(PluginImpl.class,
            Messages._PermissionGroup());

    private static final Permission SELENIUM_ADMIN = new Permission(SELENIUM_GROUP, "Admin",
            Messages._AdminPermission(), Computer.CONFIGURE, true, new PermissionScope[0]);

    /**
     * Default port for hub servlet.
     */
    private int port = 4444;

    /**
     * Exclusion pattern for nodes. Nodes matching this pattern will not have a selenium node running on them.
     */
    private String exclusionPatterns;
    private Integer newSessionWaitTimeout = -1; //ms or -1
    private Integer maxSession = 5; //number of allowed session //default value 5
    private Integer timeout = 300; //sec               // default value as defined in WebDriver
    private Integer browserTimeout = 0; //sec               // default value as defined in WebDriver
    private boolean throwOnCapabilityNotPresent = false;
    private String hubLogLevel = "INFO";

    private HostnameResolver hostnameResolver = new JenkinsRootHostnameResolver();

    private final List<SeleniumGlobalConfiguration> configurations = new ArrayList<SeleniumGlobalConfiguration>();

    /**
     * Channel to Selenium Grid JVM.
     */
    private transient Channel channel;

    private transient Future<?> hubLauncher;

    private transient StreamTaskListener listener;

    @Override
    public void postInitialize() throws Exception {

        startHub();

        Jenkins.getInstance().getActions().add(this);
    }

    @Override
    public void start() throws Exception {
        load();
    }

    @Override
    public void configure(StaplerRequest req, JSONObject formData)
            throws IOException, ServletException, Descriptor.FormException {
        super.configure(req, formData);

        port = formData.optInt("port", 4444);
        exclusionPatterns = formData.getString("exclusionPatterns");
        hubLogLevel = formData.getString("hubLogLevel");
        newSessionWaitTimeout = formData.optInt("newSessionWaitTimeout", -1);
        maxSession = formData.optInt("maxSession", 5);
        timeout = formData.optInt("timeout", 300000);
        browserTimeout = formData.optInt("browserTimeout", 0);
        throwOnCapabilityNotPresent = formData.getBoolean("throwOnCapabilityNotPresent");

        hostnameResolver = req.bindJSON(HostnameResolver.class, formData.optJSONObject("hostnameResolver"));
        if (hostnameResolver == null)
            hostnameResolver = new JenkinsRootHostnameResolver();

        save();
    }

    /**
     * @throws IOException
     * @throws InterruptedException
     *
     */
    private void startHub() throws IOException, InterruptedException {

        this.listener = new StreamTaskListener(getLogFile());

        channel = SeleniumProcessUtils.createSeleniumGridVM(listener);

        Level logLevel = Level.parse(getHubLogLevel());
        this.listener.getLogger().println("Starting Selenium Grid");

        List<String> args = new ArrayList<>();
        if (getNewSessionWaitTimeout() != null && getNewSessionWaitTimeout() >= 0) {
            args.add("-newSessionWaitTimeout");
            args.add(getNewSessionWaitTimeout().toString());
        }
        if (getMaxSession() != null && getMaxSession() > 0) {
            args.add("-maxSession");
            args.add(getMaxSession().toString());
        }
        if (getTimeout() != null) {
            args.add("-timeout");
            args.add(getTimeout().toString());
        }
        if (getBrowserTimeout() != null) {
            args.add("-browserTimeout");
            args.add(getBrowserTimeout().toString());
        }
        if (getThrowOnCapabilityNotPresent()) {
            args.add("-throwOnCapabilityNotPresent");
            args.add(Boolean.toString(getThrowOnCapabilityNotPresent()));
        }

        args.add("-host");
        args.add(getMasterHostName());

        hubLauncher = channel.callAsync(new HubLauncher(port, args.toArray(new String[args.size()]), logLevel));
    }

    public File getLogFile() {
        return new File(Jenkins.getInstance().getRootDir(), "selenium.log");
    }

    public void waitForHubLaunch() throws ExecutionException, InterruptedException {
        hubLauncher.get();
    }

    public String getDisplayName() {
        return "Selenium Grid";
    }

    public String getIconFileName() {
        return "/plugin/selenium/24x24/selenium.png";
    }

    public String getUrlName() {
        return "/selenium";
    }

    @Exported
    public int getPort() {
        return port;
    }

    @Exported
    public HostnameResolver getHostnameResolver() {
        return hostnameResolver;
    }

    public Api getApi() {
        return new Api(this);
    }

    @Exported
    public String getExclusionPatterns() {
        return exclusionPatterns;
    }

    @Exported
    public Integer getNewSessionWaitTimeout() {
        return newSessionWaitTimeout;
    }

    @Exported
    public Integer getMaxSession() {
        return maxSession;
    }

    @Exported
    public Integer getTimeout() {
        return timeout;
    }

    @Exported
    public Integer getBrowserTimeout() {
        return browserTimeout;
    }

    @Exported
    public String getHubLogLevel() {
        return hubLogLevel != null ? hubLogLevel : "INFO";
    }

    @Exported
    public boolean getConfigurationChanged() {
        HubParams activeHubParams = getCurrentHubParams();
        return activeHubParams.isNotActiveOn(getMasterHostName(), port);
    }

    @Exported
    public Integer getActivePort() {
        return getCurrentHubParams().getPort();
    }

    @Exported
    public String getActiveHost() {
        return getCurrentHubParams().getHost();
    }

    public HubParams getCurrentHubParams() {
        HubParams result = new HubParams();
        if (channel == null) {
            return result;
        }

        try {
            return channel.call(new HubParamsCallable());
        } catch (Throwable e) {
            LOGGER.log(Level.SEVERE, "Unable to communicate with hub", e);
        }

        return result;
    }

    @Exported
    public boolean getThrowOnCapabilityNotPresent() {
        return throwOnCapabilityNotPresent;
    }

    @Override
    public void stop() throws Exception {
        for (Computer c : Jenkins.getInstance().getComputers()) {
            for (SeleniumGlobalConfiguration cfg : configurations) {
                cfg.stop(c);
            }
        }

        this.listener.closeQuietly();
        channel.close();
    }

    @Exported(inline = true)
    public Collection<SeleniumTestSlotGroup> getRemoteControls() throws IOException, InterruptedException {
        if (channel == null)
            return Collections.emptyList();

        return channel.call(new MasterToSlaveCallable<Collection<SeleniumTestSlotGroup>, RuntimeException>() {

            /**
             *
             */
            private static final long serialVersionUID = 1791985298575049757L;

            public Collection<SeleniumTestSlotGroup> call() {
                Map<URL, SeleniumTestSlotGroup> groups = new HashMap<URL, SeleniumTestSlotGroup>();

                if (HubHolder.getHub() == null) {
                    return Collections.emptyList();
                }

                Registry registry = HubHolder.getHub().getRegistry();
                if (registry != null) {
                    for (RemoteProxy proxy : registry.getAllProxies()) {
                        for (TestSlot slot : proxy.getTestSlots()) {
                            URL host = slot.getProxy().getRemoteHost();
                            SeleniumTestSlotGroup grp = groups.get(host);
                            if (grp == null) {
                                grp = new SeleniumTestSlotGroup(host);
                                groups.put(host, grp);
                            }
                            grp.addTestSlot(new SeleniumTestSlot(slot));
                        }

                    }
                }
                List<SeleniumTestSlotGroup> values = new ArrayList<SeleniumTestSlotGroup>(groups.values());
                Collections.sort(values);
                return values;
            }
        });

    }

    /**
     * Determines the host name of the Jenkins master.
     */
    public static String getMasterHostName() {
        return getPlugin().hostnameResolver.retrieveHost();
    }

    /**
     * Handles incremental log.
     */
    public void doProgressiveLog(StaplerRequest req, StaplerResponse rsp) throws IOException {
        new LargeText(getLogFile(), false).doProgressText(req, rsp);
    }

    @SuppressWarnings("unchecked")
    public Descriptor<PluginImpl> getDescriptor() {
        return Jenkins.getInstance().getDescriptorOrDie(getClass());
    }

    @Extension
    public static class DescriptorImpl extends Descriptor<PluginImpl> {
        @Override
        public String getDisplayName() {
            return "";
        }
    }

    /**
     *  Backwards compatibility in configuration
     */
    private transient String rcFirefoxProfileTemplate;
    private transient Boolean rcBrowserSessionReuse;
    private transient Boolean rcTrustAllSSLCerts;
    private transient Boolean rcBrowserSideLog;
    private transient boolean rcDebug;

    // Migrate old configurations to new configuration
    public Object readResolve() {
        // Update from plugin 2.0 to 2.1 where browsers were introduced
        if (rcFirefoxProfileTemplate != null || rcBrowserSessionReuse != null || rcTrustAllSSLCerts != null
                || rcBrowserSideLog != null) {
            String rcFirefoxProfileTemplate = getDefaultForNull(this.rcFirefoxProfileTemplate, "");
            Boolean rcBrowserSessionReuse = getDefaultForNull(this.rcBrowserSessionReuse, Boolean.FALSE);
            Boolean rcTrustAllSSLCerts = getDefaultForNull(this.rcTrustAllSSLCerts, Boolean.FALSE);
            Boolean rcBrowserSideLog = getDefaultForNull(this.rcBrowserSideLog, Boolean.FALSE);

            List<SeleniumBrowser> browsers = new ArrayList<SeleniumBrowser>();
            browsers.add(new IEBrowser(5, "", ""));
            browsers.add(new FirefoxBrowser(5, "", ""));
            browsers.add(new ChromeBrowser(5, "", ""));

            int port = 4445;
            try {
                ServerSocket ss = new ServerSocket(0);
                port = ss.getLocalPort();
                ss.close();
            } catch (IOException e) {
            }

            SeleniumNodeConfiguration c = new CustomRCConfiguration(port, rcBrowserSideLog, rcDebug,
                    rcTrustAllSSLCerts, rcBrowserSessionReuse, -1, rcFirefoxProfileTemplate, browsers, null);

            synchronized (configurations) {
                configurations.add(
                        new SeleniumGlobalConfiguration("Selenium v2.0 configuration", new MatchAllMatcher(), c));
            }

        }

        // update to 2.3 where hostname options were introduced
        if (hostnameResolver == null) {
            hostnameResolver = new JenkinsRootHostnameResolver();
        }

        return this;
    }

    /**
     * Returns either the object, or the default value is null.
     *
     * @param object
     *            Object to return
     * @param defaultObject
     *            Default value in case object is null
     * @return Object value
     */
    private <T> T getDefaultForNull(T object, T defaultObject) {
        return object == null ? defaultObject : object;
    }

    public static void startSeleniumNode(Computer c, TaskListener listener, String conf)
            throws IOException, InterruptedException {
        LOGGER.fine("Examining if we need to start Selenium Grid Node");

        final PluginImpl p = Jenkins.getInstance().getPlugin(PluginImpl.class);

        final String exclusions = p.getExclusionPatterns();
        List<String> exclusionPatterns = new ArrayList<String>();
        if (StringUtils.hasText(exclusions)) {
            exclusionPatterns = Arrays.asList(exclusions.split(SEPARATOR));
        }
        if (exclusionPatterns.size() > 0) {
            // loop over all the labels and check if we need to exclude a node
            // based on the exlusionPatterns
            for (Label label : c.getNode().getAssignedLabels()) {
                for (String pattern : exclusionPatterns) {
                    if (label.toString().matches(pattern)) {
                        LOGGER.fine("Node " + c.getNode().getDisplayName()
                                + " is excluded from Selenium Grid because its label '" + label
                                + "' matches exclusion pattern '" + pattern + "'");
                        return;
                    }
                }
            }
        }

        final String masterName = PluginImpl.getMasterHostName();
        if (masterName == null) {
            listener.getLogger()
                    .println("Unable to determine the host name of the master. Skipping Selenium execution. "
                            + "Please " + HyperlinkNote.encodeTo("/configure", "configure the Jenkins URL")
                            + " from the system configuration screen.");
            return;
        }

        // make sure that Selenium Hub is started before we start RCs.
        try {
            p.waitForHubLaunch();
        } catch (ExecutionException e) {
            throw new IOException("Failed to wait for the Hub launch to complete", e);
        }

        List<SeleniumGlobalConfiguration> confs = getPlugin().getGlobalConfigurationForComputer(c);
        if (confs == null || confs.size() == 0) {
            LOGGER.fine("There is no matching configurations for that computer. Skipping selenium execution.");
            return;
        }

        listener.getLogger()
                .println("Starting Selenium nodes on " + ("".equals(c.getName()) ? "(master)" : c.getName()));

        for (SeleniumGlobalConfiguration config : confs) {
            if ((conf != null && config.getName().equals(conf)) || conf == null) {
                try {
                    config.start(c, listener);
                } catch (ExecutionException e) {
                    LOGGER.log(Level.SEVERE, e.getMessage(), e);
                }
            }
        }
    }

    public static PluginImpl getPlugin() {
        return Jenkins.getInstance().getPlugin(PluginImpl.class);
    }

    @Exported
    public List<SeleniumGlobalConfiguration> getGlobalConfigurations() {
        return configurations;
    }

    public boolean hasGlobalConfiguration(String name) {
        return getConfiguration(name) != null;
    }

    public void removeGlobalConfigurations(String name) throws IOException {
        removeGlobalConfigurations(name, true);
    }

    public List<SeleniumGlobalConfiguration> getGlobalConfigurationForComputer(Computer computer) {
        List<SeleniumGlobalConfiguration> confs = new ArrayList<SeleniumGlobalConfiguration>();
        for (SeleniumGlobalConfiguration c : PluginImpl.getPlugin().getGlobalConfigurations()) {
            if (c.getMatcher().match(computer.getNode())) {
                confs.add(c);
            }
        }
        return confs;

    }

    public SeleniumGlobalConfiguration getConfiguration(String name) {
        validateAdmin();
        for (SeleniumGlobalConfiguration c : configurations) {
            if (name.equals(c.getName())) {
                return c;
            }
        }
        return null;
    }

    /**
     *
     * @param req
     *            StaplerRequest
     * @param rsp
     *            StaplerResponse to redirect with
     * @throws IOException
     *             if redirection goes wrong
     */
    public void doAddRedirect(StaplerRequest req, StaplerResponse rsp) throws IOException {
        validateAdmin();
        rsp.sendRedirect2("add");
    }

    /**
     * Check if the current user has selenium admin privileges.
     * Throws exception if false.
     *
     * Useful in stapler request methods to fail hard on insufficient privileges.
     *
     * @throws AccessDeniedException
     */
    public void validateAdmin() {
        Jenkins.getInstance().checkPermission(getRequiredPermission());
    }

    /**
     * Return true if the current user has selenium admin privileges.
     *
     * Useful in jelly files to check admin privileges to hide configurations without throwing an exception.
     *
     * @return True if the user is a selenium admin, false otherwise
     */
    public boolean isAdmin() {
        return Jenkins.getInstance().hasPermission(getRequiredPermission());
    }

    /**
     *
     * @param req
     *            StaplerRequest
     * @param rsp
     *            StaplerResponse to redirect with
     * @throws IOException
     *             if redirection goes wrong
     */
    public void doCreate(StaplerRequest req, StaplerResponse rsp) throws Exception {
        validateAdmin();
        SeleniumGlobalConfiguration conf = req.bindJSON(SeleniumGlobalConfiguration.class, req.getSubmittedForm());
        if (null == conf.getName() || conf.getName().trim().equals("")) {
            throw new Failure("You must specify a name for the configuration");
        }

        if (PluginImpl.getPlugin().hasGlobalConfiguration(conf.getName())) {
            throw new Failure(
                    "The configuration name you have chosen is already taken, please choose a unique name.");
        }

        PluginImpl.getPlugin().getGlobalConfigurations().add(conf);
        PluginImpl.getPlugin().save();
        rsp.sendRedirect2("configurations");
    }

    public Permission getRequiredPermission() {
        return SELENIUM_ADMIN;
    }

    public DescriptorExtensionList<SeleniumNodeConfiguration, ConfigurationDescriptor> getConfigTypes() {
        return SeleniumNodeConfiguration.all();
    }

    public DescriptorExtensionList<SeleniumConfigurationMatcher, MatcherDescriptor> getMatcherTypes() {
        return SeleniumConfigurationMatcher.all();
    }

    public DescriptorExtensionList<HostnameResolver, HostnameResolverDescriptor> getResolverTypes() {
        return HostnameResolver.all();
    }

    public Map<Computer, List<SeleniumGlobalConfiguration>> getComputers() {
        Map<Computer, List<SeleniumGlobalConfiguration>> cps = new TreeMap<Computer, List<SeleniumGlobalConfiguration>>(
                new Comparator<Computer>() {

                    public int compare(Computer o1, Computer o2) {
                        return o1.getName().compareTo(o2.getName());
                    }
                });
        for (Computer c : Jenkins.getInstance().getComputers()) {
            List<SeleniumGlobalConfiguration> confs = getGlobalConfigurationForComputer(c);
            if (confs != null && confs.size() > 0) {
                cps.put(c, confs);
            }
        }
        return cps;
    }

    public Channel getHubChannel() {
        return channel;
    }

    public SeleniumConfigurationMatcher getDefaultMatcher() {
        return new MatchAllMatcher();
    }

    public SeleniumNodeConfiguration getDefaultConfiguration() {
        validateAdmin();
        List<WebDriverBrowser> browsers = new ArrayList<WebDriverBrowser>();
        browsers.add(new hudson.plugins.selenium.configuration.browser.webdriver.IEBrowser(1, null, null));
        browsers.add(new hudson.plugins.selenium.configuration.browser.webdriver.FirefoxBrowser(5, null, null));
        browsers.add(new hudson.plugins.selenium.configuration.browser.webdriver.ChromeBrowser(5, null, null));
        return new CustomWDConfiguration(4445, null, browsers, null, 5);
    }

    /**
     */
    public void replaceGlobalConfigurations(String name, SeleniumGlobalConfiguration conf) throws IOException {
        validateAdmin();
        removeGlobalConfigurations(name, false);
        configurations.add(conf);
        PluginImpl.getPlugin().save();
    }

    /**
     */
    private void removeGlobalConfigurations(String name, boolean save) throws IOException {
        Iterator<SeleniumGlobalConfiguration> it = configurations.iterator();
        while (it.hasNext()) {
            SeleniumGlobalConfiguration conf = it.next();
            if (conf.getName().equals(name)) {
                it.remove();
                for (Computer c : Jenkins.getInstance().getComputers()) {
                    conf.remove(c);
                }
                if (save) {
                    save();
                }

                // there should only be one config with that name
                return;
            }
        }
    }

    public HttpResponse doRestart() throws IOException {
        validateAdmin();
        try {
            channel.call(new StopHubCallable());
        } catch (Exception e) {
            throw new IOException(e);
        }
        channel.close();
        try {
            startHub();
            waitForHubLaunch();
        } catch (Exception e) {
            throw new IOException(e);
        }
        return HttpResponses.forwardToPreviousPage();
    }
}