Skip to content

Commit

Permalink
Merge pull request #2487 from Vlatombe/JENKINS-21486
Browse files Browse the repository at this point in the history
[JENKINS-21486] Fix plugin dependencies resolution, second round.
  • Loading branch information
daniel-beck committed Jul 30, 2016
2 parents 86e14dd + dff8e80 commit b27b150
Show file tree
Hide file tree
Showing 12 changed files with 297 additions and 32 deletions.
13 changes: 7 additions & 6 deletions core/src/main/java/hudson/PluginManager.java
Expand Up @@ -505,7 +505,7 @@ public void run(Reactor session) throws Exception {

// schedule execution of loading plugins
for (final PluginWrapper p : activePlugins.toArray(new PluginWrapper[activePlugins.size()])) {
g.followedBy().notFatal().attains(PLUGINS_PREPARED).add("Loading plugin " + p.getShortName(), new Executable() {
g.followedBy().notFatal().attains(PLUGINS_PREPARED).add(String.format("Loading plugin %s v%s (%s)", p.getLongName(), p.getVersion(), p.getShortName()), new Executable() {
public void run(Reactor session) throws Exception {
try {
p.resolvePluginDependencies();
Expand Down Expand Up @@ -844,7 +844,8 @@ public void dynamicLoad(File arc, boolean removeExisting) throws IOException, In
// so existing plugins can't be depending on this newly deployed one.

plugins.add(p);
activePlugins.add(p);
if (p.isActive())
activePlugins.add(p);
synchronized (((UberClassLoader) uberClassLoader).loaded) {
((UberClassLoader) uberClassLoader).loaded.clear();
}
Expand Down Expand Up @@ -1867,22 +1868,22 @@ public static final class PluginCycleDependenciesMonitor extends AdministrativeM

private transient volatile boolean isActive = false;

private transient volatile List<String> pluginsWithCycle;
private transient volatile List<PluginWrapper> pluginsWithCycle;

public boolean isActivated() {
if(pluginsWithCycle == null){
pluginsWithCycle = new ArrayList<String>();
pluginsWithCycle = new ArrayList<>();
for (PluginWrapper p : Jenkins.getInstance().getPluginManager().getPlugins()) {
if(p.hasCycleDependency()){
pluginsWithCycle.add(p.getShortName());
pluginsWithCycle.add(p);
isActive = true;
}
}
}
return isActive;
}

public List<String> getPluginsWithCycle() {
public List<PluginWrapper> getPluginsWithCycle() {
return pluginsWithCycle;
}
}
Expand Down
174 changes: 150 additions & 24 deletions core/src/main/java/hudson/PluginWrapper.java
Expand Up @@ -26,43 +26,51 @@

import com.google.common.collect.ImmutableSet;
import hudson.PluginManager.PluginInstanceStore;
import hudson.model.AdministrativeMonitor;
import hudson.model.Api;
import hudson.model.ModelObject;
import jenkins.MissingDependencyException;
import jenkins.YesNoMaybe;
import jenkins.model.Jenkins;
import hudson.model.UpdateCenter;
import hudson.model.UpdateSite;
import hudson.util.VersionNumber;
import org.jvnet.localizer.ResourceBundleHolder;
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.interceptor.RequirePOST;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.LogFactory;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Closeable;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;

import static java.util.logging.Level.WARNING;
import static org.apache.commons.io.FilenameUtils.getBaseName;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.LogFactory;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.interceptor.RequirePOST;

import java.util.Enumeration;
import java.util.jar.JarFile;
import java.util.logging.Level;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

/**
* Represents a Jenkins plug-in and associated control information
Expand All @@ -88,6 +96,12 @@
*/
@ExportedBean
public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
/**
* A plugin won't be loaded unless his declared dependencies are present and match the required minimal version.
* This can be set to false to disable the version check (legacy behaviour)
*/
private static final boolean ENABLE_PLUGIN_DEPENDENCIES_VERSION_CHECK = Boolean.parseBoolean(System.getProperty(PluginWrapper.class.getName()+"." + "dependenciesVersionCheck.enabled", "true"));

/**
* {@link PluginManager} to which this belongs to.
*/
Expand Down Expand Up @@ -142,6 +156,12 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
private final List<Dependency> dependencies;
private final List<Dependency> optionalDependencies;

public List<String> getDependencyErrors() {
return Collections.unmodifiableList(dependencyErrors);
}

private final transient List<String> dependencyErrors = new ArrayList<>();

/**
* Is this plugin bundled in jenkins.war?
*/
Expand Down Expand Up @@ -229,7 +249,7 @@ public Dependency(String s) {

@Override
public String toString() {
return shortName + " (" + version + ")";
return shortName + " (" + version + ")" + (optional ? " optional" : "");
}
}

Expand Down Expand Up @@ -394,6 +414,21 @@ private String getVersionOf(Manifest manifest) {
return "???";
}

/**
* Returns the required Jenkins core version of this plugin.
* @return the required Jenkins core version of this plugin.
* @since XXX
*/
@Exported
public @CheckForNull String getRequiredCoreVersion() {
String v = manifest.getMainAttributes().getValue("Jenkins-Version");
if (v!= null) return v;

v = manifest.getMainAttributes().getValue("Hudson-Version");
if (v!= null) return v;
return null;
}

/**
* Returns the version number of this plugin
*/
Expand Down Expand Up @@ -524,20 +559,71 @@ public boolean hasLicensesXml() {
* thrown if one or several mandatory dependencies doesn't exists.
*/
/*package*/ void resolvePluginDependencies() throws IOException {
List<Dependency> missingDependencies = new ArrayList<>();
if (ENABLE_PLUGIN_DEPENDENCIES_VERSION_CHECK) {
String requiredCoreVersion = getRequiredCoreVersion();
if (requiredCoreVersion == null) {
LOGGER.warning(shortName + " doesn't declare required core version.");
} else {
VersionNumber actualVersion = Jenkins.getVersion();
if (actualVersion.isOlderThan(new VersionNumber(requiredCoreVersion))) {
dependencyErrors.add(Messages.PluginWrapper_obsoleteCore(Jenkins.getVersion().toString(), requiredCoreVersion));
}
}
}
// make sure dependencies exist
for (Dependency d : dependencies) {
if (parent.getPlugin(d.shortName) == null)
missingDependencies.add(d);
}
if (!missingDependencies.isEmpty())
throw new MissingDependencyException(this.shortName, missingDependencies);
PluginWrapper dependency = parent.getPlugin(d.shortName);
if (dependency == null) {
PluginWrapper failedDependency = NOTICE.getPlugin(d.shortName);
if (failedDependency != null) {
dependencyErrors.add(Messages.PluginWrapper_failed_to_load_dependency(failedDependency.getLongName(), d.version));
break;
} else {
dependencyErrors.add(Messages.PluginWrapper_missing(d.shortName, d.version));
}
} else {
if (dependency.isActive()) {
if (isDependencyObsolete(d, dependency)) {
dependencyErrors.add(Messages.PluginWrapper_obsolete(dependency.getLongName(), dependency.getVersion(), d.version));
}
} else {
if (isDependencyObsolete(d, dependency)) {
dependencyErrors.add(Messages.PluginWrapper_disabledAndObsolete(dependency.getLongName(), dependency.getVersion(), d.version));
} else {
dependencyErrors.add(Messages.PluginWrapper_disabled(dependency.getLongName()));
}
}

}
}
// add the optional dependencies that exists
for (Dependency d : optionalDependencies) {
if (parent.getPlugin(d.shortName) != null)
dependencies.add(d);
PluginWrapper dependency = parent.getPlugin(d.shortName);
if (dependency != null && dependency.isActive()) {
if (isDependencyObsolete(d, dependency)) {
dependencyErrors.add(Messages.PluginWrapper_obsolete(dependency.getLongName(), dependency.getVersion(), d.version));
} else {
dependencies.add(d);
}
}
}
if (!dependencyErrors.isEmpty()) {
NOTICE.addPlugin(this);
StringBuilder messageBuilder = new StringBuilder();
messageBuilder.append(Messages.PluginWrapper_failed_to_load_plugin(getLongName(), getVersion())).append(System.lineSeparator());
for (Iterator<String> iterator = dependencyErrors.iterator(); iterator.hasNext(); ) {
String dependencyError = iterator.next();
messageBuilder.append(" - ").append(dependencyError);
if (iterator.hasNext()) {
messageBuilder.append(System.lineSeparator());
}
}
throw new IOException(messageBuilder.toString());
}
}

private boolean isDependencyObsolete(Dependency d, PluginWrapper dependency) {
return ENABLE_PLUGIN_DEPENDENCIES_VERSION_CHECK && dependency.getVersionNumber().isOlderThan(new VersionNumber(d.version));
}

/**
Expand Down Expand Up @@ -645,6 +731,46 @@ public boolean isPinningForcingOldVersion() {
return false;
}

@Extension
public final static PluginWrapperAdministrativeMonitor NOTICE = new PluginWrapperAdministrativeMonitor();

/**
* Administrative Monitor for failed plugins
*/
public static final class PluginWrapperAdministrativeMonitor extends AdministrativeMonitor {
private final Map<String, PluginWrapper> plugins = new HashMap<>();

void addPlugin(PluginWrapper plugin) {
plugins.put(plugin.shortName, plugin);
}

public boolean isActivated() {
return !plugins.isEmpty();
}

public Collection<PluginWrapper> getPlugins() {
return plugins.values();
}

public PluginWrapper getPlugin(String shortName) {
return plugins.get(shortName);
}

/**
* Depending on whether the user said "dismiss" or "correct", send him to the right place.
*/
public void doAct(StaplerRequest req, StaplerResponse rsp) throws IOException {
if(req.hasParameter("correct")) {
rsp.sendRedirect(req.getContextPath()+"/pluginManager");

}
}

public static PluginWrapperAdministrativeMonitor get() {
return AdministrativeMonitor.all().get(PluginWrapperAdministrativeMonitor.class);
}
}

//
//
// Action methods
Expand Down
7 changes: 7 additions & 0 deletions core/src/main/resources/hudson/Messages.properties
Expand Up @@ -73,4 +73,11 @@ ProxyConfiguration.Success=Success

Functions.NoExceptionDetails=No Exception details

PluginWrapper.missing={0} v{1} is missing. To fix, install v{1} or later.
PluginWrapper.failed_to_load_plugin={0} v{1} failed to load.
PluginWrapper.failed_to_load_dependency={0} v{1} failed to load. Fix this plugin first.
PluginWrapper.disabledAndObsolete={0} v{1} is disabled and older than required. To fix, install v{2} or later and enable it.
PluginWrapper.disabled={0} is disabled. To fix, enable it.
PluginWrapper.obsolete={0} v{1} is older than required. To fix, install v{2} or later.
PluginWrapper.obsoleteCore=You must update Jenkins from v{0} to v{1} or later to run this plugin.
TcpSlaveAgentListener.PingAgentProtocol.displayName=Ping protocol
Expand Up @@ -28,7 +28,7 @@ THE SOFTWARE.
${%PluginCycles}
<ul>
<j:forEach var="p" items="${it.pluginsWithCycle}">
<li><j:out value="${p}"/></li>
<li><j:out value="${p.longName} v${p.version}"/></li>
</j:forEach>
</ul>
</div>
Expand Down
@@ -0,0 +1,22 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<div class="error">
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}">
<div style="float:right">
<f:submit name="correct" value="${%Correct}"/>
</div>
</form>
There are dependency errors loading some plugins:
<ul>
<j:forEach items="${it.plugins}" var="plugin">
<li>${plugin.longName} v${plugin.version}
<ul>
<j:forEach items="${plugin.dependencyErrors}" var="d">
<li>${d}</li>
</j:forEach>
</ul>
</li>
</j:forEach>
</ul>
</div>
</j:jelly>

0 comments on commit b27b150

Please sign in to comment.