/**
* *****************************************************************************
*
* Copyright (c) 2012 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the Eclipse Public License v1.0 which
* accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Winston Prakash
*
******************************************************************************
*/
package org.eclipse.hudson.init;
import hudson.ProxyConfiguration;
import hudson.Util;
import hudson.XmlFile;
import hudson.markup.MarkupFormatter;
import hudson.security.Permission;
import hudson.util.DaemonThreadFactory;
import hudson.util.HudsonIsLoading;
import hudson.util.VersionNumber;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.eclipse.hudson.WebAppController;
import org.eclipse.hudson.plugins.InstalledPluginManager;
import org.eclipse.hudson.plugins.InstalledPluginManager.InstalledPluginInfo;
import org.eclipse.hudson.plugins.PluginInstallationJob;
import org.eclipse.hudson.plugins.UpdateSiteManager;
import org.eclipse.hudson.plugins.UpdateSiteManager.AvailablePluginInfo;
import org.eclipse.hudson.security.HudsonSecurityEntitiesHolder;
import org.eclipse.hudson.security.HudsonSecurityManager;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides support for initial setup during first run. Gives opportunity to
* Hudson Admin to - Install mandatory, featured and recommended plugins -
* Update compatiblity, featured and recommended plugins suitable for current
* Hudson - Provide Authentication if needed - Setup proxy if required
*
* @author Winston Prakash
*/
final public class InitialSetup {
private Logger logger = LoggerFactory.getLogger(InitialSetup.class);
private final File pluginsDir;
private final ServletContext servletContext;
private final UpdateSiteManager updateSiteManager;
private final InstalledPluginManager installedPluginManager;
private List<AvailablePluginInfo> installedObsoletePlugins = new ArrayList<AvailablePluginInfo>();
private List<AvailablePluginInfo> installedRecommendedPlugins = new ArrayList<AvailablePluginInfo>();
private List<AvailablePluginInfo> installableRecommendedPlugins = new ArrayList<AvailablePluginInfo>();
private List<AvailablePluginInfo> updatableRecommendedPlugins = new ArrayList<AvailablePluginInfo>();
private List<AvailablePluginInfo> installedFeaturedPlugins = new ArrayList<AvailablePluginInfo>();
private List<AvailablePluginInfo> installableFeaturedPlugins = new ArrayList<AvailablePluginInfo>();
private List<AvailablePluginInfo> updatableFeaturedPlugins = new ArrayList<AvailablePluginInfo>();
private List<AvailablePluginInfo> installedCompatibilityPlugins = new ArrayList<AvailablePluginInfo>();
private List<AvailablePluginInfo> installableCompatibilityPlugins = new ArrayList<AvailablePluginInfo>();
private List<AvailablePluginInfo> updatableCompatibilityPlugins = new ArrayList<AvailablePluginInfo>();
private ProxyConfiguration proxyConfig;
private ExecutorService installerService = Executors.newSingleThreadExecutor(
new DaemonThreadFactory(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("Initial setup installer thread");
return t;
}
}));
private final HudsonSecurityManager hudsonSecurityManager;
private static XmlFile initSetupFile;
private final File hudsonHomeDir;
private boolean proxyNeeded = false;
private final List<PluginInstallationJob> installationsJobs = new CopyOnWriteArrayList<PluginInstallationJob>();
private static ClassLoader outerClassLoader;
private static ClassLoader initialClassLoader;
private static Thread initThread;
private static int highInitThreadNumber = 0;
private static InitialSetup INSTANCE;
public InitialSetup(File homeDir, ServletContext context) throws MalformedURLException, IOException {
hudsonHomeDir = homeDir;
pluginsDir = new File(homeDir, "plugins");
servletContext = context;
hudsonSecurityManager = HudsonSecurityEntitiesHolder.getHudsonSecurityManager();
proxyConfig = new ProxyConfiguration(homeDir);
updateSiteManager = new UpdateSiteManager("default", hudsonHomeDir, proxyConfig);
installedPluginManager = new InstalledPluginManager(pluginsDir);
initSetupFile = new XmlFile(new File(homeDir, "initSetup.xml"));
refreshUpdateCenterMetadataCache();
check();
// This is only created once during startup, so is effectively a singleton
INSTANCE = this;
}
public static InitialSetup getLastInitialSetup() {
return INSTANCE;
}
public boolean needsInitSetup() throws IOException {
if (initSetupFile.exists()) {
String str = FileUtils.readFileToString(initSetupFile.getFile());
return !str.trim().contains("Hudson 3.3");
} else {
if (Boolean.getBoolean("skipInitSetup")) {
try {
initSetupFile.write("Hudson 3.3 Initial Setup Done");
} catch (IOException ex) {
logger.error(ex.getLocalizedMessage());
}
return false;
} else {
return true;
}
}
}
public boolean needsAdminLogin() {
return !hudsonSecurityManager.hasPermission(Permission.HUDSON_ADMINISTER);
}
public ServletContext getServletContext() {
return servletContext;
}
public ProxyConfiguration getProxyConfig() {
return proxyConfig;
}
public MarkupFormatter getMarkupFormatter() {
return hudsonSecurityManager.getMarkupFormatter();
}
public List<AvailablePluginInfo> getObsoletePlugins() {
return installedObsoletePlugins;
}
public List<AvailablePluginInfo> getInstalledRecommendedPlugins() {
return installedRecommendedPlugins;
}
public List<AvailablePluginInfo> getInstallableRecommendedPlugins() {
return installableRecommendedPlugins;
}
public List<AvailablePluginInfo> getUpdatableRecommendedPlugins() {
return updatableRecommendedPlugins;
}
public List<AvailablePluginInfo> getInstalledFeaturedPlugins() {
return installedFeaturedPlugins;
}
public List<AvailablePluginInfo> getInstallableFeaturedPlugins() {
return installableFeaturedPlugins;
}
public List<AvailablePluginInfo> getUpdatableFeaturedPlugins() {
return updatableFeaturedPlugins;
}
public List<AvailablePluginInfo> getInstallableCompatibilityPlugins() {
return installableCompatibilityPlugins;
}
public List<AvailablePluginInfo> getInstalledCompatibilityPlugins() {
return installedCompatibilityPlugins;
}
public List<AvailablePluginInfo> getUpdatableCompatibilityPlugins() {
return updatableCompatibilityPlugins;
}
public InstalledPluginInfo getInstalled(AvailablePluginInfo plugin) {
return installedPluginManager.getInstalledPlugin(plugin.getName());
}
public List<AvailablePluginInfo> getUpdatablePlugins() {
List<AvailablePluginInfo> updatablePlugins = new ArrayList<AvailablePluginInfo>();
Set<String> installedPluginNames = installedPluginManager.getInstalledPluginNames();
Set<String> availablePluginNames = updateSiteManager.getAvailablePluginNames();
for (String pluginName : availablePluginNames) {
AvailablePluginInfo availablePlugin = updateSiteManager.getAvailablePlugin(pluginName);
if (installedPluginNames.contains(pluginName)) {
InstalledPluginInfo installedPlugin = installedPluginManager.getInstalledPlugin(pluginName);
if (!availablePlugin.isObsolete() && isNewerThan(availablePlugin.getVersion(), installedPlugin.getVersion())) {
updatablePlugins.add(availablePlugin);
}
}
}
return updatablePlugins;
}
public Future<PluginInstallationJob> install(AvailablePluginInfo plugin) {
for (AvailablePluginInfo dep : getNeededDependencies(plugin)) {
install(dep);
}
return submitInstallationJob(plugin);
}
public boolean isProxyNeeded() {
return proxyNeeded;
}
public HttpResponse doinstallPlugin(@QueryParameter String pluginName) {
if (!hudsonSecurityManager.hasPermission(Permission.HUDSON_ADMINISTER)) {
return HttpResponses.forbidden();
}
AvailablePluginInfo plugin = updateSiteManager.getAvailablePlugin(pluginName);
try {
PluginInstallationJob installJob = null;
// If the plugin is already being installed, don't schedule another. Make the search thread safe
List<PluginInstallationJob> jobs = Collections.synchronizedList(installationsJobs);
synchronized (jobs) {
for (PluginInstallationJob job : jobs) {
if (job.getName().equals(pluginName)) {
installJob = job;
}
}
}
// No previous install of the plugn, create new
if (installJob == null) {
Future<PluginInstallationJob> newJob = install(plugin);
installJob = newJob.get();
}
if (!installJob.getStatus()) {
return new ErrorHttpResponse("Plugin " + pluginName + " could not be installed. " + installJob.getErrorMsg());
}
} catch (Exception ex) {
return new ErrorHttpResponse("Plugin " + pluginName + " could not be installed. " + ex.getLocalizedMessage());
}
reCheck();
return HttpResponses.ok();
}
public HttpResponse doDisablePlugin(@QueryParameter String pluginName) {
if (!hudsonSecurityManager.hasPermission(Permission.HUDSON_ADMINISTER)) {
return HttpResponses.forbidden();
}
InstalledPluginInfo plugin = installedPluginManager.getInstalledPlugin(pluginName);
try {
plugin.setEnable(false);
} catch (Exception ex) {
return new ErrorHttpResponse("Plugin " + pluginName + " could not be disabled. " + ex.getLocalizedMessage());
}
return HttpResponses.ok();
}
public HttpResponse doProxyConfigure(
@QueryParameter("proxy.server") String server,
@QueryParameter("proxy.port") String port,
@QueryParameter("proxy.noProxyFor") String noProxyFor,
@QueryParameter("proxy.userName") String userName,
@QueryParameter("proxy.password") String password,
@QueryParameter("proxy.authNeeded") String authNeeded) throws IOException {
if (!hudsonSecurityManager.hasPermission(Permission.HUDSON_ADMINISTER)) {
return HttpResponses.forbidden();
}
try {
boolean proxySet = setProxy(server, port, noProxyFor, userName, password, authNeeded);
if (proxySet) {
proxyConfig.save();
}
// Try opening a URL and see if the proxy works fine
proxyConfig.openUrl(new URL("http://www.google.com"));
} catch (IOException ex) {
return new ErrorHttpResponse(ex.getLocalizedMessage());
}
return HttpResponses.ok();
}
public HttpResponse doFinish() {
try {
initSetupFile.write("Hudson 3.3 Initial Setup Done");
} catch (IOException ex) {
logger.error(ex.getLocalizedMessage());
}
installerService.shutdownNow();
invokeHudson();
return HttpResponses.ok();
}
private static class OuterClassLoader extends ClassLoader {
OuterClassLoader(ClassLoader parent) {
super(parent);
}
}
public void invokeHudson() {
invokeHudson(false);
}
public void invokeHudson(boolean restart) {
final WebAppController controller = WebAppController.get();
if (initialClassLoader == null) {
initialClassLoader = getClass().getClassLoader();
}
Class hudsonIsLoadingClass;
try {
outerClassLoader = new OuterClassLoader(initialClassLoader);
hudsonIsLoadingClass = outerClassLoader.loadClass("hudson.util.HudsonIsLoading");
HudsonIsLoading hudsonIsLoading = (HudsonIsLoading) hudsonIsLoadingClass.newInstance();
Class runnableClass = outerClassLoader.loadClass("org.eclipse.hudson.init.InitialRunnable");
Constructor ctor = runnableClass.getDeclaredConstructors()[0];
ctor.setAccessible(true);
InitialRunnable initialRunnable = (InitialRunnable) ctor.newInstance(controller, logger, hudsonHomeDir, servletContext, restart);
controller.install(hudsonIsLoading);
initThread = new Thread(initialRunnable, "hudson initialization thread " + (++highInitThreadNumber));
initThread.setContextClassLoader(outerClassLoader);
initThread.start();
} catch (Exception ex) {
logger.error("Hudson failed to load!!!", ex);
}
/**
* Above replaces these lines controller.install(new HudsonIsLoading());
*
* new Thread("hudson initialization thread") { }.start();
*/
}
public static ClassLoader getHudsonContextClassLoader() {
return outerClassLoader;
}
private static class ErrorHttpResponse implements HttpResponse {
private String message;
ErrorHttpResponse(String message) {
this.message = message;
}
@Override
public void generateResponse(StaplerRequest sr, StaplerResponse rsp, Object o) throws IOException, ServletException {
rsp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
rsp.setContentType("text/plain;charset=UTF-8");
PrintWriter w = new PrintWriter(rsp.getWriter());
w.println(message);
w.close();
}
}
private boolean setProxy(String server, String port, String noProxyFor,
String userName, String password, String authNeeded) throws IOException {
server = Util.fixEmptyAndTrim(server);
if ((server != null) && !"".equals(server)) {
// If port is not specified assume it is port 80 (usual default for HTTP port)
int portNumber = 80;
if (!"".equals(Util.fixNull(port))) {
portNumber = Integer.parseInt(Util.fixNull(port));
}
boolean proxyAuthNeeded = "on".equals(Util.fixNull(authNeeded));
if (!proxyAuthNeeded) {
userName = "";
password = "";
}
proxyConfig.configure(server, portNumber, Util.fixEmptyAndTrim(noProxyFor),
Util.fixEmptyAndTrim(userName), Util.fixEmptyAndTrim(password), "on".equals(Util.fixNull(authNeeded)));
return true;
} else {
proxyConfig.getXmlFile().delete();
proxyConfig.name = null;
return false;
}
}
private Future<PluginInstallationJob> submitInstallationJob(AvailablePluginInfo plugin) {
PluginInstallationJob newJob = new PluginInstallationJob(plugin, pluginsDir, proxyConfig);
installationsJobs.add(newJob);
return installerService.submit(newJob, newJob);
}
private boolean isNewerThan(String availableVersion, String installedVersion) {
try {
return new VersionNumber(installedVersion).compareTo(new VersionNumber(availableVersion)) < 0;
} catch (IllegalArgumentException e) {
// couldn't parse as the version number.
return false;
}
}
void reCheck() {
installedObsoletePlugins.clear();
installedRecommendedPlugins.clear();
installableRecommendedPlugins.clear();
updatableRecommendedPlugins.clear();
installedFeaturedPlugins.clear();
installableFeaturedPlugins.clear();
updatableFeaturedPlugins.clear();
installableCompatibilityPlugins.clear();
installedCompatibilityPlugins.clear();
updatableCompatibilityPlugins.clear();
installedPluginManager.loadInstalledPlugins();
check();
}
private void check() {
if (!pluginsDir.exists()) {
pluginsDir.mkdirs();
}
Set<String> installedPluginNames = installedPluginManager.getInstalledPluginNames();
Set<String> availablePluginNames = updateSiteManager.getAvailablePluginNames();
for (String pluginName : availablePluginNames) {
AvailablePluginInfo availablePlugin = updateSiteManager.getAvailablePlugin(pluginName);
if (installedPluginNames.contains(pluginName)) {
//Installed
InstalledPluginInfo installedPlugin = installedPluginManager.getInstalledPlugin(pluginName);
if (availablePlugin.getType().equals(UpdateSiteManager.COMPATIBILITY)) {
//Installed Compatibility Plugin
if (isNewerThan(availablePlugin.getVersion(), installedPlugin.getVersion())) {
//Updatabale Compatibility Plugin update needed
updatableCompatibilityPlugins.add(availablePlugin);
} else {
//Installed Compatibility Plugin. No updates available
installedCompatibilityPlugins.add(availablePlugin);
}
} else if (availablePlugin.getType().equals(UpdateSiteManager.FEATURED)) {
if (isNewerThan(availablePlugin.getVersion(), installedPlugin.getVersion())) {
//Updatabale featured Plugin update needed
updatableFeaturedPlugins.add(availablePlugin);
} else {
//Installed featured Plugin. No updates available
installedFeaturedPlugins.add(availablePlugin);
}
} else if (availablePlugin.getType().equals(UpdateSiteManager.OBSOLETE)) {
//Installed obsolete Plugin.
installedObsoletePlugins.add(availablePlugin);
}else if (availablePlugin.getType().equals(UpdateSiteManager.RECOMMENDED)) {
if (isNewerThan(availablePlugin.getVersion(), installedPlugin.getVersion())) {
//Updatabale recommended Plugin update needed
updatableRecommendedPlugins.add(availablePlugin);
} else {
//Installed recommended Plugin. No updates available
installedRecommendedPlugins.add(availablePlugin);
}
}
} else {
//Not installed
if (availablePlugin.getType().equals(UpdateSiteManager.COMPATIBILITY)) {
//Mandatory Plugin. Need to be installed
installableCompatibilityPlugins.add(availablePlugin);
}
if (availablePlugin.getType().equals(UpdateSiteManager.FEATURED)) {
//Featured Plugin. Available for installation
installableFeaturedPlugins.add(availablePlugin);
}
if (availablePlugin.getType().equals(UpdateSiteManager.RECOMMENDED)) {
//Recommended Plugin. Available for installation
installableRecommendedPlugins.add(availablePlugin);
}
}
}
}
private List<AvailablePluginInfo> getNeededDependencies(AvailablePluginInfo pluginInfo) {
List<AvailablePluginInfo> deps = new ArrayList<AvailablePluginInfo>();
if ((pluginInfo != null) && (pluginInfo.getDependencies().size() > 0)) {
for (Map.Entry<String, String> e : pluginInfo.getDependencies().entrySet()) {
AvailablePluginInfo depPlugin = updateSiteManager.getAvailablePlugin(e.getKey());
if (depPlugin != null) {
VersionNumber requiredVersion = new VersionNumber(e.getValue());
// Is the plugin installed already? If not, add it.
InstalledPluginInfo current = installedPluginManager.getInstalledPlugin(depPlugin.getName());
if (current == null) {
deps.add(depPlugin);
} else if (current.isOlderThan(requiredVersion)) {
deps.add(depPlugin);
}
} else {
logger.error("Could not find " + e.getKey() + " which is required by " + pluginInfo.getDisplayName());
}
}
}
return deps;
}
protected void refreshUpdateCenterMetadataCache() throws IOException {
try {
updateSiteManager.refreshFromUpdateSite();
return;
} catch (Exception exc) {
proxyNeeded = true;
logger.info("Could not fetch update center metadata from " + updateSiteManager.getUpdateSiteUrl() + ". Using bundled update center metadata.");
}
URL updateCenterJsonUrl = servletContext.getResource("/WEB-INF/update-center.json");
if (updateCenterJsonUrl != null) {
long lastModified = updateCenterJsonUrl.openConnection().getLastModified();
File localCacheFile = new File(hudsonHomeDir, "updates/default.json");
if (!localCacheFile.exists() || (localCacheFile.lastModified() < lastModified)) {
InputStream urlStream = null;
String jsonStr = null;
try {
urlStream = updateCenterJsonUrl.openStream();
jsonStr = org.apache.commons.io.IOUtils.toString(urlStream);
} finally {
IOUtils.closeQuietly(urlStream);
}
jsonStr = jsonStr.trim();
if (jsonStr.startsWith("updateCenter.post(")) {
jsonStr = jsonStr.substring("updateCenter.post(".length());
}
if (jsonStr.endsWith(");")) {
jsonStr = jsonStr.substring(0, jsonStr.lastIndexOf(");"));
}
FileUtils.writeStringToFile(localCacheFile, jsonStr);
localCacheFile.setLastModified(lastModified);
updateSiteManager.refresh();
}
}
}
}