diff --git a/.gitignore b/.gitignore index 3cde4d575d..5c8b1b7ac8 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ out .project build +# VSCode workspace file +*.code-workspace + # vim *~ *.swp diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index e9b443acba..a2275d7d1f 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -1831,7 +1831,7 @@ public class Functions { * from {@link ConsoleAnnotatorFactory}s and {@link ConsoleAnnotationDescriptor}s. */ public static String generateConsoleAnnotationScriptAndStylesheet() { - String cp = Stapler.getCurrentRequest().getContextPath(); + String cp = Stapler.getCurrentRequest().getContextPath() + Jenkins.RESOURCE_PATH; StringBuilder buf = new StringBuilder(); for (ConsoleAnnotatorFactory f : ConsoleAnnotatorFactory.all()) { String path = cp + "/extensionList/" + ConsoleAnnotatorFactory.class.getName() + "/" + f.getClass().getName(); diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index ac2fcd5507..576c2b96af 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -25,9 +25,6 @@ package hudson; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import hudson.security.ACLContext; -import jenkins.ExtensionRefreshException; -import jenkins.util.SystemProperties; import hudson.PluginWrapper.Dependency; import hudson.init.InitMilestone; import hudson.init.InitStrategy; @@ -37,22 +34,26 @@ import hudson.model.AbstractModelObject; import hudson.model.AdministrativeMonitor; import hudson.model.Api; import hudson.model.Descriptor; +import hudson.model.DownloadService; import hudson.model.Failure; import hudson.model.ItemGroupMixIn; import hudson.model.UpdateCenter; -import hudson.model.UpdateSite; import hudson.model.UpdateCenter.DownloadJob; import hudson.model.UpdateCenter.InstallationJob; +import hudson.model.UpdateSite; import hudson.security.ACL; +import hudson.security.ACLContext; import hudson.security.Permission; import hudson.security.PermissionScope; import hudson.util.CyclicGraphDetector; import hudson.util.CyclicGraphDetector.CycleDetectedException; +import hudson.util.FormValidation; import hudson.util.PersistedList; import hudson.util.Service; import hudson.util.VersionNumber; import hudson.util.XStream2; import jenkins.ClassLoaderReflectionToolkit; +import jenkins.ExtensionRefreshException; import jenkins.InitReactorRunner; import jenkins.MissingDependencyException; import jenkins.RestartRequiredException; @@ -60,12 +61,12 @@ import jenkins.YesNoMaybe; import jenkins.install.InstallState; import jenkins.install.InstallUtil; import jenkins.model.Jenkins; +import jenkins.security.CustomClassFilter; +import jenkins.util.SystemProperties; import jenkins.util.io.OnMaster; import jenkins.util.xml.RestrictiveEntityResolver; - import net.sf.json.JSONArray; import net.sf.json.JSONObject; - import org.acegisecurity.Authentication; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.FileUploadException; @@ -83,7 +84,9 @@ import org.jvnet.hudson.reactor.Reactor; import org.jvnet.hudson.reactor.ReactorException; import org.jvnet.hudson.reactor.TaskBuilder; import org.jvnet.hudson.reactor.TaskGraphBuilder; +import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.DoNotUse; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.HttpRedirect; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; @@ -95,6 +98,10 @@ 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.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; @@ -102,6 +109,7 @@ import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; +import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.File; import java.io.FilenameFilter; @@ -109,10 +117,12 @@ import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.lang.reflect.Method; +import java.net.JarURLConnection; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; +import java.net.URLConnection; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -123,6 +133,7 @@ import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.ServiceLoader; import java.util.Set; import java.util.TreeMap; import java.util.UUID; @@ -130,32 +141,15 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Future; +import java.util.function.Supplier; +import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.logging.Level; import java.util.logging.Logger; -import org.xml.sax.Attributes; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; import static hudson.init.InitMilestone.*; -import hudson.model.DownloadService; -import hudson.util.FormValidation; -import java.io.ByteArrayInputStream; -import java.net.JarURLConnection; -import java.net.URLConnection; -import java.util.ServiceLoader; -import java.util.function.Supplier; -import java.util.jar.JarEntry; - -import static java.util.logging.Level.FINE; -import static java.util.logging.Level.INFO; -import static java.util.logging.Level.SEVERE; -import static java.util.logging.Level.WARNING; -import jenkins.security.CustomClassFilter; -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; +import static java.util.logging.Level.*; /** * Manages {@link PluginWrapper}s. @@ -935,8 +929,15 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas @Restricted(NoExternalUse.class) public synchronized void resolveDependantPlugins() { for (PluginWrapper plugin : plugins) { + // Set of optional dependants plugins of plugin + Set optionalDependants = new HashSet<>(); Set dependants = new HashSet<>(); for (PluginWrapper possibleDependant : plugins) { + // No need to check if plugin is dependant of itself + if(possibleDependant.getShortName().equals(plugin.getShortName())) { + continue; + } + // The plugin could have just been deleted. If so, it doesn't // count as a dependant. if (possibleDependant.isDeleted()) { @@ -946,10 +947,20 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas for (Dependency dependency : dependencies) { if (dependency.shortName.equals(plugin.getShortName())) { dependants.add(possibleDependant.getShortName()); + + // If, in addition, the dependency is optional, add to the optionalDependants list + if (dependency.optional) { + optionalDependants.add(possibleDependant.getShortName()); + } + + // already know possibleDependant depends on plugin, no need to continue with the rest of + // dependencies. We continue with the next possibleDependant + break; } } } plugin.setDependants(dependants); + plugin.setOptionalDependants(optionalDependants); } } @@ -1829,6 +1840,31 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas return new MetadataCache(); } + /** + * Disable a list of plugins using a strategy for their dependants plugins. + * @param strategy the strategy regarding how the dependant plugins are processed + * @param plugins the list of plugins + * @return the list of results for every plugin and their dependant plugins. + * @throws IOException see {@link PluginWrapper#disable()} + */ + public @NonNull List disablePlugins(@NonNull PluginWrapper.PluginDisableStrategy strategy, @NonNull List plugins) throws IOException { + // Where we store the results of each plugin disablement + List results = new ArrayList<>(plugins.size()); + + // Disable all plugins passed + for (String pluginName : plugins) { + PluginWrapper plugin = this.getPlugin(pluginName); + + if (plugin == null) { + results.add(new PluginWrapper.PluginDisableResult(pluginName, PluginWrapper.PluginDisableStatus.NO_SUCH_PLUGIN, Messages.PluginWrapper_NoSuchPlugin(pluginName))); + } else { + results.add(plugin.disable(strategy)); + } + } + + return results; + } + @Restricted(NoExternalUse.class) // table.jelly public static final class MetadataCache { private final Map data = new HashMap<>(); diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java index 24d90aaa45..e282fd365b 100644 --- a/core/src/main/java/hudson/PluginWrapper.java +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -25,17 +25,18 @@ package hudson; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; import hudson.PluginManager.PluginInstanceStore; import hudson.model.AdministrativeMonitor; import hudson.model.Api; import hudson.model.ModelObject; -import java.nio.file.Files; -import java.nio.file.InvalidPathException; -import jenkins.YesNoMaybe; -import jenkins.model.Jenkins; import hudson.model.UpdateCenter; import hudson.model.UpdateSite; import hudson.util.VersionNumber; +import jenkins.YesNoMaybe; +import jenkins.model.Jenkins; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.LogFactory; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -47,8 +48,6 @@ 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; @@ -56,15 +55,19 @@ import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; 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.Objects; import java.util.Set; import java.util.function.Predicate; import java.util.jar.JarFile; @@ -73,6 +76,11 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; +import static hudson.PluginWrapper.PluginDisableStatus.ALREADY_DISABLED; +import static hudson.PluginWrapper.PluginDisableStatus.DISABLED; +import static hudson.PluginWrapper.PluginDisableStatus.ERROR_DISABLING; +import static hudson.PluginWrapper.PluginDisableStatus.NOT_DISABLED_DEPENDANTS; +import static hudson.PluginWrapper.PluginDisableStatus.NO_SUCH_PLUGIN; import static java.util.logging.Level.WARNING; import static org.apache.commons.io.FilenameUtils.getBaseName; @@ -200,6 +208,11 @@ public class PluginWrapper implements Comparable, ModelObject { */ private Set dependants = Collections.emptySet(); + /** + * List of plugins that depend optionally on this plugin. + */ + private Set optionalDependants = 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. @@ -214,6 +227,14 @@ public class PluginWrapper implements Comparable, ModelObject { this.dependants = dependants; } + /** + * Set the list of components that depend optionally on this plugin. + * @param optionalDependants The list of components that depend optionally on this plugin. + */ + public void setOptionalDependants(@Nonnull Set optionalDependants) { + this.optionalDependants = optionalDependants; + } + /** * Get the list of components that depend on this plugin. * @return The list of components that depend on this plugin. @@ -226,6 +247,13 @@ public class PluginWrapper implements Comparable, ModelObject { } } + /** + * @return The list of components that depend optionally on this plugin. + */ + public @Nonnull Set getOptionalDependants() { + return optionalDependants; + } + /** * Does this plugin have anything that depends on it. * @return {@code true} if something (Jenkins core, or another plugin) depends on this @@ -234,7 +262,17 @@ public class PluginWrapper implements Comparable, ModelObject { public boolean hasDependants() { return (isBundled || !dependants.isEmpty()); } - + + /** + * Does this plugin have anything that depends optionally on it. + * @return {@code true} if something (Jenkins core, or another plugin) depends optionally on this + * plugin, otherwise {@code false}. + */ + public boolean hasOptionalDependants() { + return !optionalDependants.isEmpty(); + } + + /** * Does this plugin depend on any other plugins. * @return {@code true} if this plugin depends on other plugins, otherwise {@code false}. @@ -519,9 +557,19 @@ public class PluginWrapper implements Comparable, ModelObject { } /** - * Disables this plugin next time Jenkins runs. + * Disables this plugin next time Jenkins runs. As it doesn't check anything, it's recommended to use the method + * {@link #disable(PluginDisableStrategy)} */ + @Deprecated //see https://issues.jenkins-ci.org/browse/JENKINS-27177 public void disable() throws IOException { + disableWithoutCheck(); + } + + /** + * Disable a plugin wihout checking any dependency. Only add the disable file. + * @throws IOException + */ + private void disableWithoutCheck() throws IOException { // creates an empty file try (OutputStream os = Files.newOutputStream(disableFile.toPath())) { os.close(); @@ -530,6 +578,103 @@ public class PluginWrapper implements Comparable, ModelObject { } } + /** + * Disable this plugin using a strategy. + * @param strategy strategy to use + * @return an object representing the result of the disablement of this plugin and its dependants plugins. + */ + public @Nonnull PluginDisableResult disable(@Nonnull PluginDisableStrategy strategy) { + PluginDisableResult result = new PluginDisableResult(shortName); + + if (!this.isEnabled()) { + result.setMessage(Messages.PluginWrapper_Already_Disabled(shortName)); + result.setStatus(ALREADY_DISABLED); + return result; + } + + // Act as a flag indicating if this plugin, finally, can be disabled. If there is a not-disabled-dependant + // plugin, this one couldn't be disabled. + String aDependantNotDisabled = null; + + // List of dependants plugins to 'check'. 'Check' means disable for mandatory or all strategies, or review if + // this dependant-mandatory plugin is enabled in order to return an error for the NONE strategy. + Set dependantsToCheck = dependantsToCheck(strategy); + + // Review all the dependants and add to the plugin result what happened with its dependants + for (String dependant : dependantsToCheck) { + PluginWrapper dependantPlugin = parent.getPlugin(dependant); + + // The dependant plugin doesn't exist, add an error to the report + if (dependantPlugin == null) { + PluginDisableResult dependantStatus = new PluginDisableResult(dependant, NO_SUCH_PLUGIN, Messages.PluginWrapper_NoSuchPlugin(dependant)); + result.addDependantDisableStatus(dependantStatus); + + // If the strategy is none and there is some enabled dependant plugin, the plugin cannot be disabled. If + // this dependant plugin is not enabled, continue searching for one enabled. + } else if (strategy.equals(PluginDisableStrategy.NONE)) { + if (dependantPlugin.isEnabled()) { + aDependantNotDisabled = dependant; + break; // in this case, we don't need to continue with the rest of its dependants + } + + // If the strategy is not none and this dependant plugin is not enabled, add it as already disabled + } else if (!dependantPlugin.isEnabled()) { + PluginDisableResult dependantStatus = new PluginDisableResult(dependant, ALREADY_DISABLED, Messages.PluginWrapper_Already_Disabled(dependant)); + result.addDependantDisableStatus(dependantStatus); + + // If the strategy is not none and this dependant plugin is enabled, disable it + } else { + // As there is no cycles in the plugin dependencies, the recursion shouldn't be infinite. The + // strategy used is the same for its dependants plugins + PluginDisableResult dependantResult = dependantPlugin.disable(strategy); + PluginDisableStatus dependantStatus = dependantResult.status; + + // If something wrong happened, flag this dependant plugin to set the plugin later as not-disabled due + // to its dependants plugins. + if (ERROR_DISABLING.equals(dependantStatus) || NOT_DISABLED_DEPENDANTS.equals(dependantStatus)) { + aDependantNotDisabled = dependant; + break; // we found a dependant plugin enabled, stop looking for dependant plugins to disable. + } + result.addDependantDisableStatus(dependantResult); + } + } + + // If there is no enabled-dependant plugin, disable this plugin and add it to the result + if (aDependantNotDisabled == null) { + try { + this.disableWithoutCheck(); + result.setMessage(Messages.PluginWrapper_Plugin_Disabled(shortName)); + result.setStatus(DISABLED); + } catch (IOException io) { + result.setMessage(Messages.PluginWrapper_Error_Disabling(shortName, io.toString())); + result.setStatus(ERROR_DISABLING); + } + // if there is yet some not disabled dependant plugin (only possible with none strategy), this plugin cannot + // be disabled. + } else { + result.setMessage(Messages.PluginWrapper_Plugin_Has_Dependant(shortName, aDependantNotDisabled, strategy)); + result.setStatus(NOT_DISABLED_DEPENDANTS); + } + + return result; + } + + private Set dependantsToCheck(PluginDisableStrategy strategy) { + Set dependantsToCheck; + switch (strategy) { + case ALL: + // getDependants returns all the dependant plugins, mandatory or optional. + dependantsToCheck = this.getDependants(); + break; + default: + // It includes MANDATORY, NONE: + // with NONE, the process only fail if mandatory dependant plugins exists + // As of getDependants has all the dependants, we get the difference between them and only the optionals + dependantsToCheck = Sets.difference(this.getDependants(), this.getOptionalDependants()); + } + return dependantsToCheck; + } + /** * Returns true if this plugin is enabled for this session. */ @@ -829,6 +974,93 @@ public class PluginWrapper implements Comparable, ModelObject { } } + /** + * The result of the disablement of a plugin and its dependants plugins. + */ + public static class PluginDisableResult { + private String plugin; + private PluginDisableStatus status; + private String message; + private Set dependantsDisableStatus = new HashSet<>(); + + public PluginDisableResult(String plugin) { + this.plugin = plugin; + } + + public PluginDisableResult(String plugin, PluginDisableStatus status, String message) { + this.plugin = plugin; + this.status = status; + this.message = message; + } + + public String getPlugin() { + return plugin; + } + + public PluginDisableStatus getStatus() { + return status; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PluginDisableResult that = (PluginDisableResult) o; + return Objects.equals(plugin, that.plugin); + } + + @Override + public int hashCode() { + return Objects.hash(plugin); + } + + public void setStatus(PluginDisableStatus status) { + this.status = status; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Set getDependantsDisableStatus() { + return dependantsDisableStatus; + } + + public void addDependantDisableStatus(PluginDisableResult dependantDisableStatus) { + dependantsDisableStatus.add(dependantDisableStatus); + } + + } + + /** + * An enum to hold the status of a disabling action against a plugin. To do it more reader-friendly. + */ + public enum PluginDisableStatus { + NO_SUCH_PLUGIN, + DISABLED, + ALREADY_DISABLED, + NOT_DISABLED_DEPENDANTS, + ERROR_DISABLING + } + + /** + * The strategies defined for disabling a plugin. + */ + public enum PluginDisableStrategy { + NONE, + MANDATORY, + ALL; + + @Override + public String toString() { + return this.name().toLowerCase(); + } + } + // // // Action methods diff --git a/core/src/main/java/hudson/cli/DisablePluginCommand.java b/core/src/main/java/hudson/cli/DisablePluginCommand.java new file mode 100644 index 0000000000..36d4b6bd8c --- /dev/null +++ b/core/src/main/java/hudson/cli/DisablePluginCommand.java @@ -0,0 +1,236 @@ +/* + * The MIT License + * + * Copyright (c) 2018 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.cli; + +import hudson.Extension; +import hudson.PluginWrapper; +import hudson.lifecycle.RestartNotSupportedException; +import jenkins.model.Jenkins; +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; + +import java.io.PrintStream; +import java.util.List; + +/** + * Disable one or more installed plugins. + * @since TODO + */ +@Extension +public class DisablePluginCommand extends CLICommand { + + @Argument(metaVar = "plugin1 plugin2 plugin3", required = true, usage = "Plugins to be disabled.") + private List pluginNames; + + @Option(name = "-restart", aliases = "-r", usage = "Restart Jenkins after disabling plugins.") + private boolean restart; + + @Option(name = "-strategy", aliases = "-s", metaVar = "strategy", usage = "How to process the dependant plugins. \n" + + "- none: if a mandatory dependant plugin exists and it is enabled, the plugin cannot be disabled (default value).\n" + + "- mandatory: all mandatory dependant plugins are also disabled, optional dependant plugins remain enabled.\n" + + "- all: all dependant plugins are also disabled, no matter if its dependency is optional or mandatory.") + private String strategy = PluginWrapper.PluginDisableStrategy.NONE.toString(); + + @Option(name = "-quiet", aliases = "-q", usage = "Be quiet, print only the error messages") + private boolean quiet; + + private static final int INDENT_SPACE = 3; + + @Override + public String getShortDescription() { + return Messages.DisablePluginCommand_ShortDescription(); + } + + // package-private access to be able to use it in the tests + static final int RETURN_CODE_NOT_DISABLED_DEPENDANTS = 16; + static final int RETURN_CODE_NO_SUCH_PLUGIN = 17; + + @Override + protected void printUsageSummary(PrintStream stderr) { + super.printUsageSummary(stderr); + stderr.println(Messages.DisablePluginCommand_PrintUsageSummary()); + } + + @Override + protected int run() throws Exception { + Jenkins jenkins = Jenkins.get(); + jenkins.checkPermission(Jenkins.ADMINISTER); + + // get the strategy as an enum + PluginWrapper.PluginDisableStrategy strategyToUse; + try { + strategyToUse = PluginWrapper.PluginDisableStrategy.valueOf(strategy.toUpperCase()); + } catch (IllegalArgumentException iae) { + throw new IllegalArgumentException(hudson.cli.Messages.DisablePluginCommand_NoSuchStrategy(strategy, String.format("%s, %s, %s", PluginWrapper.PluginDisableStrategy.NONE, PluginWrapper.PluginDisableStrategy.MANDATORY, PluginWrapper.PluginDisableStrategy.ALL))); + } + + // disable... + List results = jenkins.pluginManager.disablePlugins(strategyToUse, pluginNames); + + // print results ... + printResults(results); + + // restart if it was required and it's necessary (at least one plugin was disabled in this execution) + restartIfNecessary(results); + + // return the appropriate error code + return getResultCode(results); + } + + /** + * Print the result of all the process + * @param results the list of results for the disablement of each plugin + */ + private void printResults(List results) { + for (PluginWrapper.PluginDisableResult oneResult : results) { + printResult(oneResult, 0); + } + } + + /** + * Print indented the arguments with the format passed beginning with the indent passed. + * @param indent number of spaces at the beginning. + * @param format format as in {@link String#format(String, Object...)} + * @param arguments arguments to print as in {@link String#format(String, Object...)} + */ + private void printIndented(int indent, String format, String... arguments) { + if (indent == 0) { + stdout.format(format + "%n", (Object[]) arguments); + } else { + String[] newArgs = new String[arguments.length + 1]; + newArgs[0] = " "; + System.arraycopy(arguments, 0, newArgs, 1, arguments.length); + + String f = "%" + indent + "s" + format + "%n"; + stdout.format(f, newArgs); + } + } + + /** + * Print the result of a plugin disablement with the indent passed. + * @param oneResult the result of the disablement of a plugin. + * @param indent the initial indent. + */ + private void printResult(PluginWrapper.PluginDisableResult oneResult, int indent) { + PluginWrapper.PluginDisableStatus status = oneResult.getStatus(); + if (quiet && (PluginWrapper.PluginDisableStatus.DISABLED.equals(status) || PluginWrapper.PluginDisableStatus.ALREADY_DISABLED.equals(status))) { + return; + } + + printIndented(indent, Messages.DisablePluginCommand_StatusMessage(oneResult.getPlugin(), oneResult.getStatus(), oneResult.getMessage())); + if (oneResult.getDependantsDisableStatus().size() > 0) { + indent += INDENT_SPACE; + for (PluginWrapper.PluginDisableResult oneDependantResult : oneResult.getDependantsDisableStatus()) { + printResult(oneDependantResult, indent); + } + } + } + + /** + * Restart if at least one plugin was disabled in this process. + * @param results the list of results after the disablement of the plugins. + */ + private void restartIfNecessary(List results) throws RestartNotSupportedException { + if (restart) { + for (PluginWrapper.PluginDisableResult oneResult : results) { + if (restartIfNecessary(oneResult)) { + break; + } + } + } + } + + /** + * Restart if this particular result of the disablement of a plugin and its dependant plugins (depending on the + * strategy used) has a plugin disablexd. + * @param oneResult the result of a plugin (and its dependants). + * @return true if it end up in restarting jenkins. + */ + private boolean restartIfNecessary(PluginWrapper.PluginDisableResult oneResult) throws RestartNotSupportedException { + PluginWrapper.PluginDisableStatus status = oneResult.getStatus(); + if (PluginWrapper.PluginDisableStatus.DISABLED.equals(status)) { + Jenkins.get().safeRestart(); + return true; + } + + if (oneResult.getDependantsDisableStatus().size() > 0) { + for (PluginWrapper.PluginDisableResult oneDependantResult : oneResult.getDependantsDisableStatus()) { + if (restartIfNecessary(oneDependantResult)) { + return true; + } + } + } + + return false; + } + + + /** + * Calculate the result code of the full process based in what went on during the process + * @param results he list of results for the disablement of each plugin + * @return the status code. 0 if all plugins disabled. {@link #RETURN_CODE_NOT_DISABLED_DEPENDANTS} if some + * dependant plugin is not disabled (with strategy NONE), {@link #RETURN_CODE_NO_SUCH_PLUGIN} if some passed + * plugin doesn't exist. Whatever happens first. + */ + private int getResultCode(List results) { + int result = 0; + for (PluginWrapper.PluginDisableResult oneResult : results) { + result = getResultCode(oneResult); + if (result != 0) { + break; + } + } + + return result; + } + + /** + * Calculate the result code of the disablement of one plugin based in what went on during the process of this one + * and its dependant plugins. + * @param result the result of the disablement of this plugin + * @return the status code + */ + private int getResultCode(PluginWrapper.PluginDisableResult result) { + int returnCode = 0; + switch (result.getStatus()){ + case NOT_DISABLED_DEPENDANTS: + returnCode = RETURN_CODE_NOT_DISABLED_DEPENDANTS; + break; + case NO_SUCH_PLUGIN: + returnCode = RETURN_CODE_NO_SUCH_PLUGIN; + } + + if (returnCode == 0) { + for (PluginWrapper.PluginDisableResult oneDependantResult : result.getDependantsDisableStatus()) { + returnCode = getResultCode(oneDependantResult); + if (returnCode != 0) { + break; + } + } + } + + return returnCode; + } +} diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 032049019b..c5ccf765f3 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -4284,8 +4284,6 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve @RequirePOST public void doExit( StaplerRequest req, StaplerResponse rsp ) throws IOException { checkPermission(ADMINISTER); - LOGGER.info(String.format("Shutting down VM as requested by %s from %s", - getAuthentication().getName(), req!=null?req.getRemoteAddr():"???")); if (rsp!=null) { rsp.setStatus(HttpServletResponse.SC_OK); rsp.setContentType("text/plain"); @@ -4294,10 +4292,22 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve } } - cleanUp(); - System.exit(0); - } + new Thread("exit thread") { + @Override + public void run() { + try { + ACL.impersonate(ACL.SYSTEM); + LOGGER.info(String.format("Shutting down VM as requested by %s from %s", + getAuthentication().getName(), req != null ? req.getRemoteAddr() : "???")); + cleanUp(); + System.exit(0); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Failed to shut down Jenkins", e); + } + } + }.start(); + } /** * Shutdown the system safely. diff --git a/core/src/main/resources/hudson/Messages.properties b/core/src/main/resources/hudson/Messages.properties index 80ecb8db83..57fa82605f 100644 --- a/core/src/main/resources/hudson/Messages.properties +++ b/core/src/main/resources/hudson/Messages.properties @@ -83,6 +83,10 @@ 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. PluginWrapper.PluginWrapperAdministrativeMonitor.DisplayName=Plugins Failed To Load - +PluginWrapper.Already.Disabled=The plugin ''{0}'' was already disabled +PluginWrapper.Plugin.Has.Dependant=The plugin ''{0}'' has, at least, one dependant plugin ({1}) and the disable strategy is {2}, so it cannot be disabled +PluginWrapper.Plugin.Disabled=Plugin ''{0}'' disabled +PluginWrapper.NoSuchPlugin=No such plugin found with the name ''{0}'' +PluginWrapper.Error.Disabling=There was an error disabling the ''{0}'' plugin. Error: ''{1}'' TcpSlaveAgentListener.PingAgentProtocol.displayName=Ping protocol diff --git a/core/src/main/resources/hudson/Messages_es.properties b/core/src/main/resources/hudson/Messages_es.properties index 9ad1d7eeb3..c787fed9c3 100644 --- a/core/src/main/resources/hudson/Messages_es.properties +++ b/core/src/main/resources/hudson/Messages_es.properties @@ -55,3 +55,7 @@ PluginManager.PluginDoesntSupportDynamicLoad.RestartRequired=El plugin {0} no so PluginManager.PortNotANumber=El puerto no es un número AboutJenkins.Description=Eche un vistazo a la información sobre la versión y la licencia. PluginManager.PortNotInRange=El puerto no está en el rango de {0} hasta {1} +PluginWrapper.Already.Disabled=El plugin {0} ya estaba deshabilitado +PluginWrapper.Plugin.Has.Dependant=El plugin {0} tiene, al menos, un plugin dependiente ({1}) y la estrategia de deshabilitación es {2}, así que no puede ser deshabilitado +PluginWrapper.Plugin.Disabled=Plugin {0} deshabilitado +PluginWrapper.NoSuchPlugin=No se encuentra un plugin con el nombre {0} diff --git a/core/src/main/resources/hudson/cli/Messages.properties b/core/src/main/resources/hudson/cli/Messages.properties index df4e3f5125..85bbf56b6d 100644 --- a/core/src/main/resources/hudson/cli/Messages.properties +++ b/core/src/main/resources/hudson/cli/Messages.properties @@ -113,3 +113,10 @@ WaitNodeOfflineCommand.ShortDescription=Wait for a node to become offline. CliProtocol.displayName=Jenkins CLI Protocol/1 (unencrypted) CliProtocol2.displayName=Jenkins CLI Protocol/2 (transport encryption) + +DisablePluginCommand.ShortDescription=\ +Disable one or more installed plugins. +DisablePluginCommand.NoSuchStrategy=This strategy ({0}) does not exist. Allowed strategies are {1} +DisablePluginCommand.PrintUsageSummary=\ +Disable the plugins with the given short names. You can define how to proceed with the dependant plugins and if a restart after should be done. You can also set the quiet mode to avoid extra info in the console. +DisablePluginCommand.StatusMessage=Disabling ''{0}'': {1} ({2}) diff --git a/core/src/main/resources/hudson/cli/Messages_es.properties b/core/src/main/resources/hudson/cli/Messages_es.properties index 11525579ca..d8f7b4d7cc 100644 --- a/core/src/main/resources/hudson/cli/Messages_es.properties +++ b/core/src/main/resources/hudson/cli/Messages_es.properties @@ -59,4 +59,9 @@ CancelQuietDownCommand.ShortDescription=Cancelar el efecto del comando "quiet-do OfflineNodeCommand.ShortDescription=Dejar de utilizar un nodo temporalmente hasta que se ejecute el comando "online-node". WaitNodeOnlineCommand.ShortDescription=Esperando hasta que el nodo esté activado WaitNodeOfflineCommand.ShortDescription=Esperando a que el nodo esté desactivado - +DisablePluginCommand.ShortDescription=\ +Deshabilita uno o más plugins instalados. +DisablePluginCommand.NoSuchStrategy=Esta estrategia ({0}) no existe. Las estrategias permitidas son {1} +DisablePluginCommand.PrintUsageSummary=\ +Deshabilita los plugins con los nombre cortos dados. Puede definir cómo proceder con los plugins dependientes y si se realiza un reinicio posterior. +DisablePluginCommand.StatusMessage=Deshabilitando {0}: {1} ({2}) diff --git a/core/src/main/resources/jenkins/install/platform-plugins.json b/core/src/main/resources/jenkins/install/platform-plugins.json index 5cd0067fb5..6228138059 100644 --- a/core/src/main/resources/jenkins/install/platform-plugins.json +++ b/core/src/main/resources/jenkins/install/platform-plugins.json @@ -101,5 +101,12 @@ { "name": "publish-over-ssh" }, { "name": "ssh" } ] + }, + { + "category":"Languages", + "plugins": [ + { "name": "locale"}, + { "name": "localization-zh-cn"} + ] } ] diff --git a/pom.xml b/pom.xml index 0ab3c86021..47c16272a8 100644 --- a/pom.xml +++ b/pom.xml @@ -75,7 +75,7 @@ THE SOFTWARE. - 2.151 + 2.152 -SNAPSHOT - ${jacocoSurefireArgs} -Dfile.encoding=UTF-8 -Xmx1g + ${jacocoSurefireArgs} -Dfile.encoding=UTF-8 -Xmx1g -Djdk.net.URLClassPath.disableClassPathURLCheck=true true diff --git a/test/src/test/java/hudson/cli/DisablePluginCommandTest.java b/test/src/test/java/hudson/cli/DisablePluginCommandTest.java new file mode 100644 index 0000000000..8b96dbcfa1 --- /dev/null +++ b/test/src/test/java/hudson/cli/DisablePluginCommandTest.java @@ -0,0 +1,388 @@ + +/* + * The MIT License + * + * Copyright (c) 2018 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.cli; + +import hudson.Functions; +import hudson.PluginWrapper; +import org.apache.commons.lang.StringUtils; +import org.junit.Assume; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.recipes.WithPlugin; + +import java.io.IOException; +import java.util.function.BiPredicate; + +import static hudson.cli.CLICommandInvoker.Matcher.failedWith; +import static hudson.cli.CLICommandInvoker.Matcher.succeeded; +import static hudson.cli.DisablePluginCommand.RETURN_CODE_NOT_DISABLED_DEPENDANTS; +import static hudson.cli.DisablePluginCommand.RETURN_CODE_NO_SUCH_PLUGIN; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.*; + +public class DisablePluginCommandTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + /** + * Can disable a plugin with an optional dependent plugin. + * With strategy none. + */ + @Test + @Issue("JENKINS-27177") + @WithPlugin({"depender-0.0.2.hpi", "dependee-0.0.2.hpi"}) + public void canDisablePluginWithOptionalDependerStrategyNone() { + assertThat(disablePluginsCLiCommand("-strategy", "NONE", "dependee"), succeeded()); + assertPluginDisabled("dependee"); + } + + @Test + @Issue("JENKINS-27177") + @WithPlugin({"depender-0.0.2.hpi", "dependee-0.0.2.hpi", "mandatory-depender-0.0.2.hpi"}) + public void canDisablePluginWithDependantsDisabledStrategyNone() throws IOException { + disablePlugin("mandatory-depender"); + CLICommandInvoker.Result result = disablePluginsCLiCommand("-strategy", "NONE", "dependee"); + + assertThat(result, succeeded()); + assertEquals("Disabling only dependee", 1, StringUtils.countMatches(result.stdout(), "Disabling")); + assertPluginDisabled("dependee"); + } + + /** + * Can't disable a plugin with a mandatory dependent plugin. + * With default strategy (none). + */ + @Test + @Issue("JENKINS-27177") + @WithPlugin({"mandatory-depender-0.0.2.hpi", "dependee-0.0.2.hpi"}) + public void cannotDisablePluginWithMandatoryDependerStrategyNone() { + assertThat(disablePluginsCLiCommand("dependee"), failedWith(RETURN_CODE_NOT_DISABLED_DEPENDANTS)); + assertPluginEnabled("dependee"); + } + + /** + * Can't disable a plugin with a mandatory dependent plugin before its dependent plugin. + * With default strategy (none). + */ + @Test + @Issue("JENKINS-27177") + @WithPlugin({"mandatory-depender-0.0.2.hpi", "dependee-0.0.2.hpi"}) + public void cannotDisableDependentPluginWrongOrderStrategyNone() { + assertThat(disablePluginsCLiCommand("dependee", "mandatory-depender"), failedWith(RETURN_CODE_NOT_DISABLED_DEPENDANTS)); + assertPluginDisabled("mandatory-depender"); + assertPluginEnabled("dependee"); + } + + /** + * Can disable a plugin with a mandatory dependent plugin before its dependent plugin with strategy + */ + @Test + @Issue("JENKINS-27177") + @WithPlugin({"mandatory-depender-0.0.2.hpi", "dependee-0.0.2.hpi"}) + public void canDisableDependentPluginWrongOrderStrategyAll() { + assertThat(disablePluginsCLiCommand("dependee", "mandatory-depender", "-strategy", "all"), succeeded()); + assertPluginDisabled("mandatory-depender"); + assertPluginDisabled("dependee"); + } + + /** + * Can disable a plugin with a mandatory dependent plugin after being disabled the mandatory dependent plugin. With + * default strategy (none). + */ + @Test + @Issue("JENKINS-27177") + @WithPlugin({"mandatory-depender-0.0.2.hpi", "dependee-0.0.2.hpi"}) + public void canDisableDependentPluginsRightOrderStrategyNone() { + assertThat(disablePluginsCLiCommand("mandatory-depender", "dependee"), succeeded()); + assertPluginDisabled("dependee"); + assertPluginDisabled("mandatory-depender"); + } + + /** + * Can disable a plugin without dependents plugins and Jenkins restart after it if -restart argument is passed. + */ + @Test + @Issue("JENKINS-27177") + @WithPlugin("dependee-0.0.2.hpi") + public void restartAfterDisable() { + assumeNotWindows(); + assertThat(disablePluginsCLiCommand("-restart", "dependee"), succeeded()); + assertPluginDisabled("dependee"); + assertJenkinsInQuietMode(); + } + + /** + * Can disable a plugin without dependents plugins and Jenkins doesn't restart after it if -restart is not passed. + */ + @Test + @Issue("JENKINS-27177") + @WithPlugin("dependee-0.0.2.hpi") + public void notRestartAfterDisablePluginWithoutArgumentRestart() { + assertThat(disablePluginsCLiCommand("dependee"), succeeded()); + assertPluginDisabled("dependee"); + assertJenkinsNotInQuietMode(); + } + + /** + * A non-existing plugin returns with a {@link DisablePluginCommand#RETURN_CODE_NO_SUCH_PLUGIN} status code. + */ + @Test + @Issue("JENKINS-27177") + @WithPlugin("dependee-0.0.2.hpi") + public void returnCodeDisableInvalidPlugin() { + assertThat(disablePluginsCLiCommand("wrongname"), failedWith(RETURN_CODE_NO_SUCH_PLUGIN)); + } + + /** + * A plugin already disabled returns 0 and jenkins doesn't restart even though you passed the -restart argument. + * @throws IOException See {@link PluginWrapper#disable()}. + */ + @Test + @Issue("JENKINS-27177") + @WithPlugin("dependee-0.0.2.hpi") + public void disableAlreadyDisabledPluginNotRestart() throws IOException { + // Disable before the command call + disablePlugin("dependee"); + + assertPluginDisabled("dependee"); + assertThat(disablePluginsCLiCommand("-restart", "dependee"), succeeded()); + assertPluginDisabled("dependee"); + assertJenkinsNotInQuietMode(); + } + + /** + * If some plugins are disabled, Jenkins will restart even though the status code isn't 0 (is 16). + */ + @Test + @Issue("JENKINS-27177") + @WithPlugin({"variant.hpi", "depender-0.0.2.hpi", "mandatory-depender-0.0.2.hpi", "plugin-first.hpi", "dependee-0.0.2.hpi", }) + public void restartAfterDisablePluginsAndErrors() { + assumeNotWindows(); + assertThat(disablePluginsCLiCommand("-restart", "variant", "dependee", "depender", "plugin-first", "mandatory-depender"), failedWith(RETURN_CODE_NOT_DISABLED_DEPENDANTS)); + assertPluginDisabled("variant"); + assertPluginEnabled("dependee"); + assertPluginDisabled("depender"); + assertPluginDisabled("plugin-first"); + assertPluginDisabled("mandatory-depender"); + assertJenkinsInQuietMode(); // some plugins were disabled, so it should be restarting + } + + /** + * All the dependant plugins, mandatory or optional, are disabled using -strategy all. + */ + @Test + @Issue("JENKINS-27177") + @WithPlugin({"variant.hpi", "depender-0.0.2.hpi", "mandatory-depender-0.0.2.hpi", "plugin-first.hpi", "dependee-0.0.2.hpi", }) + public void disablePluginsStrategyAll() { + assertPluginEnabled("dependee"); + assertPluginEnabled("depender"); + assertPluginEnabled("mandatory-depender"); + assertThat(disablePluginsCLiCommand("-strategy", "all", "variant", "dependee", "plugin-first"), succeeded()); + assertPluginDisabled("variant"); + assertPluginDisabled("dependee"); + assertPluginDisabled("depender"); + assertPluginDisabled("plugin-first"); + assertPluginDisabled("mandatory-depender"); + } + + /** + * Only the mandatory dependant plugins are disabled using -strategy mandatory. + */ + @Test + @Issue("JENKINS-27177") + @WithPlugin({"variant.hpi", "depender-0.0.2.hpi", "mandatory-depender-0.0.2.hpi", "plugin-first.hpi", "dependee-0.0.2.hpi", }) + public void disablePluginsStrategyMandatory() { + assertThat(disablePluginsCLiCommand("-strategy", "mandatory", "variant", "dependee", "plugin-first"), succeeded()); + assertPluginDisabled("variant"); + assertPluginDisabled("dependee"); + assertPluginEnabled("depender"); + assertPluginDisabled("plugin-first"); + assertPluginDisabled("mandatory-depender"); + } + + /** + * A plugin already disabled because it's a dependent plugin of one previously disabled appear two times in the log + * with different messages. + */ + @Test + @Issue("JENKINS-27177") + @WithPlugin({"depender-0.0.2.hpi", "dependee-0.0.2.hpi", }) + public void disablePluginsMessageAlreadyDisabled() { + CLICommandInvoker.Result result = disablePluginsCLiCommand("-strategy", "all", "dependee", "depender"); + assertThat(result, succeeded()); + + assertPluginDisabled("dependee"); + assertPluginDisabled("depender"); + + assertTrue("An occurrence of the depender plugin in the log says it was successfully disabled", checkResultWith(result, StringUtils::contains, "depender", PluginWrapper.PluginDisableStatus.DISABLED)); + assertTrue("An occurrence of the depender plugin in the log says it was already disabled", checkResultWith(result, StringUtils::contains, "depender", PluginWrapper.PluginDisableStatus.ALREADY_DISABLED)); + } + + /** + * The return code is the first error distinct of 0 found during the process. In this case dependant plugins not + * disabled. + */ + @Test + @Issue("JENKINS-27177") + @WithPlugin({"dependee-0.0.2.hpi", "mandatory-depender-0.0.2.hpi"}) + public void returnCodeFirstErrorIsDependants() { + CLICommandInvoker.Result result = disablePluginsCLiCommand("dependee", "badplugin"); + assertThat(result, failedWith(RETURN_CODE_NOT_DISABLED_DEPENDANTS)); + + assertPluginEnabled("dependee"); + } + + /** + * The return code is the first error distinct of 0 found during the process. In this case no such plugin. + */ + @Test + @Issue("JENKINS-27177") + @WithPlugin({"dependee-0.0.2.hpi", "mandatory-depender-0.0.2.hpi"}) + public void returnCodeFirstErrorIsNoSuchPlugin() { + CLICommandInvoker.Result result = disablePluginsCLiCommand("badplugin", "dependee"); + assertThat(result, failedWith(RETURN_CODE_NO_SUCH_PLUGIN)); + + assertPluginEnabled("dependee"); + } + + /** + * In quiet mode, no message is printed if all plugins are disabled or were already disabled. + */ + @Test + @Issue("JENKINS-27177") + @WithPlugin({"depender-0.0.2.hpi", "dependee-0.0.2.hpi", "mandatory-depender-0.0.2.hpi"}) + public void quietModeEmptyOutputSucceed() { + CLICommandInvoker.Result result = disablePluginsCLiCommand("-strategy", "all", "-quiet", "dependee"); + assertThat(result, succeeded()); + + assertPluginDisabled("dependee"); + assertPluginDisabled("depender"); + assertPluginDisabled("mandatory-depender"); + + assertTrue("No log in quiet mode if all plugins disabled", StringUtils.isEmpty(result.stdout())); + } + + /** + * In quiet mode, only the errors (no such plugin) are printed. + */ + @Test + @Issue("JENKINS-27177") + @WithPlugin({"depender-0.0.2.hpi", "dependee-0.0.2.hpi", "mandatory-depender-0.0.2.hpi"}) + public void quietModeWithErrorNoSuch() { + CLICommandInvoker.Result result = disablePluginsCLiCommand("-quiet", "-strategy", "all", "dependee", "badplugin"); + assertThat(result, failedWith(RETURN_CODE_NO_SUCH_PLUGIN)); + + assertPluginDisabled("dependee"); + assertPluginDisabled("depender"); + assertPluginDisabled("mandatory-depender"); + + assertTrue("Only error NO_SUCH_PLUGIN in quiet mode", checkResultWith(result, StringUtils::startsWith, "badplugin", PluginWrapper.PluginDisableStatus.NO_SUCH_PLUGIN)); + } + + /** + * In quiet mode, only the errors (dependants plugins) are printed. + */ + @Test + @Issue("JENKINS-27177") + @WithPlugin({"depender-0.0.2.hpi", "dependee-0.0.2.hpi", "mandatory-depender-0.0.2.hpi"}) + public void quietModeWithErrorDependants() { + CLICommandInvoker.Result result = disablePluginsCLiCommand("-quiet", "-strategy", "none", "dependee"); + assertThat(result, failedWith(RETURN_CODE_NOT_DISABLED_DEPENDANTS)); + + assertPluginEnabled("dependee"); + assertPluginEnabled("depender"); + assertPluginEnabled("mandatory-depender"); + + assertTrue("Only error NOT_DISABLED_DEPENDANTS in quiet mode", checkResultWith(result, StringUtils::startsWith, "dependee", PluginWrapper.PluginDisableStatus.NOT_DISABLED_DEPENDANTS)); + } + + /** + * Helper method to check the output of a result with a specific method allowing two arguments ( + * StringUtils::startsWith or StringUtils::contents). This method avoid to have it hardcoded the messages. We avoid + * having to compose the descriptive text of the message by using a stop flag to ignore the last characters. + * This method supposes that the descriptive text is at the last of the string. + * @param result the result of the command. + * @param method a method with two string arguments to check against + * @param plugin the plugin printed outSetting the plugin and status as parameters, the method gets + * the string printed using the + * @param status the status printed out + * @return true if the output has been checked against the method using the plugin and status args + */ + private boolean checkResultWith(CLICommandInvoker.Result result, BiPredicate method, String plugin, PluginWrapper.PluginDisableStatus status) { + String noMatterFollowingChars = "/!$stop"; + String outExpected = Messages.DisablePluginCommand_StatusMessage(plugin, status, noMatterFollowingChars); + outExpected = StringUtils.substringBefore(outExpected, noMatterFollowingChars); + return method.test(result.stdout(), outExpected); + } + + /** + * Disable a plugin using the {@link PluginWrapper#disable()} method. + * @param name the name of the plugin. + * @throws IOException if the disablement cannot be made. + */ + private void disablePlugin(String name) throws IOException { + PluginWrapper plugin = j.getPluginManager().getPlugin(name); + assertThat(plugin, is(notNullValue())); + plugin.disable(); + } + + /** + * Disable a list of plugins using the CLI command. + * @param args Arguments to pass to the command. + * @return Result of the command. 0 if succeed, 16 if some plugin couldn't be disabled due to dependant plugins. + */ + private CLICommandInvoker.Result disablePluginsCLiCommand(String... args) { + return new CLICommandInvoker(j, new DisablePluginCommand()).invokeWithArgs(args); + } + + private void assertPluginDisabled(String name) { + PluginWrapper plugin = j.getPluginManager().getPlugin(name); + assertThat(plugin, is(notNullValue())); + assertFalse(plugin.isEnabled()); + } + + private void assertPluginEnabled(String name) { + PluginWrapper plugin = j.getPluginManager().getPlugin(name); + assertThat(plugin, is(notNullValue())); + assertTrue(plugin.isEnabled()); + } + + private void assertJenkinsInQuietMode() { + QuietDownCommandTest.assertJenkinsInQuietMode(j); + } + + private void assertJenkinsNotInQuietMode() { + QuietDownCommandTest.assertJenkinsNotInQuietMode(j); + } + + private void assumeNotWindows() { + Assume.assumeFalse(Functions.isWindows()); + } +} diff --git a/test/src/test/java/hudson/console/ConsoleAnnotatorTest.java b/test/src/test/java/hudson/console/ConsoleAnnotatorTest.java index 18e46dda4b..346d205a02 100644 --- a/test/src/test/java/hudson/console/ConsoleAnnotatorTest.java +++ b/test/src/test/java/hudson/console/ConsoleAnnotatorTest.java @@ -3,6 +3,7 @@ package hudson.console; import com.gargoylesoftware.htmlunit.Page; import com.gargoylesoftware.htmlunit.TextPage; import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.html.DomElement; import com.gargoylesoftware.htmlunit.html.DomNodeUtil; import com.gargoylesoftware.htmlunit.html.HtmlPage; import hudson.FilePath; @@ -27,6 +28,7 @@ import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Future; +import jenkins.model.Jenkins; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.*; import org.junit.Rule; @@ -288,6 +290,12 @@ public class ConsoleAnnotatorTest { // verify that there's an element inserted by the script assertNotNull(html.getElementById("inserted-by-test1")); assertNotNull(html.getElementById("inserted-by-test2")); + for (DomElement e : html.getElementsByTagName("script")) { + String src = e.getAttribute("src"); + if (!src.isEmpty()) { + assertThat(src, containsString(Jenkins.SESSION_HASH)); + } + } } public static final class JustToIncludeScript extends ConsoleNote { diff --git a/war/pom.xml b/war/pom.xml index 1ab587db93..7cd7dad40e 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -110,7 +110,7 @@ THE SOFTWARE. org.jenkins-ci.modules windows-slave-installer - 1.9.2 + 1.9.3 org.jenkins-ci.modules diff --git a/war/src/main/js/api/pluginManager.js b/war/src/main/js/api/pluginManager.js index 923c2c1ddb..8032828e5e 100644 --- a/war/src/main/js/api/pluginManager.js +++ b/war/src/main/js/api/pluginManager.js @@ -40,6 +40,12 @@ exports.init = function(handler) { plugins.names.push(pluginName); if (plugin.suggested) { plugins.recommendedPlugins.push(pluginName); + } else if (pluginCategory.category === "Languages") { + var language = window.navigator.userLanguage || window.navigator.language; + var code = language.toLocaleLowerCase(); + if (pluginName === ("localization-" + code)) { + plugins.recommendedPlugins.push(pluginName); + } } } }