提交 e3b92c8c 编写于 作者: K Kohsuke Kawaguchi

Merge remote-tracking branch 'origin/master'

Conflicts:
	changelog.html
......@@ -17,6 +17,9 @@ build
# vim
*~
# ctags
tags
# OS X
.DS_Store
......
......@@ -54,9 +54,28 @@ Upcoming changes</a>
<!-- Record your changes in the trunk here. -->
<div id="trunk" style="display:none"><!--=TRUNK-BEGIN=-->
<ul class=image>
<li class=>
</ul>
<li class="rfe">
Plugin Manager UI changes to prevent users from enabling/disabling/uninstalling plugins at the "wrong" time.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-23150">issue 23150</a>)
<li class="bug">
bytecode-compatibility-transformer produces malformed bytecode.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-28781">issue 28781</a>)
<li class="bug">
Properly handle <code>RuntimeException</code>s in run retention policy handler calls.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-29888">issue 29888</a>)
<li class="bug">
Prevent <code>NullPointerException</code> in CLI if Jenkins cannot find the specified job
or a job with the nearest name.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-30742">issue 30742</a>)
<li class="bug">
Do not show <i>REST API</i> link for pages, which have no API handlers.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-29014">issue 29014</a>)
<li class="bug">
JS alert preventing to leave a configuration page without changes.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-21720">issue 21720</a>)
<li class="bug">
JS error triggered by collapsing build history widget.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-30569">issue 30569</a>)
</div><!--=TRUNK-END=-->
<h3><a name=v1.632>What's new in 1.632</a> (2015/10/05)</h3>
<ul class=image>
......@@ -72,6 +91,12 @@ Upcoming changes</a>
<li class="rfe">
ConsoleLogFilter wasn't truly global
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-30777">issue 30777</a>)
<li class="rfe">
API changes: <code>hudson.Util.isOverridden()</code> now supports protected methods.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-30002">issue 30002</a>)
<li class="bug">
Sidepanel controls with confirmation (<code>lib/layout/task</code>) did not assign the proper CSS style.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-30787">issue 30787</a>)
</ul>
<h3><a name=v1.631>What's new in 1.631</a> (2015/09/27)</h3>
<ul class=image>
......
......@@ -200,7 +200,7 @@ THE SOFTWARE.
<dependency>
<groupId>org.jenkins-ci</groupId>
<artifactId>bytecode-compatibility-transformer</artifactId>
<version>1.5</version>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci</groupId>
......
......@@ -119,6 +119,8 @@ import org.xml.sax.helpers.DefaultHandler;
import static hudson.init.InitMilestone.*;
import hudson.model.DownloadService;
import hudson.util.FormValidation;
import static java.util.logging.Level.SEVERE;
import static java.util.logging.Level.WARNING;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
......@@ -520,9 +522,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 synchronized 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.
*
......@@ -779,6 +805,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
*/
public void doInstall(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
boolean dynamicLoad = req.getParameter("dynamicLoad")!=null;
final List<Future<UpdateCenter.UpdateCenterJob>> deployJobs = new ArrayList<>();
Enumeration<String> en = req.getParameterNames();
while (en.hasMoreElements()) {
......@@ -811,9 +838,36 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
if (p == null) {
throw new Failure("No such plugin: " + n);
}
p.deploy(dynamicLoad);
deployJobs.add(p.deploy(dynamicLoad));
}
}
// Fire a one-off thread to wait for the plugins to be deployed and then
// refresh the dependant plugins list.
new Thread() {
@Override
public void run() {
INSTALLING: while (true) {
for (Future<UpdateCenter.UpdateCenterJob> deployJob : deployJobs) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
LOGGER.log(SEVERE, "Unexpected error while waiting for some plugins to install. Plugin Manager state may be invalid. Please restart Jenkins ASAP.", e);
}
if (!deployJob.isCancelled() && !deployJob.isDone()) {
// One of the plugins is not installing/canceled, so
// go back to sleep and try again in a while.
continue INSTALLING;
}
}
// All the plugins are installed. It's now safe to refresh.
resolveDependantPlugins();
break;
}
}
}.start();
rsp.sendRedirect("../updateCenter/");
}
......
......@@ -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
......@@ -632,8 +685,14 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
@RequirePOST
public HttpResponse doDoUninstall() throws IOException {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
Jenkins jenkins = Jenkins.getActiveInstance();
jenkins.checkPermission(Jenkins.ADMINISTER);
archive.delete();
// Redo who depends on who.
jenkins.getPluginManager().resolveDependantPlugins();
return HttpResponses.redirectViaContextPath("/pluginManager/installed"); // send back to plugin manager
}
......
......@@ -26,6 +26,7 @@ package hudson;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.NativeLong;
import edu.umd.cs.findbugs.annotations.SuppressWarnings;
import hudson.Proc.LocalProc;
import hudson.model.TaskListener;
......@@ -33,6 +34,7 @@ import hudson.os.PosixAPI;
import hudson.util.QuotedStringTokenizer;
import hudson.util.VariableResolver;
import hudson.util.jna.WinIOException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.time.FastDateFormat;
import org.apache.tools.ant.BuildException;
......@@ -40,14 +42,18 @@ import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Chmod;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.types.FileSet;
import jnr.posix.FileStat;
import jnr.posix.POSIX;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
......@@ -70,12 +76,14 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import hudson.util.jna.Kernel32Utils;
import static hudson.util.jna.GNUCLibrary.LIBC;
import java.security.DigestInputStream;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.commons.codec.digest.DigestUtils;
/**
......@@ -1415,16 +1423,36 @@ public class Util {
}
/**
* Checks if the public method defined on the base type with the given arguments
* are overridden in the given derived type.
* Checks if the method defined on the base type with the given arguments
* is overridden in the given derived type.
*/
public static boolean isOverridden(@Nonnull Class base, @Nonnull Class derived, @Nonnull String methodName, @Nonnull Class... types) {
return !getMethod(base, methodName, types).equals(getMethod(derived, methodName, types));
}
private static Method getMethod(@Nonnull Class clazz, @Nonnull String methodName, @Nonnull Class... types) {
Method res = null;
try {
return !base.getMethod(methodName, types).equals(
derived.getMethod(methodName,types));
res = clazz.getDeclaredMethod(methodName, types);
// private, static or final methods can not be overridden
if (res != null && (Modifier.isPrivate(res.getModifiers()) || Modifier.isFinal(res.getModifiers())
|| Modifier.isStatic(res.getModifiers()))) {
res = null;
}
} catch (NoSuchMethodException e) {
// Method not found in clazz, let's search in superclasses
Class superclass = clazz.getSuperclass();
if (superclass != null) {
res = getMethod(superclass, methodName, types);
}
} catch (SecurityException e) {
throw new AssertionError(e);
}
if (res == null) {
throw new IllegalArgumentException(
String.format("Method %s not found in %s (or it is private, final or static)", methodName, clazz.getName()));
}
return res;
}
/**
......
......@@ -2248,8 +2248,11 @@ public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends A
public static AbstractProject resolveForCLI(
@Argument(required=true,metaVar="NAME",usage="Job name") String name) throws CmdLineException {
AbstractProject item = Jenkins.getInstance().getItemByFullName(name, AbstractProject.class);
if (item==null)
throw new CmdLineException(null,Messages.AbstractItem_NoSuchJobExists(name,AbstractProject.findNearest(name).getFullName()));
if (item==null) {
AbstractProject project = AbstractProject.findNearest(name);
throw new CmdLineException(null, project == null ? Messages.AbstractItem_NoSuchJobExistsWithoutSuggestion(name)
: Messages.AbstractItem_NoSuchJobExists(name, project.getFullName()));
}
return item;
}
......
......@@ -1803,11 +1803,9 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
try {
getParent().logRotate();
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Failed to rotate log",e);
} catch (InterruptedException e) {
LOGGER.log(Level.SEVERE, "Failed to rotate log",e);
}
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Failed to rotate log",e);
}
} finally {
onEndBuilding();
}
......
......@@ -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,351 @@ 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;
}
var pluginI18n = select('.plugins.i18n');
function i18n(messageId) {
return pluginI18n.getAttribute('data-' + messageId);
}
// 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.getAttribute('data-plugin-id');
plugins[pluginId] = pluginTR;
}
function getPluginTR(pluginId) {
return plugins[pluginId];
}
function getPluginName(pluginId) {
var pluginTR = getPluginTR(pluginId);
if (pluginTR) {
return pluginTR.getAttribute('data-plugin-name');
} else {
return pluginId;
}
}
function processSpanSet(spans) {
var ids = [];
for (var i = 0; i < spans.length; i++) {
var span = spans[i];
var pluginId = span.getAttribute('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) {
// If the only dependant is jenkins-core (it's a bundle plugin), then lets
// treat it like all its dependants are disabled. We're really only interested in
// dependant plugins in this case.
// Note: This does not cover "implied" dependencies ala detached plugins. See https://goo.gl/lQHrUh
if (dependantIds.length === 1 && dependantIds[0] === 'jenkins-core') {
pluginTR.addClassName('all-dependants-disabled');
return;
}
for (var i = 0; i < dependantIds.length; i++) {
var dependantId = dependantIds[i];
if (dependantId === 'jenkins-core') {
// Jenkins core is always enabled. So, make sure it's not possible to disable/uninstall
// any plugins that it "depends" on. (we sill have bundled plugins)
pluginTR.removeClassName('all-dependants-disabled');
return;
}
// The dependant is a plugin....
var dependantPluginTr = getPluginTR(dependantId);
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">' + i18n('cannot-enable') + '</div><div class="subtitle">' + i18n('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.getAttribute('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;
} if (pluginTR.hasClassName('has-dependants')) {
if (!pluginTR.hasClassName('all-dependants-disabled')) {
var dependantIds = pluginMetadata.dependantIds;
// If the only dependant is jenkins-core (it's a bundle plugin), then lets
// treat it like all its dependants are disabled. We're really only interested in
// dependant plugins in this case.
// Note: This does not cover "implied" dependencies ala detached plugins. See https://goo.gl/lQHrUh
if (dependantIds.length === 1 && dependantIds[0] === 'jenkins-core') {
pluginTR.addClassName('all-dependants-disabled');
return false;
}
var dependantsDiv = pluginMetadata.dependantsDiv;
var dependantSpans = pluginMetadata.dependants;
infoContainer.update('<div class="title">' + i18n('cannot-disable') + '</div><div class="subtitle">' + i18n('enabled-dependants') + '.</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 dependantId = dependantSpan.getAttribute('data-plugin-id');
if (dependantId === 'jenkins-core') {
// show the span
dependantSpan.setStyle({display: 'inline-block'});
} else {
var depPluginTR = getPluginTR(dependantId);
var depPluginMetadata = depPluginTR.jenkinsPluginMetadata;
if (depPluginMetadata.enableInput.checked) {
// It's enabled ... show the span
dependantSpan.setStyle({display: 'inline-block'});
} else {
// It's disabled ... hide 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">' + i18n('cannot-uninstall') + '</div><div class="subtitle">' + i18n('installed-dependants') + '.</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();
});
}());
\ No newline at end of file
......@@ -35,10 +35,23 @@ THE SOFTWARE.
${%Filter}:
<input type="text" id="filter-box"/>
</div>
<j:if test="${app.updateCenter.isRestartRequiredForCompletion()}">
<div class="alert alert-warning"><strong>${%Warning}</strong>: ${%requires.restart}</div>
</j:if>
<local:tabBar page="installed" xmlns:local="/hudson/PluginManager" />
<div class="pane-frame">
<table id="plugins" class="sortable pane bigtable stripped-odd">
<div class="plugins i18n"
data-cannot-enable="${%This plugin cannot be enabled}"
data-cannot-disable="${%This plugin cannot be disabled}"
data-cannot-uninstall="${%This plugin cannot be uninstalled}"
data-disabled-dependencies="${%It has one or more disabled dependencies}"
data-enabled-dependants="${%It has one or more enabled dependants}"
data-installed-dependants="${%It has one or more installed dependants}"
/>
<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 +68,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 +119,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>
......
......@@ -20,4 +20,5 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
wiki.url=http://wiki.jenkins-ci.org/display/JENKINS/Pinned+Plugins
downgradeTo=Downgrade to {0}
\ No newline at end of file
downgradeTo=Downgrade to {0}
requires.restart=This Jenkins instance requires a restart. Changing the state of plugins at this time is strongly discouraged. Restart Jenkins before proceeding.
\ No newline at end of file
......@@ -101,7 +101,9 @@ THE SOFTWARE.
</tr>
</j:if>
</l:pane>
<script defer="true">
updateBuildHistory("${it.baseUrl}/buildHistory/ajax",${it.nextBuildNumberToFetch});
</script>
<j:if test="!empty(it.nextBuildNumberToFetch)">
<script defer="true">
updateBuildHistory("${it.baseUrl}/buildHistory/ajax", ${it.nextBuildNumberToFetch});
</script>
</j:if>
</j:jelly>
......@@ -33,9 +33,6 @@
}
function initConfirm() {
// Timeout is needed since some events get sent on page load for some reason.
// Shouldn't hurt anything for this to only start monitoring events after a few millis;.
setTimeout(function() {
var configForm = document.getElementsByName("config");
if (configForm.length > 0) {
configForm = configForm[0]
......@@ -78,7 +75,6 @@
for ( var i = 0; i < inputs.length; i++) {
$(inputs[i]).on('input', confirm);
}
}, 100);
}
window.onbeforeunload = confirmExit;
......
......@@ -41,6 +41,19 @@ function updateListBox(listBox,url,config) {
}
Behaviour.specify("SELECT.select", 'select', 1000, function(e) {
function hasChanged(selectEl, originalValue) {
var firstValue = selectEl.options[0].value;
var selectedValue = selectEl.value;
if (originalValue == "" && selectedValue == firstValue) {
// There was no value pre-selected but after the call to updateListBox the first value is selected by
// default. This must not be considered a change.
return false;
} else {
return originalValue != selectedValue;
}
};
// controls that this SELECT box depends on
refillOnChange(e,function(params) {
var value = e.value;
......@@ -60,7 +73,9 @@ Behaviour.specify("SELECT.select", 'select', 1000, function(e) {
fireEvent(e,"filled"); // let other interested parties know that the items have changed
// if the update changed the current selection, others listening to this control needs to be notified.
if (e.value!=value) fireEvent(e,"change");
if (hasChanged(e, value)) {
fireEvent(e,"change");
}
}
});
});
......
......@@ -254,7 +254,9 @@ ${h.initPageVariables(context)}
<div class="col-md-6" id="footer"></div>
<div class="col-md-18">
<span class="page_generated">${%Page generated}: <i:formatDate value="${h.getCurrentTime()}" type="both" dateStyle="medium" timeStyle="medium"/></span>
<span class="rest_api"><a href="api/">REST API</a></span>
<j:if test="${!empty(it.api)}">
<span class="rest_api"><a href="api/">REST API</a></span>
</j:if>
<span class="jenkins_ver"><a href="${h.getFooterURL()}">Jenkins ver. ${h.version}</a></span>
<j:if test="${extensionsAvailable}">
<j:forEach var="pd" items="${h.pageDecorators}">
......
......@@ -167,7 +167,7 @@ THE SOFTWARE.
<j:choose>
<j:when test="${requiresConfirmation and not attrs.onClick}">
<l:confirmationLink href="${href}" post="${post}" message="${confirmationMessage ?: title}">
<l:confirmationLink class="task-icon-link" href="${href}" post="${post}" message="${confirmationMessage ?: title}">
<j:choose>
<j:when test="${iconMetadata != null}">
<l:icon class="${iconMetadata.classSpec}" style="width: 24px; height: 24px; margin: 2px;" />
......
/*
* The MIT License
*
* Copyright (c) 2015, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.util;
import org.junit.Test;
import static org.junit.Assert.*;
import hudson.Util;
/**
* Test for {@link Util.isOverridden} method.
*/
public class IsOverriddenTest {
/**
* Test that a method is found by isOverridden even when it is inherited from an intermediate class.
*/
@Test
public void isOverriddenTest() {
assertTrue(Util.isOverridden(Base.class, Derived.class, "method"));
assertTrue(Util.isOverridden(Base.class, Intermediate.class, "method"));
assertFalse(Util.isOverridden(Base.class, Base.class, "method"));
assertTrue(Util.isOverridden(Base.class, Intermediate.class, "setX", Object.class));
assertTrue(Util.isOverridden(Base.class, Intermediate.class, "getX"));
}
/**
* Negative test.
* Trying to check for a method which does not exist in the hierarchy,
*/
@Test(expected = IllegalArgumentException.class)
public void isOverriddenNegativeTest() {
Util.isOverridden(Base.class, Derived.class, "method2");
}
/**
* Do not inspect private methods.
*/
@Test(expected = IllegalArgumentException.class)
public void avoidPrivateMethodsInspection() {
Util.isOverridden(Base.class, Intermediate.class, "aPrivateMethod");
}
public abstract class Base<T> {
protected abstract void method();
private void aPrivateMethod() {}
public void setX(T t) {}
public T getX() { return null; }
}
public abstract class Intermediate extends Base<Integer> {
protected void method() {}
private void aPrivateMethod() {}
public void setX(Integer i) {}
public Integer getX() { return 0; }
}
public class Derived extends Intermediate {}
}
......@@ -47,7 +47,8 @@ import hudson.triggers.Trigger
import hudson.triggers.TriggerDescriptor;
import hudson.util.StreamTaskListener;
import hudson.util.OneShotEvent
import jenkins.model.Jenkins;
import jenkins.model.Jenkins
import org.junit.Assert;
import org.jvnet.hudson.test.HudsonTestCase
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.TestExtension;
......@@ -56,6 +57,7 @@ import org.jvnet.hudson.test.recipes.PresetData.DataSet
import org.apache.commons.io.FileUtils;
import org.junit.Assume;
import org.jvnet.hudson.test.MockFolder
import org.kohsuke.args4j.CmdLineException
/**
* @author Kohsuke Kawaguchi
......@@ -588,6 +590,25 @@ public class AbstractProjectTest extends HudsonTestCase {
assert project.triggers().size() == 1
}
@Issue("JENKINS-30742")
public void testResolveForCLI() {
try {
AbstractProject not_found = AbstractProject.resolveForCLI("never_created");
fail("Exception should occur before!");
} catch (CmdLineException e) {
assert e.getMessage().contentEquals("No such job \u2018never_created\u2019 exists.");
}
AbstractProject project = jenkins.createProject(FreeStyleProject.class, "never_created");
try {
AbstractProject not_found = AbstractProject.resolveForCLI("never_created1");
fail("Exception should occur before!");
} catch (CmdLineException e) {
assert e.getMessage().contentEquals("No such job \u2018never_created1\u2019 exists. Perhaps you meant \u2018never_created\u2019?")
}
}
static class MockBuildTriggerThrowsNPEOnStart<Item> extends Trigger {
@Override
public void start(hudson.model.Item project, boolean newInstance) { throw new NullPointerException(); }
......
/*
* The MIT License
*
* Copyright (c) 2015, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson;
import com.gargoylesoftware.htmlunit.html.DomElement;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlElementUtil;
import com.gargoylesoftware.htmlunit.html.HtmlInput;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlTableRow;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.recipes.WithPlugin;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
*/
public class PluginManagerInstalledGUITest {
@Rule
public JenkinsRule jenkinsRule = new JenkinsRule();
@Test
@WithPlugin("tasks.jpi")
public void test_enable_disable_uninstall() throws IOException, SAXException {
InstalledPlugins installedPlugins = new InstalledPlugins();
InstalledPlugin tasksPlugin = installedPlugins.get("tasks");
InstalledPlugin cvsPlugin = installedPlugins.get("cvs");
tasksPlugin.assertHasNoDependants();
cvsPlugin.assertHasDependants();
// Tasks plugin should be enabled and it should be possible to disable it
// because no other plugins depend on it.
tasksPlugin.assertEnabled();
tasksPlugin.assertEnabledStateChangeable();
tasksPlugin.assertUninstallable();
// CVS plugin should be enabled, but it should not be possible to disable or uninstall it
// because the tasks plugin depends on it.
cvsPlugin.assertEnabled();
cvsPlugin.assertEnabledStateNotChangeable();
cvsPlugin.assertNotUninstallable();
// Disable the tasks plugin
tasksPlugin.clickEnabledWidget();
// Now the tasks plugin should be disabled, but it should be possible to re-enable it
// and it should still be uninstallable.
tasksPlugin.assertNotEnabled(); // this is different to earlier
tasksPlugin.assertEnabledStateChangeable();
tasksPlugin.assertUninstallable();
// The CVS plugin should still be enabled, but it should now be possible to disable it because
// the tasks plugin is no longer enabled. Should still not be possible to uninstall it.
cvsPlugin.assertEnabled();
cvsPlugin.assertEnabledStateChangeable(); // this is different to earlier
cvsPlugin.assertNotUninstallable();
// Disable the cvs plugin
cvsPlugin.clickEnabledWidget();
// Now it should NOT be possible to change the enable state of the tasks plugin because one
// of the plugins it depends on (the CVS plugin) is not enabled.
tasksPlugin.assertNotEnabled();
tasksPlugin.assertEnabledStateNotChangeable(); // this is different to earlier
tasksPlugin.assertUninstallable();
}
private class InstalledPlugins {
private final List<InstalledPlugin> installedPlugins;
private InstalledPlugins () throws IOException, SAXException {
JenkinsRule.WebClient webClient = jenkinsRule.createWebClient();
HtmlPage installedPage = webClient.goTo("pluginManager/installed");
// Note for debugging... simply print installedPage to get the JenkinsRule
// Jenkins URL and then add a long Thread.sleep here. It's useful re being
// able to see what the code is testing.
DomElement pluginsTable = installedPage.getElementById("plugins");
HtmlElement tbody = pluginsTable.getElementsByTagName("TBODY").get(0);
installedPlugins = new ArrayList<>();
for (DomElement htmlTableRow : tbody.getChildElements()) {
installedPlugins.add(new InstalledPlugin((HtmlTableRow) htmlTableRow));
}
}
public InstalledPlugin get(String pluginId) {
for (InstalledPlugin plugin : installedPlugins) {
if (plugin.isPlugin(pluginId)) {
return plugin;
}
}
Assert.fail("Now pluginManager/installed row for plugin " + pluginId);
return null;
}
}
private class InstalledPlugin {
private final HtmlTableRow pluginRow;
public InstalledPlugin(HtmlTableRow pluginRow) {
this.pluginRow = pluginRow;
}
public String getId() {
return pluginRow.getAttribute("data-plugin-id");
}
public boolean isPlugin(String pluginId) {
return pluginId.equals(getId());
}
private HtmlInput getEnableWidget() {
HtmlElement input = pluginRow.getCells().get(0).getElementsByTagName("input").get(0);
return (HtmlInput) input;
}
public void assertEnabled() {
HtmlInput enableWidget = getEnableWidget();
Assert.assertTrue("Plugin '" + getId() + "' is expected to be enabled.", enableWidget.isChecked());
}
public void assertNotEnabled() {
HtmlInput enableWidget = getEnableWidget();
Assert.assertFalse("Plugin '" + getId() + "' is not expected to be enabled.", enableWidget.isChecked());
}
public void clickEnabledWidget() throws IOException {
HtmlInput enableWidget = getEnableWidget();
HtmlElementUtil.click(enableWidget);
}
public void assertEnabledStateChangeable() {
if (!hasDependants() && !hasDisabledDependency() && !allDependantsDisabled()) {
return;
}
if (allDependantsDisabled() && !hasDisabledDependency()) {
return;
}
Assert.fail("The enable/disable state of plugin '" + getId() + "' cannot be changed.");
}
public void assertEnabledStateNotChangeable() {
if (hasDependants() && !hasDisabledDependency() && !allDependantsDisabled()) {
return;
}
if (!hasDependants() && hasDisabledDependency()) {
return;
}
Assert.fail("The enable/disable state of plugin '" + getId() + "' cannot be changed.");
}
public void assertUninstallable() {
Assert.assertFalse("Plugin '" + getId() + "' cannot be uninstalled.", hasDependants());
}
public void assertNotUninstallable() {
Assert.assertTrue("Plugin '" + getId() + "' can be uninstalled.", hasDependants());
}
public void assertHasDependants() {
Assert.assertTrue(hasDependants());
}
public void assertHasNoDependants() {
Assert.assertFalse(hasDependants());
}
private boolean hasClassName(String className) {
String classAttribute = pluginRow.getAttribute("class");
Set<String> classes = new HashSet<>(Arrays.asList(classAttribute.split(" ")));
return classes.contains(className);
}
private boolean hasDisabledDependency() {
return hasClassName("has-disabled-dependency");
}
private boolean allDependantsDisabled() {
return hasClassName("all-dependants-disabled");
}
private boolean hasDependants() {
return hasClassName("has-dependants");
}
}
}
......@@ -26,7 +26,6 @@ package hudson.slaves;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
......@@ -40,6 +39,7 @@ import java.util.concurrent.TimeUnit;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.RandomlyFails;
public class CommandLauncherTest {
......@@ -57,8 +57,9 @@ public class CommandLauncherTest {
assertThat(log, not(containsString("ERROR: Process terminated with exit code 0")));
}
@RandomlyFails("Sometimes gets `EOFException: unexpected stream termination` before then on CI builder; maybe needs to wait in a loop for a message to appear?")
@Test
public void commandSuceedsWithoutChannel() throws Exception {
public void commandSucceedsWithoutChannel() throws Exception {
assumeTrue(!Functions.isWindows());
DumbSlave slave = createSlave("true");
......
......@@ -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 ========================= */
......@@ -1690,3 +1770,20 @@ table#legend-table td {
color: #f8f8f8;
border: 1px solid #e39280;
}
.i18n {
display: none;
}
.alert {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
}
.alert-warning {
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #faebcc;
}
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册