Only allow disable or uninstall when all dependants or dependencies are disabled/enabled

This commit is contained in:
tfennelly committed 2015-10-01 16:35:05 +01:00
1 parent 7fb4696a6f
commit 3b5424e507
6 files changed
+504 -11

No files matched your search

@@ -520,9 +520,33 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
}
}
// Redo who depends on who.
resolveDependantPlugins();
LOGGER.info("Plugin " + p.getShortName()+":"+p.getVersion() + " dynamically installed");
}
@Restricted(NoExternalUse.class)
public void resolveDependantPlugins() {
for (PluginWrapper plugin : plugins) {
Set<String> dependants = new HashSet<>();
for (PluginWrapper possibleDependant : plugins) {
// The plugin could have just been deleted. If so, it doesn't
// count as a dependant.
if (possibleDependant.isDeleted()) {
continue;
}
List<Dependency> dependencies = possibleDependant.getDependencies();
for (Dependency dependency : dependencies) {
if (dependency.shortName.equals(plugin.getShortName())) {
dependants.add(possibleDependant.getShortName());
}
}
}
plugin.setDependants(dependants);
}
}
/**
* If the war file has any "/WEB-INF/plugins/[*.jpi | *.hpi]", extract them into the plugin directory.
*
@@ -24,6 +24,7 @@
*/
package hudson;
import com.google.common.collect.ImmutableSet;
import hudson.PluginManager.PluginInstanceStore;
import hudson.model.Api;
import hudson.model.ModelObject;
@@ -40,7 +41,10 @@ import java.io.OutputStream;
import java.io.Closeable;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.jar.Manifest;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
@@ -57,6 +61,7 @@ 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
@@ -150,6 +155,54 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
*/
/*package*/ boolean isBundled;
/**
* List of plugins that depend on this plugin.
*/
private Set<String> dependants = Collections.emptySet();
/**
* The core can depend on a plugin if it is bundled. Sometimes it's the only thing that
* depends on the plugin e.g. UI support library bundle plugin.
*/
private static Set<String> CORE_ONLY_DEPENDANT = ImmutableSet.copyOf(Arrays.asList("jenkins-core"));
/**
* Set the list of components that depend on this plugin.
* @param dependants The list of components that depend on this plugin.
*/
public void setDependants(@Nonnull Set<String> dependants) {
this.dependants = dependants;
}
/**
* Get the list of components that depend on this plugin.
* @return The list of components that depend on this plugin.
*/
public @Nonnull Set<String> getDependants() {
if (isBundled && dependants.isEmpty()) {
return CORE_ONLY_DEPENDANT;
} else {
return dependants;
}
}
/**
* Does this plugin have anything that depends on it.
* @return {@code true} if something (Jenkins core, or another plugin) depends on this
* plugin, otherwise {@code false}.
*/
public boolean hasDependants() {
return (isBundled || !dependants.isEmpty());
}
/**
* Does this plugin depend on any other plugins.
* @return {@code true} if this plugin depends on other plugins, otherwise {@code false}.
*/
public boolean hasDependencies() {
return (dependencies != null && !dependencies.isEmpty());
}
@ExportedBean
public static final class Dependency {
@Exported
@@ -634,6 +687,10 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
public HttpResponse doDoUninstall() throws IOException {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
archive.delete();
// Redo who depends on who.
Jenkins.getActiveInstance().getPluginManager().resolveDependantPlugins();
return HttpResponses.redirectViaContextPath("/pluginManager/installed"); // send back to plugin manager
}
@@ -864,6 +864,9 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
System.currentTimeMillis()-itemListenerStart,l.getClass().getName()));
}
// All plugins are loaded. Now we can figure out who depends on who.
resolveDependantPlugins();
if (LOG_STARTUP_PERFORMANCE)
LOGGER.info(String.format("Took %dms for complete Jenkins startup",
System.currentTimeMillis()-start));
@@ -872,6 +875,19 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
}
}
private void resolveDependantPlugins() throws InterruptedException, ReactorException, IOException {
TaskGraphBuilder graphBuilder = new TaskGraphBuilder();
graphBuilder.add("Resolving Dependant Plugins Graph", new Executable() {
@Override
public void run(Reactor reactor) throws Exception {
pluginManager.resolveDependantPlugins();
}
});
executeReactor(null, graphBuilder);
}
/**
* Executes a reactor.
*
@@ -44,3 +44,305 @@ Behaviour.specify("#filter-box", '_table', 0, function(e) {
e.onkeyup = applyFilter;
});
/**
* Code for handling the enable/disable behavior based on plugin
* dependencies and dependants.
*/
(function(){
function selectAll(selector, element) {
if (element) {
return $(element).select(selector);
} else {
return Element.select(undefined, selector);
}
}
function select(selector, element) {
var elementsBySelector = selectAll(selector, element);
if (elementsBySelector.length > 0) {
return elementsBySelector[0];
} else {
return undefined;
}
}
/**
* Wait for document onload.
*/
Element.observe(window, "load", function() {
var pluginsTable = select('#plugins');
var pluginTRs = selectAll('.plugin', pluginsTable);
if (!pluginTRs) {
return;
}
// Create a map of the plugin rows, making it easy to index them.
var plugins = {};
for (var i = 0; i < pluginTRs.length; i++) {
var pluginTR = pluginTRs[i];
var pluginId = pluginTR.readAttribute('data-plugin-id');
plugins[pluginId] = pluginTR;
}
function getPluginTR(pluginId) {
return plugins[pluginId];
}
function getPluginName(pluginId) {
return getPluginTR(pluginId).readAttribute('data-plugin-name');
}
function processSpanSet(spans) {
var ids = [];
for (var i = 0; i < spans.length; i++) {
var span = spans[i];
var pluginId = span.readAttribute('data-plugin-id');
var pluginName = getPluginName(pluginId);
span.update(pluginName);
ids.push(pluginId);
}
return ids;
}
function markAllDependantsDisabled(pluginTR) {
var jenkinsPluginMetadata = pluginTR.jenkinsPluginMetadata;
var dependantIds = jenkinsPluginMetadata.dependantIds;
if (dependantIds) {
for (var i = 0; i < dependantIds.length; i++) {
var dependantPluginTr = getPluginTR(dependantIds[i]);
if (dependantPluginTr && dependantPluginTr.jenkinsPluginMetadata.enableInput.checked) {
// One of the plugins that depend on this plugin, is marked as enabled.
pluginTR.removeClassName('all-dependants-disabled');
return;
}
}
}
pluginTR.addClassName('all-dependants-disabled');
}
function markHasDisabledDependencies(pluginTR) {
var jenkinsPluginMetadata = pluginTR.jenkinsPluginMetadata;
var dependencyIds = jenkinsPluginMetadata.dependencyIds;
if (dependencyIds) {
for (var i = 0; i < dependencyIds.length; i++) {
var dependencyPluginTr = getPluginTR(dependencyIds[i]);
if (dependencyPluginTr && !dependencyPluginTr.jenkinsPluginMetadata.enableInput.checked) {
// One of the plugins that this plugin depend on, is marked as disabled.
pluginTR.addClassName('has-disabled-dependency');
return;
}
}
}
pluginTR.removeClassName('has-disabled-dependency');
}
function setEnableWidgetStates() {
for (var i = 0; i < pluginTRs.length; i++) {
markAllDependantsDisabled(pluginTRs[i]);
markHasDisabledDependencies(pluginTRs[i]);
}
}
function addDependencyInfoRow(pluginTR, infoTR) {
infoTR.addClassName('plugin-dependency-info');
pluginTR.insert({
after: infoTR
});
}
function removeDependencyInfoRow(pluginTR) {
var nextRows = pluginTR.nextSiblings();
if (nextRows && nextRows.length > 0) {
var nextRow = nextRows[0];
if (nextRow.hasClassName('plugin-dependency-info')) {
nextRow.remove();
}
}
}
function populateEnableDisableInfo(pluginTR, infoContainer) {
var pluginMetadata = pluginTR.jenkinsPluginMetadata;
// Remove all existing class info
infoContainer.removeAttribute('class');
infoContainer.addClassName('enable-state-info');
if (pluginTR.hasClassName('has-disabled-dependency')) {
var dependenciesDiv = pluginMetadata.dependenciesDiv;
var dependencySpans = pluginMetadata.dependencies;
infoContainer.update('<div class="title">This plugin cannot be enabled</div><div class="subtitle">It has one or more disabled dependencies.</div>');
// Go through each dependency <span> element. Show the spans where the dependency is
// disabled. Hide the others.
for (var i = 0; i < dependencySpans.length; i++) {
var dependencySpan = dependencySpans[i];
var pluginId = dependencySpan.readAttribute('data-plugin-id');
var depPluginTR = getPluginTR(pluginId);
var depPluginMetadata = depPluginTR.jenkinsPluginMetadata;
if (depPluginMetadata.enableInput.checked) {
// It's enabled ... hide the span
dependencySpan.setStyle({display: 'none'});
} else {
// It's disabled ... show the span
dependencySpan.setStyle({display: 'inline-block'});
}
}
dependenciesDiv.setStyle({display: 'inherit'});
infoContainer.appendChild(dependenciesDiv);
return true;
} else if (pluginTR.hasClassName('has-dependants')) {
if (!pluginTR.hasClassName('all-dependants-disabled')) {
var dependantsDiv = pluginMetadata.dependantsDiv;
var dependantSpans = pluginMetadata.dependants;
infoContainer.update('<div class="title">This plugin cannot be disabled</div><div class="subtitle">It has one or more enabled plugins that depend on it.</div>');
// Go through each dependant <span> element. Show the spans where the dependant is
// enabled. Hide the others.
for (var i = 0; i < dependantSpans.length; i++) {
var dependantSpan = dependantSpans[i];
var pluginId = dependantSpan.readAttribute('data-plugin-id');
var depPluginTR = getPluginTR(pluginId);
var depPluginMetadata = depPluginTR.jenkinsPluginMetadata;
if (depPluginMetadata.enableInput.checked) {
// It's disabled ... hide the span
dependantSpan.setStyle({display: 'inline-block'});
} else {
// It's enabled ... show the span
dependantSpan.setStyle({display: 'none'});
}
}
dependantsDiv.setStyle({display: 'inherit'});
infoContainer.appendChild(dependantsDiv);
return true;
}
}
return false;
}
function populateUninstallInfo(pluginTR, infoContainer) {
// Remove all existing class info
infoContainer.removeAttribute('class');
infoContainer.addClassName('uninstall-state-info');
if (pluginTR.hasClassName('has-dependants')) {
var pluginMetadata = pluginTR.jenkinsPluginMetadata;
var dependantsDiv = pluginMetadata.dependantsDiv;
var dependantSpans = pluginMetadata.dependants;
infoContainer.update('<div class="title">This plugin cannot be uninstalled</div><div class="subtitle">It has one or more plugins that depend on it.</div>');
// Go through each dependant <span> element. Show them all.
for (var i = 0; i < dependantSpans.length; i++) {
var dependantSpan = dependantSpans[i];
dependantSpan.setStyle({display: 'inline-block'});
}
dependantsDiv.setStyle({display: 'inherit'});
infoContainer.appendChild(dependantsDiv);
return true;
}
return false;
}
function initPluginRowHandling(pluginTR) {
var enableInput = select('.enable input', pluginTR);
var dependenciesDiv = select('.dependency-list', pluginTR);
var dependantsDiv = select('.dependant-list', pluginTR);
var enableTD = select('td.enable', pluginTR);
var uninstallTD = select('td.uninstall', pluginTR);
pluginTR.jenkinsPluginMetadata = {
enableInput: enableInput,
dependenciesDiv: dependenciesDiv,
dependantsDiv: dependantsDiv
};
if (dependenciesDiv) {
pluginTR.jenkinsPluginMetadata.dependencies = selectAll('span', dependenciesDiv);
pluginTR.jenkinsPluginMetadata.dependencyIds = processSpanSet(pluginTR.jenkinsPluginMetadata.dependencies);
}
if (dependantsDiv) {
pluginTR.jenkinsPluginMetadata.dependants = selectAll('span', dependantsDiv);
pluginTR.jenkinsPluginMetadata.dependantIds = processSpanSet(pluginTR.jenkinsPluginMetadata.dependants);
}
// Setup event handlers...
// Toggling of the enable/disable checkbox requires a check and possible
// change of visibility on the same checkbox on other plugins.
Element.observe(enableInput, 'click', function() {
setEnableWidgetStates();
});
//
var infoTR = document.createElement("tr");
var infoTD = document.createElement("td");
var infoDiv = document.createElement("div");
infoTR.appendChild(infoTD)
infoTD.appendChild(infoDiv)
infoTD.setAttribute('colspan', '6'); // This is the cell that all info will be added to.
infoDiv.setStyle({display: 'inherit'});
// We don't want the info row to appear immediately. We wait for e.g. 1 second and if the mouse
// is still in there (hasn't left the cell) then we show. The following code is for clearing the
// show timeout where the mouse has left before the timeout has fired.
var showInfoTimeout = undefined;
function clearShowInfoTimeout() {
if (showInfoTimeout) {
clearTimeout(showInfoTimeout);
}
showInfoTimeout = undefined;
}
// Handle mouse in/out of the enable/disable cell (left most cell).
Element.observe(enableTD, 'mouseenter', function() {
showInfoTimeout = setTimeout(function() {
showInfoTimeout = undefined;
infoDiv.update('');
if (populateEnableDisableInfo(pluginTR, infoDiv)) {
addDependencyInfoRow(pluginTR, infoTR);
}
}, 1000);
});
Element.observe(enableTD, 'mouseleave', function() {
clearShowInfoTimeout();
removeDependencyInfoRow(pluginTR);
});
// Handle mouse in/out of the uninstall cell (right most cell).
Element.observe(uninstallTD, 'mouseenter', function() {
showInfoTimeout = setTimeout(function() {
showInfoTimeout = undefined;
infoDiv.update('');
if (populateUninstallInfo(pluginTR, infoDiv)) {
addDependencyInfoRow(pluginTR, infoTR);
}
}, 1000);
});
Element.observe(uninstallTD, 'mouseleave', function() {
clearShowInfoTimeout();
removeDependencyInfoRow(pluginTR);
});
}
for (var i = 0; i < pluginTRs.length; i++) {
initPluginRowHandling(pluginTRs[i]);
}
setEnableWidgetStates();
});
}());
@@ -38,7 +38,7 @@ THE SOFTWARE.
<local:tabBar page="installed" xmlns:local="/hudson/PluginManager" />
<div class="pane-frame">
<table id="plugins" class="sortable pane bigtable stripped-odd">
<table id="plugins" class="pane bigtable sortable stripped-odd">
<j:choose>
<j:when test="${empty(app.pluginManager.plugins) and empty(app.pluginManager.failedPlugins)}">
<tr><td>
@@ -55,16 +55,16 @@ THE SOFTWARE.
<th width="1">${%Uninstall}</th>
</tr>
<j:forEach var="p" items="${app.pluginManager.plugins}">
<tr class="plugin">
<tr class="plugin ${p.hasDependants()?'has-dependants':''} ${p.isDeleted()?'deleted':''}" data-plugin-id="${p.shortName}" data-plugin-name="${p.displayName}">
<j:set var="state" value="${p.enabled?'true':null}"/>
<td class="center pane" data="${state}">
<td class="center pane enable" data="${state}">
<input type="checkbox" checked="${state}" onclick="flip(event)"
url="plugin/${p.shortName}"
original="${p.active?'true':'false'}"/>
</td>
<td class="pane">
<td class="pane details">
<div>
<a href="${p.url}">
<a href="${p.url}" class="display-name">
${p.updateInfo.displayName?:p.displayName}
</a>
</div>
@@ -106,16 +106,30 @@ THE SOFTWARE.
<a href="${%wiki.url}" target="_blank"><l:icon class="icon-help icon-sm" style="vertical-align:top"/></a>
</j:if>
</td>
<td class="center pane">
<td class="center pane uninstall">
<j:choose>
<j:when test="${p.isDeleted()}">
<p>${%Uninstallation pending}</p>
</j:when>
<j:when test="${!p.isBundled()}">
<form method="post" action="plugin/${p.shortName}/uninstall">
<input type="submit" value="${%Uninstall}"/>
</form>
</j:when>
<j:otherwise>
<j:if test="${p.hasDependants()}">
<div class="dependant-list">
<j:forEach var="dependant" items="${p.dependants}">
<span data-plugin-id="${dependant}"></span>
</j:forEach>
</div>
</j:if>
<j:if test="${p.hasDependencies()}">
<div class="dependency-list">
<j:forEach var="dependency" items="${p.dependencies}">
<span data-plugin-id="${dependency.shortName}"></span>
</j:forEach>
</div>
</j:if>
<form method="post" action="plugin/${p.shortName}/uninstall">
<input class="uninstall" type="submit" value="${%Uninstall}"/>
</form>
</j:otherwise>
</j:choose>
</td>
</tr>
+80
View File
@@ -1221,6 +1221,86 @@ TABLE.fingerprint-in-build TD {
background-color: #e8eeee;
}
#plugins input.uninstall {
font-size: 11px;
padding: 3px 5px;
border-radius: 1px;
font-family: Helvetica, Arial, sans-serif;
font-weight: bold;
background-color: #4b758b;
color: #eee;
border: 1px solid #5788a1;
}
#plugins tr.has-dependants input.uninstall {
background-color: #e0e0e0;
border: 1px solid #cccccc;
color: #505050;
background-color: white;
}
#plugins tr.all-dependants-disabled .enable input {
pointer-events: auto;
opacity: 1.0;
visibility: visible;
}
#plugins tr.has-dependants input, #plugins tr.has-disabled-dependency .enable input {
pointer-events: none;
opacity: 0.2;
}
#plugins tr.has-disabled-dependency .enable input {
opacity: 0.4;
}
#plugins tr.deleted input {
visibility: hidden !important;
}
#plugins .dependant-list, #plugins .dependency-list {
display: none;
}
#plugins .enable-state-info, #plugins .uninstall-state-info {
padding: 5px 20px;
max-width: 70%;
border: solid 1px #f0ad4e;
border-radius: 3px;
text-align: center;
}
#plugins .enable-state-info {
float: left;
}
#plugins .uninstall-state-info {
float: right;
}
#plugins .plugin-dependency-info .title, #plugins .plugin-dependency-info .subtitle {
opacity: 0.7;
}
#plugins .plugin-dependency-info .title {
margin-bottom: 5px;
font-size: larger;
font-weight: bolder;
}
#plugins .plugin-dependency-info span {
margin: 5px 5px 0px 0px;
background-color: #337ab7;
display: inline-block;
padding: .2em .6em .3em;
font-size: 75%;
font-weight: 700;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: .25em;
}
/* ========================= repeatable elements ========================= */