mirror of
https://github.com/jenkinsci/jenkins.git
synced 2026-11-03 07:05:55 +00:00
* Add new module optional-depender and a new failing test
* Variant plugin is a dependency of optional-depender
Optional-depender contains the following class
@OptionalExtension(requirePlugins = {"dependee"})
public class OptionalDependerExtension {
private static void foo(Dependee d) {
}
}
* [JENKINS-60449] Move plugin initialization at the end, keep only one extension refresh
* Improve logic to detect classloading issues
Inspect all declared constructors/methods/fields recursively to exclude
failing classes from Guice rather than have Guice explode.
This also fix the failing test, but the previous commit is still better
because it avoids an extra Jenkins.get().refreshExtensions() call.
* Only test resolveClass method if the classloader is not the bootstrap one
2249 lines
97 KiB
Java
2249 lines
97 KiB
Java
/*
|
||
* The MIT License
|
||
*
|
||
* Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Stephen Connolly, Tom Huybrechts
|
||
*
|
||
* 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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||
import hudson.PluginWrapper.Dependency;
|
||
import hudson.init.InitMilestone;
|
||
import hudson.init.InitStrategy;
|
||
import hudson.init.InitializerFinder;
|
||
import hudson.model.AbstractItem;
|
||
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.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.Retrier;
|
||
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;
|
||
import jenkins.YesNoMaybe;
|
||
import jenkins.install.InstallState;
|
||
import jenkins.install.InstallUtil;
|
||
import jenkins.model.Jenkins;
|
||
import jenkins.plugins.DetachedPluginsUtil;
|
||
import jenkins.security.CustomClassFilter;
|
||
import jenkins.telemetry.impl.java11.MissingClassTelemetry;
|
||
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;
|
||
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
|
||
import org.apache.commons.fileupload.servlet.ServletFileUpload;
|
||
import org.apache.commons.io.FileUtils;
|
||
import org.apache.commons.io.FilenameUtils;
|
||
import org.apache.commons.io.IOUtils;
|
||
import org.apache.commons.lang.StringUtils;
|
||
import org.apache.commons.logging.LogFactory;
|
||
import org.jenkinsci.Symbol;
|
||
import org.jenkinsci.bytecode.Transformer;
|
||
import org.jvnet.hudson.reactor.Executable;
|
||
import org.jvnet.hudson.reactor.Reactor;
|
||
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;
|
||
import org.kohsuke.stapler.QueryParameter;
|
||
import org.kohsuke.stapler.StaplerOverridable;
|
||
import org.kohsuke.stapler.StaplerProxy;
|
||
import org.kohsuke.stapler.StaplerRequest;
|
||
import org.kohsuke.stapler.StaplerResponse;
|
||
import org.kohsuke.stapler.export.Exported;
|
||
import org.kohsuke.stapler.export.ExportedBean;
|
||
import org.kohsuke.stapler.interceptor.RequirePOST;
|
||
import org.kohsuke.stapler.verb.POST;
|
||
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;
|
||
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;
|
||
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;
|
||
import java.util.Enumeration;
|
||
import java.util.HashMap;
|
||
import java.util.HashSet;
|
||
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;
|
||
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 java.util.stream.Collectors;
|
||
|
||
import static hudson.init.InitMilestone.*;
|
||
import static java.util.logging.Level.*;
|
||
|
||
/**
|
||
* Manages {@link PluginWrapper}s.
|
||
*
|
||
* <p>
|
||
* <b>Setting default Plugin Managers</b>. The default plugin manager in {@code Jenkins} can be replaced by defining a
|
||
* System Property ({@code hudson.PluginManager.className}). See {@link #createDefault(Jenkins)}.
|
||
* This className should be available on early startup, so it cannot come only from a library
|
||
* (e.g. Jenkins module or Extra library dependency in the WAR file project).
|
||
* Plugins cannot be used for such purpose.
|
||
* In order to be correctly instantiated, the class definition must have at least one constructor with the same
|
||
* signature as the following ones:
|
||
* <ol>
|
||
* <li>{@link LocalPluginManager#LocalPluginManager(Jenkins)} </li>
|
||
* <li>{@link LocalPluginManager#LocalPluginManager(ServletContext, File)} </li>
|
||
* <li>{@link LocalPluginManager#LocalPluginManager(File)} </li>
|
||
* </ol>
|
||
* Constructors are searched in the order provided above and only the first found suitable constructor is
|
||
* tried to build an instance. In the last two cases the {@link File} argument refers to the <i>Jenkins home directory</i>.
|
||
*
|
||
* @author Kohsuke Kawaguchi
|
||
*/
|
||
@ExportedBean
|
||
public abstract class PluginManager extends AbstractModelObject implements OnMaster, StaplerOverridable, StaplerProxy {
|
||
/** Custom plugin manager system property or context param. */
|
||
public static final String CUSTOM_PLUGIN_MANAGER = PluginManager.class.getName() + ".className";
|
||
|
||
private static final Logger LOGGER = Logger.getLogger(PluginManager.class.getName());
|
||
|
||
/**
|
||
* Time elapsed between retries to check the updates sites. It's kind of constant, but let it so for tests
|
||
*/
|
||
/* private final */ static int CHECK_UPDATE_SLEEP_TIME_MILLIS;
|
||
|
||
/**
|
||
* Number of attempts to check the updates sites. It's kind of constant, but let it so for tests
|
||
*/
|
||
/* private final */ static int CHECK_UPDATE_ATTEMPTS;
|
||
|
||
static {
|
||
try {
|
||
// Secure initialization
|
||
CHECK_UPDATE_SLEEP_TIME_MILLIS = SystemProperties.getInteger(PluginManager.class.getName() + ".checkUpdateSleepTimeMillis", 1000);
|
||
CHECK_UPDATE_ATTEMPTS = SystemProperties.getInteger(PluginManager.class.getName() + ".checkUpdateAttempts", 1);
|
||
} catch(Exception e) {
|
||
LOGGER.warning(String.format("There was an error initializing the PluginManager. Exception: %s", e));
|
||
} finally {
|
||
CHECK_UPDATE_ATTEMPTS = CHECK_UPDATE_ATTEMPTS > 0 ? CHECK_UPDATE_ATTEMPTS : 1;
|
||
CHECK_UPDATE_SLEEP_TIME_MILLIS = CHECK_UPDATE_SLEEP_TIME_MILLIS > 0 ? CHECK_UPDATE_SLEEP_TIME_MILLIS : 1000;
|
||
}
|
||
}
|
||
|
||
/** Accepted constructors for custom plugin manager, in the order they are tried. */
|
||
private enum PMConstructor {
|
||
JENKINS {
|
||
@Override
|
||
@Nonnull
|
||
PluginManager doCreate(@Nonnull Class<? extends PluginManager> klass,
|
||
@Nonnull Jenkins jenkins) throws ReflectiveOperationException {
|
||
return klass.getConstructor(Jenkins.class).newInstance(jenkins);
|
||
}
|
||
},
|
||
SC_FILE {
|
||
@Override
|
||
@Nonnull PluginManager doCreate(@Nonnull Class<? extends PluginManager> klass,
|
||
@Nonnull Jenkins jenkins) throws ReflectiveOperationException {
|
||
return klass.getConstructor(ServletContext.class, File.class).newInstance(jenkins.servletContext, jenkins.getRootDir());
|
||
}
|
||
},
|
||
FILE {
|
||
@Override
|
||
@Nonnull PluginManager doCreate(@Nonnull Class<? extends PluginManager> klass,
|
||
@Nonnull Jenkins jenkins) throws ReflectiveOperationException {
|
||
return klass.getConstructor(File.class).newInstance(jenkins.getRootDir());
|
||
}
|
||
};
|
||
|
||
final @CheckForNull PluginManager create(@Nonnull Class<? extends PluginManager> klass,
|
||
@Nonnull Jenkins jenkins) throws ReflectiveOperationException {
|
||
try {
|
||
return doCreate(klass, jenkins);
|
||
} catch(NoSuchMethodException e) {
|
||
// Constructor not found. Will try the remaining ones.
|
||
return null;
|
||
}
|
||
}
|
||
|
||
abstract @Nonnull PluginManager doCreate(@Nonnull Class<? extends PluginManager> klass,
|
||
@Nonnull Jenkins jenkins) throws ReflectiveOperationException;
|
||
}
|
||
|
||
/**
|
||
* Creates the {@link PluginManager} to use if no one is provided to a {@link Jenkins} object.
|
||
* This method will be called after creation of {@link Jenkins} object, but before it is fully initialized.
|
||
* @param jenkins Jenkins Instance.
|
||
* @return Plugin manager to use. If no custom class is configured or in case of any error, the default
|
||
* {@link LocalPluginManager} is returned.
|
||
*/
|
||
public static @Nonnull PluginManager createDefault(@Nonnull Jenkins jenkins) {
|
||
String pmClassName = SystemProperties.getString(CUSTOM_PLUGIN_MANAGER);
|
||
if (!StringUtils.isBlank(pmClassName)) {
|
||
LOGGER.log(FINE, String.format("Use of custom plugin manager [%s] requested.", pmClassName));
|
||
try {
|
||
final Class<? extends PluginManager> klass = Class.forName(pmClassName).asSubclass(PluginManager.class);
|
||
// Iteration is in declaration order
|
||
for (PMConstructor c : PMConstructor.values()) {
|
||
PluginManager pm = c.create(klass, jenkins);
|
||
if (pm != null) {
|
||
return pm;
|
||
}
|
||
}
|
||
LOGGER.log(WARNING, String.format("Provided custom plugin manager [%s] does not provide any of the suitable constructors. Using default.", pmClassName));
|
||
} catch(NullPointerException e) {
|
||
// Class.forName and Class.getConstructor are supposed to never return null though a broken ClassLoader
|
||
// could break the contract. Just in case we introduce this specific catch to avoid polluting the logs with NPEs.
|
||
LOGGER.log(WARNING, String.format("Unable to instantiate custom plugin manager [%s]. Using default.", pmClassName));
|
||
} catch(ClassCastException e) {
|
||
LOGGER.log(WARNING, String.format("Provided class [%s] does not extend PluginManager. Using default.", pmClassName));
|
||
} catch(Exception e) {
|
||
LOGGER.log(WARNING, String.format("Unable to instantiate custom plugin manager [%s]. Using default.", pmClassName), e);
|
||
}
|
||
}
|
||
return new LocalPluginManager(jenkins);
|
||
}
|
||
|
||
/**
|
||
* All discovered plugins.
|
||
*/
|
||
protected final List<PluginWrapper> plugins = new CopyOnWriteArrayList<>();
|
||
|
||
/**
|
||
* All active plugins, topologically sorted so that when X depends on Y, Y appears in the list before X does.
|
||
*/
|
||
protected final List<PluginWrapper> activePlugins = new CopyOnWriteArrayList<>();
|
||
|
||
protected final List<FailedPlugin> failedPlugins = new ArrayList<>();
|
||
|
||
/**
|
||
* Plug-in root directory.
|
||
*/
|
||
public final File rootDir;
|
||
|
||
/**
|
||
* Hold the status of the last try to check update centers. Consumed from the check.jelly to show an
|
||
* error message if the last attempt failed.
|
||
*/
|
||
private String lastErrorCheckUpdateCenters = null;
|
||
|
||
/**
|
||
* If non-null, the base directory for all exploded .hpi/.jpi plugins. Controlled by the system property / servlet
|
||
* context parameter {@literal hudson.PluginManager.workDir}.
|
||
*/
|
||
@CheckForNull
|
||
private final File workDir;
|
||
|
||
/**
|
||
* @deprecated as of 1.355
|
||
* {@link PluginManager} can now live longer than {@link jenkins.model.Jenkins} instance, so
|
||
* use {@code Hudson.getInstance().servletContext} instead.
|
||
*/
|
||
@Deprecated
|
||
public final ServletContext context;
|
||
|
||
/**
|
||
* {@link ClassLoader} that can load all the publicly visible classes from plugins
|
||
* (and including the classloader that loads Hudson itself.)
|
||
*
|
||
*/
|
||
// implementation is minimal --- just enough to run XStream
|
||
// and load plugin-contributed classes.
|
||
public final ClassLoader uberClassLoader = new UberClassLoader();
|
||
|
||
private final Transformer compatibilityTransformer = new Transformer();
|
||
|
||
/**
|
||
* Once plugin is uploaded, this flag becomes true.
|
||
* This is used to report a message that Jenkins needs to be restarted
|
||
* for new plugins to take effect.
|
||
*/
|
||
public volatile boolean pluginUploaded = false;
|
||
|
||
/**
|
||
* The initialization of {@link PluginManager} splits into two parts;
|
||
* one is the part about listing them, extracting them, and preparing classloader for them.
|
||
* The 2nd part is about creating instances. Once the former completes this flags become true,
|
||
* as the 2nd part can be repeated for each Hudson instance.
|
||
*/
|
||
private boolean pluginListed = false;
|
||
|
||
/**
|
||
* Strategy for creating and initializing plugins
|
||
*/
|
||
private final PluginStrategy strategy;
|
||
|
||
public PluginManager(ServletContext context, File rootDir) {
|
||
this.context = context;
|
||
|
||
this.rootDir = rootDir;
|
||
if(!rootDir.exists())
|
||
rootDir.mkdirs();
|
||
String workDir = SystemProperties.getString(PluginManager.class.getName()+".workDir");
|
||
this.workDir = StringUtils.isBlank(workDir) ? null : new File(workDir);
|
||
|
||
strategy = createPluginStrategy();
|
||
|
||
// load up rules for the core first
|
||
try {
|
||
compatibilityTransformer.loadRules(getClass().getClassLoader());
|
||
} catch (IOException e) {
|
||
LOGGER.log(Level.WARNING, "Failed to load compatibility rewrite rules",e);
|
||
}
|
||
}
|
||
|
||
public Transformer getCompatibilityTransformer() {
|
||
return compatibilityTransformer;
|
||
}
|
||
|
||
public Api getApi() {
|
||
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
|
||
return new Api(this);
|
||
}
|
||
|
||
/**
|
||
* If non-null, the base directory for all exploded .hpi/.jpi plugins.
|
||
* @return the base directory for all exploded .hpi/.jpi plugins or {@code null} to leave this up to the strategy.
|
||
*/
|
||
@CheckForNull
|
||
public File getWorkDir() {
|
||
return workDir;
|
||
}
|
||
|
||
/**
|
||
* Find all registered overrides (intended to allow overriding/adding views)
|
||
* @return List of extensions
|
||
* @since 1.627
|
||
*/
|
||
@Override
|
||
public Collection<PluginManagerStaplerOverride> getOverrides() {
|
||
return PluginManagerStaplerOverride.all();
|
||
}
|
||
|
||
/**
|
||
* Called immediately after the construction.
|
||
* This is a separate method so that code executed from here will see a valid value in
|
||
* {@link jenkins.model.Jenkins#pluginManager}.
|
||
*/
|
||
public TaskBuilder initTasks(final InitStrategy initStrategy) {
|
||
TaskBuilder builder;
|
||
if (!pluginListed) {
|
||
builder = new TaskGraphBuilder() {
|
||
List<File> archives;
|
||
Collection<String> bundledPlugins;
|
||
|
||
{
|
||
Handle loadBundledPlugins = add("Loading bundled plugins", new Executable() {
|
||
public void run(Reactor session) throws Exception {
|
||
bundledPlugins = loadBundledPlugins();
|
||
}
|
||
});
|
||
|
||
Handle listUpPlugins = requires(loadBundledPlugins).add("Listing up plugins", new Executable() {
|
||
public void run(Reactor session) throws Exception {
|
||
archives = initStrategy.listPluginArchives(PluginManager.this);
|
||
}
|
||
});
|
||
|
||
requires(listUpPlugins).attains(PLUGINS_LISTED).add("Preparing plugins",new Executable() {
|
||
public void run(Reactor session) throws Exception {
|
||
// once we've listed plugins, we can fill in the reactor with plugin-specific initialization tasks
|
||
TaskGraphBuilder g = new TaskGraphBuilder();
|
||
|
||
final Map<String,File> inspectedShortNames = new HashMap<>();
|
||
|
||
for( final File arc : archives ) {
|
||
g.followedBy().notFatal().attains(PLUGINS_LISTED).add("Inspecting plugin " + arc, new Executable() {
|
||
public void run(Reactor session1) throws Exception {
|
||
try {
|
||
PluginWrapper p = strategy.createPluginWrapper(arc);
|
||
if (isDuplicate(p)) return;
|
||
|
||
p.isBundled = containsHpiJpi(bundledPlugins, arc.getName());
|
||
plugins.add(p);
|
||
} catch (IOException e) {
|
||
failedPlugins.add(new FailedPlugin(arc.getName(),e));
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Inspects duplication. this happens when you run hpi:run on a bundled plugin,
|
||
* as well as putting numbered jpi files, like "cobertura-1.0.jpi" and "cobertura-1.1.jpi"
|
||
*/
|
||
private boolean isDuplicate(PluginWrapper p) {
|
||
String shortName = p.getShortName();
|
||
if (inspectedShortNames.containsKey(shortName)) {
|
||
LOGGER.info("Ignoring "+arc+" because "+inspectedShortNames.get(shortName)+" is already loaded");
|
||
return true;
|
||
}
|
||
|
||
inspectedShortNames.put(shortName,arc);
|
||
return false;
|
||
}
|
||
});
|
||
}
|
||
|
||
g.followedBy().attains(PLUGINS_LISTED).add("Checking cyclic dependencies", new Executable() {
|
||
/**
|
||
* Makes sure there's no cycle in dependencies.
|
||
*/
|
||
public void run(Reactor reactor) throws Exception {
|
||
try {
|
||
CyclicGraphDetector<PluginWrapper> cgd = new CyclicGraphDetector<PluginWrapper>() {
|
||
@Override
|
||
protected List<PluginWrapper> getEdges(PluginWrapper p) {
|
||
List<PluginWrapper> next = new ArrayList<>();
|
||
addTo(p.getDependencies(), next);
|
||
addTo(p.getOptionalDependencies(), next);
|
||
return next;
|
||
}
|
||
|
||
private void addTo(List<Dependency> dependencies, List<PluginWrapper> r) {
|
||
for (Dependency d : dependencies) {
|
||
PluginWrapper p = getPlugin(d.shortName);
|
||
if (p != null)
|
||
r.add(p);
|
||
}
|
||
}
|
||
|
||
@Override
|
||
protected void reactOnCycle(PluginWrapper q, List<PluginWrapper> cycle)
|
||
throws hudson.util.CyclicGraphDetector.CycleDetectedException {
|
||
|
||
LOGGER.log(Level.SEVERE, "found cycle in plugin dependencies: (root="+q+", deactivating all involved) "+Util.join(cycle," -> "));
|
||
for (PluginWrapper pluginWrapper : cycle) {
|
||
pluginWrapper.setHasCycleDependency(true);
|
||
failedPlugins.add(new FailedPlugin(pluginWrapper.getShortName(), new CycleDetectedException(cycle)));
|
||
}
|
||
}
|
||
|
||
};
|
||
cgd.run(getPlugins());
|
||
|
||
// obtain topologically sorted list and overwrite the list
|
||
for (PluginWrapper p : cgd.getSorted()) {
|
||
if(p.isActive())
|
||
activePlugins.add(p);
|
||
}
|
||
} catch (CycleDetectedException e) { // TODO this should be impossible, since we override reactOnCycle to not throw the exception
|
||
stop(); // disable all plugins since classloading from them can lead to StackOverflow
|
||
throw e; // let Hudson fail
|
||
}
|
||
}
|
||
});
|
||
|
||
// Let's see for a while until we open this functionality up to plugins
|
||
// g.followedBy().attains(PLUGINS_LISTED).add("Load compatibility rules", new Executable() {
|
||
// public void run(Reactor reactor) throws Exception {
|
||
// compatibilityTransformer.loadRules(uberClassLoader);
|
||
// }
|
||
// });
|
||
|
||
session.addAll(g.discoverTasks(session));
|
||
|
||
pluginListed = true; // technically speaking this is still too early, as at this point tasks are merely scheduled, not necessarily executed.
|
||
}
|
||
});
|
||
}
|
||
};
|
||
} else {
|
||
builder = TaskBuilder.EMPTY_BUILDER;
|
||
}
|
||
|
||
final InitializerFinder initializerFinder = new InitializerFinder(uberClassLoader); // misc. stuff
|
||
|
||
// lists up initialization tasks about loading plugins.
|
||
return TaskBuilder.union(initializerFinder, // this scans @Initializer in the core once
|
||
builder, new TaskGraphBuilder() {{
|
||
requires(PLUGINS_LISTED).attains(PLUGINS_PREPARED).add("Loading plugins", new Executable() {
|
||
/**
|
||
* Once the plugins are listed, schedule their initialization.
|
||
*/
|
||
public void run(Reactor session) throws Exception {
|
||
Jenkins.get().lookup.set(PluginInstanceStore.class, new PluginInstanceStore());
|
||
TaskGraphBuilder g = new TaskGraphBuilder();
|
||
|
||
// schedule execution of loading plugins
|
||
for (final PluginWrapper p : activePlugins.toArray(new PluginWrapper[0])) {
|
||
g.followedBy().notFatal().attains(PLUGINS_PREPARED).add(String.format("Loading plugin %s v%s (%s)", p.getLongName(), p.getVersion(), p.getShortName()), new Executable() {
|
||
public void run(Reactor session) throws Exception {
|
||
try {
|
||
p.resolvePluginDependencies();
|
||
strategy.load(p);
|
||
} catch (MissingDependencyException e) {
|
||
failedPlugins.add(new FailedPlugin(p.getShortName(), e));
|
||
activePlugins.remove(p);
|
||
plugins.remove(p);
|
||
LOGGER.log(Level.SEVERE, "Failed to install {0}: {1}", new Object[] { p.getShortName(), e.getMessage() });
|
||
return;
|
||
} catch (IOException e) {
|
||
failedPlugins.add(new FailedPlugin(p.getShortName(), e));
|
||
activePlugins.remove(p);
|
||
plugins.remove(p);
|
||
throw e;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// schedule execution of initializing plugins
|
||
for (final PluginWrapper p : activePlugins.toArray(new PluginWrapper[0])) {
|
||
g.followedBy().notFatal().attains(PLUGINS_STARTED).add("Initializing plugin " + p.getShortName(), new Executable() {
|
||
public void run(Reactor session) throws Exception {
|
||
if (!activePlugins.contains(p)) {
|
||
return;
|
||
}
|
||
try {
|
||
p.getPlugin().postInitialize();
|
||
} catch (Exception e) {
|
||
failedPlugins.add(new FailedPlugin(p.getShortName(), e));
|
||
activePlugins.remove(p);
|
||
plugins.remove(p);
|
||
throw e;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
g.followedBy().attains(PLUGINS_STARTED).add("Discovering plugin initialization tasks", new Executable() {
|
||
public void run(Reactor reactor) throws Exception {
|
||
// rescan to find plugin-contributed @Initializer
|
||
reactor.addAll(initializerFinder.discoverTasks(reactor));
|
||
}
|
||
});
|
||
|
||
// register them all
|
||
session.addAll(g.discoverTasks(session));
|
||
}
|
||
});
|
||
|
||
// All plugins are loaded. Now we can figure out who depends on who.
|
||
requires(PLUGINS_PREPARED).attains(COMPLETED).add("Resolving Dependent Plugins Graph", new Executable() {
|
||
@Override
|
||
public void run(Reactor reactor) throws Exception {
|
||
resolveDependentPlugins();
|
||
}
|
||
});
|
||
}});
|
||
}
|
||
|
||
void considerDetachedPlugin(String shortName) {
|
||
if (new File(rootDir, shortName + ".jpi").isFile() ||
|
||
new File(rootDir, shortName + ".hpi").isFile() ||
|
||
new File(rootDir, shortName + ".jpl").isFile() ||
|
||
new File(rootDir, shortName + ".hpl").isFile()) {
|
||
LOGGER.fine(() -> "not considering loading a detached dependency " + shortName + " as it is already on disk");
|
||
return;
|
||
}
|
||
LOGGER.fine(() -> "considering loading a detached dependency " + shortName);
|
||
for (String loadedFile : loadPluginsFromWar("/WEB-INF/detached-plugins", (dir, name) -> normalisePluginName(name).equals(shortName))) {
|
||
String loaded = normalisePluginName(loadedFile);
|
||
File arc = new File(rootDir, loaded + ".jpi");
|
||
LOGGER.info(() -> "Loading a detached plugin as a dependency: " + arc);
|
||
try {
|
||
plugins.add(strategy.createPluginWrapper(arc));
|
||
} catch (IOException e) {
|
||
failedPlugins.add(new FailedPlugin(arc.getName(), e));
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
protected @Nonnull Set<String> loadPluginsFromWar(@Nonnull String fromPath) {
|
||
return loadPluginsFromWar(fromPath, null);
|
||
}
|
||
|
||
//TODO: Consider refactoring in order to avoid DMI_COLLECTION_OF_URLS
|
||
@SuppressFBWarnings(value = "DMI_COLLECTION_OF_URLS", justification = "Plugin loading happens only once on Jenkins startup")
|
||
protected @Nonnull Set<String> loadPluginsFromWar(@Nonnull String fromPath, @CheckForNull FilenameFilter filter) {
|
||
Set<String> names = new HashSet();
|
||
|
||
ServletContext context = Jenkins.get().servletContext;
|
||
Set<String> plugins = Util.fixNull(context.getResourcePaths(fromPath));
|
||
Set<URL> copiedPlugins = new HashSet<>();
|
||
Set<URL> dependencies = new HashSet<>();
|
||
|
||
for( String pluginPath : plugins) {
|
||
String fileName = pluginPath.substring(pluginPath.lastIndexOf('/')+1);
|
||
if(fileName.length()==0) {
|
||
// see http://www.nabble.com/404-Not-Found-error-when-clicking-on-help-td24508544.html
|
||
// I suspect some containers are returning directory names.
|
||
continue;
|
||
}
|
||
try {
|
||
URL url = context.getResource(pluginPath);
|
||
if (filter != null && url != null) {
|
||
if (!filter.accept(new File(url.getFile()).getParentFile(), fileName)) {
|
||
continue;
|
||
}
|
||
}
|
||
|
||
names.add(fileName);
|
||
copyBundledPlugin(url, fileName);
|
||
copiedPlugins.add(url);
|
||
try {
|
||
addDependencies(url, fromPath, dependencies);
|
||
} catch (Exception e) {
|
||
LOGGER.log(Level.SEVERE, "Failed to resolve dependencies for the bundled plugin " + fileName, e);
|
||
}
|
||
} catch (IOException e) {
|
||
LOGGER.log(Level.SEVERE, "Failed to extract the bundled plugin "+fileName,e);
|
||
}
|
||
}
|
||
|
||
// Copy dependencies. These are not detached plugins, but are required by them.
|
||
for (URL dependency : dependencies) {
|
||
if (copiedPlugins.contains(dependency)) {
|
||
// Ignore. Already copied.
|
||
continue;
|
||
}
|
||
|
||
String fileName = new File(dependency.getFile()).getName();
|
||
try {
|
||
names.add(fileName);
|
||
copyBundledPlugin(dependency, fileName);
|
||
copiedPlugins.add(dependency);
|
||
} catch (IOException e) {
|
||
LOGGER.log(Level.SEVERE, "Failed to extract the bundled dependency plugin " + fileName, e);
|
||
}
|
||
}
|
||
|
||
return names;
|
||
}
|
||
|
||
//TODO: Consider refactoring in order to avoid DMI_COLLECTION_OF_URLS
|
||
@SuppressFBWarnings(value = "DMI_COLLECTION_OF_URLS", justification = "Plugin loading happens only once on Jenkins startup")
|
||
protected static void addDependencies(URL hpiResUrl, String fromPath, Set<URL> dependencySet) throws URISyntaxException, MalformedURLException {
|
||
if (dependencySet.contains(hpiResUrl)) {
|
||
return;
|
||
}
|
||
|
||
Manifest manifest = parsePluginManifest(hpiResUrl);
|
||
String dependencySpec = manifest.getMainAttributes().getValue("Plugin-Dependencies");
|
||
if (dependencySpec != null) {
|
||
String[] dependencyTokens = dependencySpec.split(",");
|
||
ServletContext context = Jenkins.get().servletContext;
|
||
|
||
for (String dependencyToken : dependencyTokens) {
|
||
if (dependencyToken.endsWith(";resolution:=optional")) {
|
||
// ignore optional dependencies
|
||
continue;
|
||
}
|
||
|
||
String[] artifactIdVersionPair = dependencyToken.split(":");
|
||
String artifactId = artifactIdVersionPair[0];
|
||
VersionNumber dependencyVersion = new VersionNumber(artifactIdVersionPair[1]);
|
||
|
||
PluginManager manager = Jenkins.get().getPluginManager();
|
||
VersionNumber installedVersion = manager.getPluginVersion(manager.rootDir, artifactId);
|
||
if (installedVersion != null && !installedVersion.isOlderThan(dependencyVersion)) {
|
||
// Do not downgrade dependencies that are already installed.
|
||
continue;
|
||
}
|
||
|
||
URL dependencyURL = context.getResource(fromPath + "/" + artifactId + ".hpi");
|
||
|
||
if (dependencyURL == null) {
|
||
// Maybe bundling has changed .jpi files
|
||
dependencyURL = context.getResource(fromPath + "/" + artifactId + ".jpi");
|
||
}
|
||
|
||
if (dependencyURL != null) {
|
||
// And transitive deps...
|
||
addDependencies(dependencyURL, fromPath, dependencySet);
|
||
// And then add the current plugin
|
||
dependencySet.add(dependencyURL);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Load detached plugins and their dependencies.
|
||
* <p>
|
||
* Only loads plugins that:
|
||
* <ul>
|
||
* <li>Have been detached since the last running version.</li>
|
||
* <li>Are already installed and need to be upgraded. This can be the case if this Jenkins install has been running since before plugins were "unbundled".</li>
|
||
* <li>Are dependencies of one of the above e.g. script-security is not one of the detached plugins but it must be loaded if matrix-project is loaded.</li>
|
||
* </ul>
|
||
*/
|
||
protected void loadDetachedPlugins() {
|
||
VersionNumber lastExecVersion = new VersionNumber(InstallUtil.getLastExecVersion());
|
||
if (lastExecVersion.isNewerThan(InstallUtil.NEW_INSTALL_VERSION) && lastExecVersion.isOlderThan(Jenkins.getVersion())) {
|
||
|
||
LOGGER.log(INFO, "Upgrading Jenkins. The last running version was {0}. This Jenkins is version {1}.",
|
||
new Object[] {lastExecVersion, Jenkins.VERSION});
|
||
|
||
final List<DetachedPluginsUtil.DetachedPlugin> detachedPlugins = DetachedPluginsUtil.getDetachedPlugins(lastExecVersion);
|
||
|
||
Set<String> loadedDetached = loadPluginsFromWar("/WEB-INF/detached-plugins", new FilenameFilter() {
|
||
@Override
|
||
public boolean accept(File dir, String name) {
|
||
name = normalisePluginName(name);
|
||
|
||
// If this was a plugin that was detached some time in the past i.e. not just one of the
|
||
// plugins that was bundled "for fun".
|
||
if (DetachedPluginsUtil.isDetachedPlugin(name)) {
|
||
VersionNumber installedVersion = getPluginVersion(rootDir, name);
|
||
VersionNumber bundledVersion = getPluginVersion(dir, name);
|
||
// If the plugin is already installed, we need to decide whether to replace it with the bundled version.
|
||
if (installedVersion != null && bundledVersion != null) {
|
||
// If the installed version is older than the bundled version, then it MUST be upgraded.
|
||
// If the installed version is newer than the bundled version, then it MUST NOT be upgraded.
|
||
// If the versions are equal we just keep the installed version.
|
||
return installedVersion.isOlderThan(bundledVersion);
|
||
}
|
||
}
|
||
|
||
// If it's a plugin that was detached since the last running version.
|
||
for (DetachedPluginsUtil.DetachedPlugin detachedPlugin : detachedPlugins) {
|
||
if (detachedPlugin.getShortName().equals(name)) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// Otherwise skip this and do not install.
|
||
return false;
|
||
}
|
||
});
|
||
|
||
LOGGER.log(INFO, "Upgraded Jenkins from version {0} to version {1}. Loaded detached plugins (and dependencies): {2}",
|
||
new Object[] {lastExecVersion, Jenkins.VERSION, loadedDetached});
|
||
|
||
InstallUtil.saveLastExecVersion();
|
||
} else {
|
||
final Set<DetachedPluginsUtil.DetachedPlugin> forceUpgrade = new HashSet<>();
|
||
// TODO using getDetachedPlugins here seems wrong; should be forcing an upgrade when the installed version is older than that in WEB-INF/detached-plugins/
|
||
for (DetachedPluginsUtil.DetachedPlugin p : DetachedPluginsUtil.getDetachedPlugins()) {
|
||
VersionNumber installedVersion = getPluginVersion(rootDir, p.getShortName());
|
||
VersionNumber requiredVersion = p.getRequiredVersion();
|
||
if (installedVersion != null && installedVersion.isOlderThan(requiredVersion)) {
|
||
LOGGER.log(Level.WARNING,
|
||
"Detached plugin {0} found at version {1}, required minimum version is {2}",
|
||
new Object[]{p.getShortName(), installedVersion, requiredVersion});
|
||
forceUpgrade.add(p);
|
||
}
|
||
}
|
||
if (!forceUpgrade.isEmpty()) {
|
||
Set<String> loadedDetached = loadPluginsFromWar("/WEB-INF/detached-plugins", new FilenameFilter() {
|
||
@Override
|
||
public boolean accept(File dir, String name) {
|
||
name = normalisePluginName(name);
|
||
for (DetachedPluginsUtil.DetachedPlugin detachedPlugin : forceUpgrade) {
|
||
if (detachedPlugin.getShortName().equals(name)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
});
|
||
LOGGER.log(INFO, "Upgraded detached plugins (and dependencies): {0}",
|
||
new Object[]{loadedDetached});
|
||
}
|
||
}
|
||
}
|
||
|
||
private String normalisePluginName(@Nonnull String name) {
|
||
// Normalise the name by stripping off the file extension (if present)...
|
||
return name.replace(".jpi", "").replace(".hpi", "");
|
||
}
|
||
|
||
private @CheckForNull VersionNumber getPluginVersion(@Nonnull File dir, @Nonnull String pluginId) {
|
||
VersionNumber version = getPluginVersion(new File(dir, pluginId + ".jpi"));
|
||
if (version == null) {
|
||
version = getPluginVersion(new File(dir, pluginId + ".hpi"));
|
||
}
|
||
return version;
|
||
}
|
||
|
||
private @CheckForNull VersionNumber getPluginVersion(@Nonnull File pluginFile) {
|
||
if (!pluginFile.exists()) {
|
||
return null;
|
||
}
|
||
try {
|
||
return getPluginVersion(pluginFile.toURI().toURL());
|
||
} catch (MalformedURLException e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private @CheckForNull VersionNumber getPluginVersion(@Nonnull URL pluginURL) {
|
||
Manifest manifest = parsePluginManifest(pluginURL);
|
||
if (manifest == null) {
|
||
return null;
|
||
}
|
||
String versionSpec = manifest.getMainAttributes().getValue("Plugin-Version");
|
||
return new VersionNumber(versionSpec);
|
||
}
|
||
|
||
/*
|
||
* contains operation that considers xxx.hpi and xxx.jpi as equal
|
||
* this is necessary since the bundled plugins are still called *.hpi
|
||
*/
|
||
private boolean containsHpiJpi(Collection<String> bundledPlugins, String name) {
|
||
return bundledPlugins.contains(name.replaceAll("\\.hpi",".jpi"))
|
||
|| bundledPlugins.contains(name.replaceAll("\\.jpi",".hpi"));
|
||
}
|
||
|
||
/**
|
||
* Returns the manifest of a bundled but not-extracted plugin.
|
||
*/
|
||
@Deprecated // See https://groups.google.com/d/msg/jenkinsci-dev/kRobm-cxFw8/6V66uhibAwAJ
|
||
public @CheckForNull Manifest getBundledPluginManifest(String shortName) {
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* TODO: revisit where/how to expose this. This is an experiment.
|
||
*/
|
||
public void dynamicLoad(File arc) throws IOException, InterruptedException, RestartRequiredException {
|
||
dynamicLoad(arc, false, null);
|
||
}
|
||
|
||
/**
|
||
* Try the dynamicLoad, removeExisting to attempt to dynamic load disabled plugins
|
||
*/
|
||
@Restricted(NoExternalUse.class)
|
||
public void dynamicLoad(File arc, boolean removeExisting, @CheckForNull List<PluginWrapper> batch) throws IOException, InterruptedException, RestartRequiredException {
|
||
try (ACLContext context = ACL.as(ACL.SYSTEM)) {
|
||
LOGGER.log(FINE, "Attempting to dynamic load {0}", arc);
|
||
PluginWrapper p = null;
|
||
String sn;
|
||
try {
|
||
sn = strategy.getShortName(arc);
|
||
} catch (AbstractMethodError x) {
|
||
LOGGER.log(WARNING, "JENKINS-12753 fix not active: {0}", x.getMessage());
|
||
p = strategy.createPluginWrapper(arc);
|
||
sn = p.getShortName();
|
||
}
|
||
PluginWrapper pw = getPlugin(sn);
|
||
if (pw!=null) {
|
||
if (removeExisting) { // try to load disabled plugins
|
||
for (Iterator<PluginWrapper> i = plugins.iterator(); i.hasNext();) {
|
||
pw = i.next();
|
||
if(sn.equals(pw.getShortName())) {
|
||
i.remove();
|
||
break;
|
||
}
|
||
}
|
||
} else {
|
||
throw new RestartRequiredException(Messages._PluginManager_PluginIsAlreadyInstalled_RestartRequired(sn));
|
||
}
|
||
}
|
||
if (p == null) {
|
||
p = strategy.createPluginWrapper(arc);
|
||
}
|
||
if (p.supportsDynamicLoad()== YesNoMaybe.NO)
|
||
throw new RestartRequiredException(Messages._PluginManager_PluginDoesntSupportDynamicLoad_RestartRequired(sn));
|
||
|
||
// there's no need to do cyclic dependency check, because we are deploying one at a time,
|
||
// so existing plugins can't be depending on this newly deployed one.
|
||
|
||
plugins.add(p);
|
||
if (p.isActive())
|
||
activePlugins.add(p);
|
||
synchronized (((UberClassLoader) uberClassLoader).loaded) {
|
||
((UberClassLoader) uberClassLoader).loaded.clear();
|
||
}
|
||
|
||
// TODO antimodular; perhaps should have a PluginListener to complement ExtensionListListener?
|
||
CustomClassFilter.Contributed.load();
|
||
|
||
try {
|
||
p.resolvePluginDependencies();
|
||
strategy.load(p);
|
||
|
||
if (batch != null) {
|
||
batch.add(p);
|
||
} else {
|
||
start(Collections.singletonList(p));
|
||
}
|
||
|
||
} catch (Exception e) {
|
||
failedPlugins.add(new FailedPlugin(sn, e));
|
||
activePlugins.remove(p);
|
||
plugins.remove(p);
|
||
throw new IOException("Failed to install "+ sn +" plugin",e);
|
||
}
|
||
|
||
LOGGER.log(FINE, "Plugin {0}:{1} dynamically {2}", new Object[] {p.getShortName(), p.getVersion(), batch != null ? "loaded but not yet started" : "installed"});
|
||
}
|
||
}
|
||
|
||
@Restricted(NoExternalUse.class)
|
||
public void start(List<PluginWrapper> plugins) throws Exception {
|
||
try (ACLContext context = ACL.as(ACL.SYSTEM)) {
|
||
Map<String, PluginWrapper> pluginsByName = plugins.stream().collect(Collectors.toMap(p -> p.getShortName(), p -> p));
|
||
|
||
// recalculate dependencies of plugins optionally depending the newly deployed ones.
|
||
for (PluginWrapper depender: this.plugins) {
|
||
if (plugins.contains(depender)) {
|
||
// skip itself.
|
||
continue;
|
||
}
|
||
for (Dependency d: depender.getOptionalDependencies()) {
|
||
PluginWrapper dependee = pluginsByName.get(d.shortName);
|
||
if (dependee != null) {
|
||
// this plugin depends on the newly loaded one!
|
||
// recalculate dependencies!
|
||
getPluginStrategy().updateDependency(depender, dependee);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Redo who depends on who.
|
||
resolveDependentPlugins();
|
||
|
||
try {
|
||
Jenkins.get().refreshExtensions();
|
||
} catch (ExtensionRefreshException e) {
|
||
throw new IOException("Failed to refresh extensions after installing some plugins", e);
|
||
}
|
||
for (PluginWrapper p : plugins) {
|
||
p.getPlugin().postInitialize();
|
||
}
|
||
|
||
// run initializers in the added plugins
|
||
Reactor r = new Reactor(InitMilestone.ordering());
|
||
Set<ClassLoader> loaders = plugins.stream().map(p -> p.classLoader).collect(Collectors.toSet());
|
||
r.addAll(new InitializerFinder(uberClassLoader) {
|
||
@Override
|
||
protected boolean filter(Method e) {
|
||
return !loaders.contains(e.getDeclaringClass().getClassLoader()) || super.filter(e);
|
||
}
|
||
}.discoverTasks(r));
|
||
new InitReactorRunner().run(r);
|
||
}
|
||
}
|
||
|
||
@Restricted(NoExternalUse.class)
|
||
public synchronized void resolveDependentPlugins() {
|
||
for (PluginWrapper plugin : plugins) {
|
||
// Set of optional dependents plugins of plugin
|
||
Set<String> optionalDependents = new HashSet<>();
|
||
Set<String> dependents = new HashSet<>();
|
||
for (PluginWrapper possibleDependent : plugins) {
|
||
// No need to check if plugin is dependent of itself
|
||
if(possibleDependent.getShortName().equals(plugin.getShortName())) {
|
||
continue;
|
||
}
|
||
|
||
// The plugin could have just been deleted. If so, it doesn't
|
||
// count as a dependent.
|
||
if (possibleDependent.isDeleted()) {
|
||
continue;
|
||
}
|
||
List<Dependency> dependencies = possibleDependent.getDependencies();
|
||
for (Dependency dependency : dependencies) {
|
||
if (dependency.shortName.equals(plugin.getShortName())) {
|
||
dependents.add(possibleDependent.getShortName());
|
||
|
||
// If, in addition, the dependency is optional, add to the optionalDependents list
|
||
if (dependency.optional) {
|
||
optionalDependents.add(possibleDependent.getShortName());
|
||
}
|
||
|
||
// already know possibleDependent depends on plugin, no need to continue with the rest of
|
||
// dependencies. We continue with the next possibleDependent
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
plugin.setDependents(dependents);
|
||
plugin.setOptionalDependents(optionalDependents);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* If the war file has any "/WEB-INF/plugins/[*.jpi | *.hpi]", extract them into the plugin directory.
|
||
*
|
||
* @return
|
||
* File names of the bundled plugins. Normally empty (not to be confused with {@link #loadDetachedPlugins}) but OEM WARs may have some.
|
||
* @throws Exception
|
||
* Any exception will be reported and halt the startup.
|
||
*/
|
||
protected abstract Collection<String> loadBundledPlugins() throws Exception;
|
||
|
||
/**
|
||
* Copies the plugin from the given URL to the given destination.
|
||
* Despite the name, this is used also from {@link #loadDetachedPlugins}.
|
||
* Includes a reasonable up-to-date check.
|
||
* A convenience method to be used by {@link #loadBundledPlugins()}.
|
||
* @param fileName like {@code abc.jpi}
|
||
*/
|
||
protected void copyBundledPlugin(URL src, String fileName) throws IOException {
|
||
LOGGER.log(FINE, "Copying {0}", src);
|
||
fileName = fileName.replace(".hpi",".jpi"); // normalize fileNames to have the correct suffix
|
||
String legacyName = fileName.replace(".jpi",".hpi");
|
||
long lastModified = getModificationDate(src);
|
||
File file = new File(rootDir, fileName);
|
||
|
||
// normalization first, if the old file exists.
|
||
rename(new File(rootDir,legacyName),file);
|
||
|
||
// update file if:
|
||
// - no file exists today
|
||
// - bundled version and current version differs (by timestamp).
|
||
if (!file.exists() || file.lastModified() != lastModified) {
|
||
FileUtils.copyURLToFile(src, file);
|
||
file.setLastModified(getModificationDate(src));
|
||
// lastModified is set for two reasons:
|
||
// - to avoid unpacking as much as possible, but still do it on both upgrade and downgrade
|
||
// - to make sure the value is not changed after each restart, so we can avoid
|
||
// unpacking the plugin itself in ClassicPluginStrategy.explode
|
||
}
|
||
|
||
// Plugin pinning has been deprecated.
|
||
// See https://groups.google.com/d/msg/jenkinsci-dev/kRobm-cxFw8/6V66uhibAwAJ
|
||
}
|
||
|
||
/*package*/ static @CheckForNull Manifest parsePluginManifest(URL bundledJpi) {
|
||
try {
|
||
URLClassLoader cl = new URLClassLoader(new URL[]{bundledJpi});
|
||
InputStream in=null;
|
||
try {
|
||
URL res = cl.findResource(PluginWrapper.MANIFEST_FILENAME);
|
||
if (res!=null) {
|
||
in = getBundledJpiManifestStream(res);
|
||
return new Manifest(in);
|
||
}
|
||
} finally {
|
||
Util.closeAndLogFailures(in, LOGGER, PluginWrapper.MANIFEST_FILENAME, bundledJpi.toString());
|
||
if (cl instanceof Closeable)
|
||
((Closeable)cl).close();
|
||
}
|
||
} catch (IOException e) {
|
||
LOGGER.log(WARNING, "Failed to parse manifest of "+bundledJpi, e);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Retrieves input stream for the Manifest url.
|
||
* The method intelligently handles the case of {@link JarURLConnection} pointing to files within JAR.
|
||
* @param url Url of the manifest file
|
||
* @return Input stream, which allows to retrieve manifest. This stream must be closed outside
|
||
* @throws IOException Operation error
|
||
*/
|
||
@Nonnull
|
||
/*package*/ static InputStream getBundledJpiManifestStream(@Nonnull URL url) throws IOException {
|
||
URLConnection uc = url.openConnection();
|
||
InputStream in = null;
|
||
// Magic, which allows to avoid using stream generated for JarURLConnection.
|
||
// It prevents getting into JENKINS-37332 due to the file descriptor leak
|
||
if (uc instanceof JarURLConnection) {
|
||
final JarURLConnection jarURLConnection = (JarURLConnection) uc;
|
||
final String entryName = jarURLConnection.getEntryName();
|
||
|
||
try(final JarFile jarFile = jarURLConnection.getJarFile()) {
|
||
final JarEntry entry = (entryName != null && jarFile != null) ? jarFile.getJarEntry(entryName) : null;
|
||
if (entry != null) {
|
||
try(InputStream i = jarFile.getInputStream(entry)) {
|
||
byte[] manifestBytes = IOUtils.toByteArray(i);
|
||
in = new ByteArrayInputStream(manifestBytes);
|
||
}
|
||
} else {
|
||
LOGGER.log(Level.WARNING, "Failed to locate the JAR file for {0}"
|
||
+ "The default URLConnection stream access will be used, file descriptor may be leaked.",
|
||
url);
|
||
}
|
||
}
|
||
}
|
||
|
||
// If input stream is undefined, use the default implementation
|
||
if (in == null) {
|
||
in = url.openStream();
|
||
}
|
||
|
||
return in;
|
||
}
|
||
|
||
/**
|
||
* Retrieves modification date of the specified file.
|
||
* The method intelligently handles the case of {@link JarURLConnection} pointing to files within JAR.
|
||
* @param url Url of the file
|
||
* @return Modification date
|
||
* @throws IOException Operation error
|
||
*/
|
||
@Nonnull
|
||
/*package*/ static long getModificationDate(@Nonnull URL url) throws IOException {
|
||
URLConnection uc = url.openConnection();
|
||
|
||
// It prevents file descriptor leak if the URL references a file within JAR
|
||
// See JENKINS-37332 for more info
|
||
// The code idea is taken from https://github.com/jknack/handlebars.java/pull/394
|
||
if (uc instanceof JarURLConnection) {
|
||
final JarURLConnection connection = (JarURLConnection) uc;
|
||
final URL jarURL = connection.getJarFileURL();
|
||
if (jarURL.getProtocol().equals("file")) {
|
||
String file = jarURL.getFile();
|
||
return new File(file).lastModified();
|
||
} else {
|
||
// We access the data without file protocol
|
||
if (connection.getEntryName() != null) {
|
||
LOGGER.log(WARNING, "Accessing modification date of {0} file, which is an entry in JAR file. "
|
||
+ "The access protocol is not file:, falling back to the default logic (risk of file descriptor leak).",
|
||
url);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fallbak to the default implementation
|
||
return uc.getLastModified();
|
||
}
|
||
|
||
/**
|
||
* Rename a legacy file to a new name, with care to Windows where {@link File#renameTo(File)}
|
||
* doesn't work if the destination already exists.
|
||
*/
|
||
private void rename(File legacyFile, File newFile) throws IOException {
|
||
if (!legacyFile.exists()) return;
|
||
if (newFile.exists()) {
|
||
Util.deleteFile(newFile);
|
||
}
|
||
if (!legacyFile.renameTo(newFile)) {
|
||
LOGGER.warning("Failed to rename " + legacyFile + " to " + newFile);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Creates a hudson.PluginStrategy, looking at the corresponding system property.
|
||
*/
|
||
protected PluginStrategy createPluginStrategy() {
|
||
String strategyName = SystemProperties.getString(PluginStrategy.class.getName());
|
||
if (strategyName != null) {
|
||
try {
|
||
Class<?> klazz = getClass().getClassLoader().loadClass(strategyName);
|
||
Object strategy = klazz.getConstructor(PluginManager.class)
|
||
.newInstance(this);
|
||
if (strategy instanceof PluginStrategy) {
|
||
LOGGER.info("Plugin strategy: " + strategyName);
|
||
return (PluginStrategy) strategy;
|
||
} else {
|
||
LOGGER.warning("Plugin strategy (" + strategyName +
|
||
") is not an instance of hudson.PluginStrategy");
|
||
}
|
||
} catch (ClassNotFoundException e) {
|
||
LOGGER.warning("Plugin strategy class not found: "
|
||
+ strategyName);
|
||
} catch (Exception e) {
|
||
LOGGER.log(WARNING, "Could not instantiate plugin strategy: "
|
||
+ strategyName + ". Falling back to ClassicPluginStrategy", e);
|
||
}
|
||
LOGGER.info("Falling back to ClassicPluginStrategy");
|
||
}
|
||
|
||
// default and fallback
|
||
return new ClassicPluginStrategy(this);
|
||
}
|
||
|
||
public PluginStrategy getPluginStrategy() {
|
||
return strategy;
|
||
}
|
||
|
||
/**
|
||
* Returns true if any new plugin was added.
|
||
*/
|
||
public boolean isPluginUploaded() {
|
||
return pluginUploaded;
|
||
}
|
||
|
||
/**
|
||
* All discovered plugins.
|
||
*/
|
||
@Exported
|
||
public List<PluginWrapper> getPlugins() {
|
||
return Collections.unmodifiableList(plugins);
|
||
}
|
||
|
||
public List<FailedPlugin> getFailedPlugins() {
|
||
return failedPlugins;
|
||
}
|
||
|
||
/**
|
||
* Get the plugin instance with the given short name.
|
||
* @param shortName the short name of the plugin
|
||
* @return The plugin singleton or {@code null} if a plugin with the given short name does not exist.
|
||
* The fact the plugin is loaded does not mean it is enabled and fully initialized for the current Jenkins session.
|
||
* Use {@link PluginWrapper#isActive()} to check it.
|
||
*/
|
||
@CheckForNull
|
||
public PluginWrapper getPlugin(String shortName) {
|
||
for (PluginWrapper p : getPlugins()) {
|
||
if(p.getShortName().equals(shortName))
|
||
return p;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Get the plugin instance that implements a specific class, use to find your plugin singleton.
|
||
* Note: beware the classloader fun.
|
||
* @param pluginClazz The class that your plugin implements.
|
||
* @return The plugin singleton or {@code null} if for some reason the plugin is not loaded.
|
||
* The fact the plugin is loaded does not mean it is enabled and fully initialized for the current Jenkins session.
|
||
* Use {@link Plugin#getWrapper()} and then {@link PluginWrapper#isActive()} to check it.
|
||
*/
|
||
@CheckForNull
|
||
public PluginWrapper getPlugin(Class<? extends Plugin> pluginClazz) {
|
||
for (PluginWrapper p : getPlugins()) {
|
||
if(pluginClazz.isInstance(p.getPlugin()))
|
||
return p;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Get the plugin instances that extend a specific class, use to find similar plugins.
|
||
* Note: beware the classloader fun.
|
||
* @param pluginSuperclass The class that your plugin is derived from.
|
||
* @return The list of plugins implementing the specified class.
|
||
*/
|
||
public List<PluginWrapper> getPlugins(Class<? extends Plugin> pluginSuperclass) {
|
||
List<PluginWrapper> result = new ArrayList<>();
|
||
for (PluginWrapper p : getPlugins()) {
|
||
if(pluginSuperclass.isInstance(p.getPlugin()))
|
||
result.add(p);
|
||
}
|
||
return Collections.unmodifiableList(result);
|
||
}
|
||
|
||
public String getDisplayName() {
|
||
return Messages.PluginManager_DisplayName();
|
||
}
|
||
|
||
public String getSearchUrl() {
|
||
return "pluginManager";
|
||
}
|
||
|
||
/**
|
||
* Discover all the service provider implementations of the given class,
|
||
* via {@code META-INF/services}.
|
||
* @deprecated Use {@link ServiceLoader} instead, or (more commonly) {@link ExtensionList}.
|
||
*/
|
||
@Deprecated
|
||
public <T> Collection<Class<? extends T>> discover( Class<T> spi ) {
|
||
Set<Class<? extends T>> result = new HashSet<>();
|
||
|
||
for (PluginWrapper p : activePlugins) {
|
||
Service.load(spi, p.classLoader, result);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Return the {@link PluginWrapper} that loaded the given class 'c'.
|
||
*
|
||
* @since 1.402.
|
||
*/
|
||
public PluginWrapper whichPlugin(Class c) {
|
||
PluginWrapper oneAndOnly = null;
|
||
ClassLoader cl = c.getClassLoader();
|
||
for (PluginWrapper p : activePlugins) {
|
||
if (p.classLoader==cl) {
|
||
if (oneAndOnly!=null)
|
||
return null; // ambiguous
|
||
oneAndOnly = p;
|
||
}
|
||
}
|
||
return oneAndOnly;
|
||
}
|
||
|
||
/**
|
||
* Orderly terminates all the plugins.
|
||
*/
|
||
public void stop() {
|
||
for (PluginWrapper p : activePlugins) {
|
||
p.stop();
|
||
p.releaseClassLoader();
|
||
}
|
||
activePlugins.clear();
|
||
// Work around a bug in commons-logging.
|
||
// See http://www.szegedi.org/articles/memleak.html
|
||
LogFactory.release(uberClassLoader);
|
||
}
|
||
|
||
/**
|
||
* Get the list of all plugins - available and installed.
|
||
* @return The list of all plugins - available and installed.
|
||
*/
|
||
@Restricted(DoNotUse.class) // WebOnly
|
||
public HttpResponse doPlugins() {
|
||
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
|
||
JSONArray response = new JSONArray();
|
||
Map<String,JSONObject> allPlugins = new HashMap<>();
|
||
for (PluginWrapper plugin : plugins) {
|
||
JSONObject pluginInfo = new JSONObject();
|
||
pluginInfo.put("installed", true);
|
||
pluginInfo.put("name", plugin.getShortName());
|
||
pluginInfo.put("title", plugin.getDisplayName());
|
||
pluginInfo.put("active", plugin.isActive());
|
||
pluginInfo.put("enabled", plugin.isEnabled());
|
||
pluginInfo.put("bundled", plugin.isBundled);
|
||
pluginInfo.put("deleted", plugin.isDeleted());
|
||
pluginInfo.put("downgradable", plugin.isDowngradable());
|
||
pluginInfo.put("website", plugin.getUrl());
|
||
List<Dependency> dependencies = plugin.getDependencies();
|
||
if (dependencies != null && !dependencies.isEmpty()) {
|
||
Map<String, String> dependencyMap = new HashMap<>();
|
||
for (Dependency dependency : dependencies) {
|
||
dependencyMap.put(dependency.shortName, dependency.version);
|
||
}
|
||
pluginInfo.put("dependencies", dependencyMap);
|
||
} else {
|
||
pluginInfo.put("dependencies", Collections.emptyMap());
|
||
}
|
||
response.add(pluginInfo);
|
||
}
|
||
for (UpdateSite site : Jenkins.get().getUpdateCenter().getSiteList()) {
|
||
for (UpdateSite.Plugin plugin: site.getAvailables()) {
|
||
JSONObject pluginInfo = allPlugins.get(plugin.name);
|
||
if(pluginInfo == null) {
|
||
pluginInfo = new JSONObject();
|
||
pluginInfo.put("installed", false);
|
||
}
|
||
pluginInfo.put("name", plugin.name);
|
||
pluginInfo.put("title", plugin.getDisplayName());
|
||
pluginInfo.put("excerpt", plugin.excerpt);
|
||
pluginInfo.put("site", site.getId());
|
||
pluginInfo.put("dependencies", plugin.dependencies);
|
||
pluginInfo.put("website", plugin.wiki);
|
||
response.add(pluginInfo);
|
||
}
|
||
}
|
||
return hudson.util.HttpResponses.okJSON(response);
|
||
}
|
||
|
||
@RequirePOST
|
||
public HttpResponse doUpdateSources(StaplerRequest req) throws IOException {
|
||
Jenkins.get().checkPermission(CONFIGURE_UPDATECENTER);
|
||
|
||
if (req.hasParameter("remove")) {
|
||
UpdateCenter uc = Jenkins.get().getUpdateCenter();
|
||
BulkChange bc = new BulkChange(uc);
|
||
try {
|
||
for (String id : req.getParameterValues("sources"))
|
||
uc.getSites().remove(uc.getById(id));
|
||
} finally {
|
||
bc.commit();
|
||
}
|
||
} else
|
||
if (req.hasParameter("add"))
|
||
return new HttpRedirect("addSite");
|
||
|
||
return new HttpRedirect("./sites");
|
||
}
|
||
|
||
/**
|
||
* Called to progress status beyond installing plugins, e.g. if
|
||
* there were failures that prevented installation from naturally proceeding
|
||
*/
|
||
@RequirePOST
|
||
@Restricted(DoNotUse.class) // WebOnly
|
||
public void doInstallPluginsDone() {
|
||
Jenkins j = Jenkins.get();
|
||
j.checkPermission(Jenkins.ADMINISTER);
|
||
InstallUtil.proceedToNextStateFrom(InstallState.INITIAL_PLUGINS_INSTALLING);
|
||
}
|
||
|
||
/**
|
||
* Performs the installation of the plugins.
|
||
*/
|
||
@RequirePOST
|
||
public void doInstall(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
|
||
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
|
||
Set<String> plugins = new LinkedHashSet<>();
|
||
|
||
Enumeration<String> en = req.getParameterNames();
|
||
while (en.hasMoreElements()) {
|
||
String n = en.nextElement();
|
||
if(n.startsWith("plugin.")) {
|
||
n = n.substring(7);
|
||
plugins.add(n);
|
||
}
|
||
}
|
||
|
||
boolean dynamicLoad = req.getParameter("dynamicLoad")!=null;
|
||
install(plugins, dynamicLoad);
|
||
|
||
rsp.sendRedirect("../updateCenter/");
|
||
}
|
||
|
||
/**
|
||
* Installs a list of plugins from a JSON POST.
|
||
* @param req The request object.
|
||
* @return A JSON response that includes a "correlationId" in the "data" element.
|
||
* That "correlationId" can then be used in calls to
|
||
* {@link UpdateCenter#doInstallStatus(org.kohsuke.stapler.StaplerRequest)}.
|
||
* @throws IOException Error reading JSON payload fro request.
|
||
*/
|
||
@RequirePOST
|
||
@Restricted(DoNotUse.class) // WebOnly
|
||
public HttpResponse doInstallPlugins(StaplerRequest req) throws IOException {
|
||
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
|
||
String payload = IOUtils.toString(req.getInputStream(), req.getCharacterEncoding());
|
||
JSONObject request = JSONObject.fromObject(payload);
|
||
JSONArray pluginListJSON = request.getJSONArray("plugins");
|
||
List<String> plugins = new ArrayList<>();
|
||
|
||
for (int i = 0; i < pluginListJSON.size(); i++) {
|
||
plugins.add(pluginListJSON.getString(i));
|
||
}
|
||
|
||
UUID correlationId = UUID.randomUUID();
|
||
try {
|
||
boolean dynamicLoad = request.getBoolean("dynamicLoad");
|
||
install(plugins, dynamicLoad, correlationId);
|
||
|
||
JSONObject responseData = new JSONObject();
|
||
responseData.put("correlationId", correlationId.toString());
|
||
|
||
return hudson.util.HttpResponses.okJSON(responseData);
|
||
} catch (Exception e) {
|
||
return hudson.util.HttpResponses.errorJSON(e.getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Performs the installation of the plugins.
|
||
* @param plugins The collection of plugins to install.
|
||
* @param dynamicLoad If true, the plugin will be dynamically loaded into this Jenkins. If false,
|
||
* the plugin will only take effect after the reboot.
|
||
* See {@link UpdateCenter#isRestartRequiredForCompletion()}
|
||
* @return The install job list.
|
||
* @since 2.0
|
||
*/
|
||
@Restricted(NoExternalUse.class)
|
||
public List<Future<UpdateCenter.UpdateCenterJob>> install(@Nonnull Collection<String> plugins, boolean dynamicLoad) {
|
||
return install(plugins, dynamicLoad, null);
|
||
}
|
||
|
||
private List<Future<UpdateCenter.UpdateCenterJob>> install(@Nonnull Collection<String> plugins, boolean dynamicLoad, @CheckForNull UUID correlationId) {
|
||
List<Future<UpdateCenter.UpdateCenterJob>> installJobs = new ArrayList<>();
|
||
|
||
LOGGER.log(INFO, "Starting installation of a batch of {0} plugins plus their dependencies", plugins.size());
|
||
long start = System.nanoTime();
|
||
List<PluginWrapper> batch = new ArrayList<>();
|
||
|
||
for (String n : plugins) {
|
||
// JENKINS-22080 plugin names can contain '.' as could (according to rumour) update sites
|
||
int index = n.indexOf('.');
|
||
UpdateSite.Plugin p = null;
|
||
|
||
if (index == -1) {
|
||
p = getPlugin(n, UpdateCenter.ID_DEFAULT);
|
||
} else {
|
||
while (index != -1) {
|
||
if (index + 1 >= n.length()) {
|
||
break;
|
||
}
|
||
String pluginName = n.substring(0, index);
|
||
String siteName = n.substring(index + 1);
|
||
UpdateSite.Plugin plugin = getPlugin(pluginName, siteName);
|
||
// There could be cases like:
|
||
// 'plugin.ambiguous.updatesite' where both
|
||
// 'plugin' @ 'ambiguous.updatesite' and 'plugin.ambiguous' @ 'updatesite' resolve to valid plugins
|
||
if (plugin != null) {
|
||
if (p != null) {
|
||
throw new Failure("Ambiguous plugin: " + n);
|
||
}
|
||
p = plugin;
|
||
}
|
||
index = n.indexOf('.', index + 1);
|
||
}
|
||
}
|
||
|
||
if (p == null) {
|
||
throw new Failure("No such plugin: " + n);
|
||
}
|
||
Future<UpdateCenter.UpdateCenterJob> jobFuture = p.deploy(dynamicLoad, correlationId, batch);
|
||
installJobs.add(jobFuture);
|
||
}
|
||
|
||
final Jenkins jenkins = Jenkins.get();
|
||
final UpdateCenter updateCenter = jenkins.getUpdateCenter();
|
||
|
||
if (dynamicLoad) {
|
||
installJobs.add(updateCenter.addJob(updateCenter.new CompleteBatchJob(batch, start, correlationId)));
|
||
}
|
||
|
||
final Authentication currentAuth = Jenkins.getAuthentication();
|
||
|
||
if (!jenkins.getInstallState().isSetupComplete()) {
|
||
jenkins.setInstallState(InstallState.INITIAL_PLUGINS_INSTALLING);
|
||
updateCenter.persistInstallStatus();
|
||
new Thread() {
|
||
@Override
|
||
public void run() {
|
||
boolean failures = false;
|
||
INSTALLING: while (true) {
|
||
try {
|
||
updateCenter.persistInstallStatus();
|
||
Thread.sleep(500);
|
||
failures = false;
|
||
for (Future<UpdateCenter.UpdateCenterJob> jobFuture : installJobs) {
|
||
if(!jobFuture.isDone() && !jobFuture.isCancelled()) {
|
||
continue INSTALLING;
|
||
}
|
||
UpdateCenter.UpdateCenterJob job = jobFuture.get();
|
||
if(job instanceof InstallationJob && ((InstallationJob)job).status instanceof DownloadJob.Failure) {
|
||
failures = true;
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
LOGGER.log(WARNING, "Unexpected error while waiting for initial plugin set to install.", e);
|
||
}
|
||
break;
|
||
}
|
||
updateCenter.persistInstallStatus();
|
||
if(!failures) {
|
||
try (ACLContext acl = ACL.as(currentAuth)) {
|
||
InstallUtil.proceedToNextStateFrom(InstallState.INITIAL_PLUGINS_INSTALLING);
|
||
}
|
||
}
|
||
}
|
||
}.start();
|
||
}
|
||
|
||
return installJobs;
|
||
}
|
||
|
||
private UpdateSite.Plugin getPlugin(String pluginName, String siteName) {
|
||
UpdateSite updateSite = Jenkins.get().getUpdateCenter().getById(siteName);
|
||
if (updateSite == null) {
|
||
throw new Failure("No such update center: " + siteName);
|
||
}
|
||
return updateSite.getPlugin(pluginName);
|
||
}
|
||
|
||
/**
|
||
* Bare-minimum configuration mechanism to change the update center.
|
||
*/
|
||
@RequirePOST
|
||
public HttpResponse doSiteConfigure(@QueryParameter String site) throws IOException {
|
||
Jenkins hudson = Jenkins.get();
|
||
hudson.checkPermission(CONFIGURE_UPDATECENTER);
|
||
UpdateCenter uc = hudson.getUpdateCenter();
|
||
PersistedList<UpdateSite> sites = uc.getSites();
|
||
for (UpdateSite s : sites) {
|
||
if (s.getId().equals(UpdateCenter.ID_DEFAULT))
|
||
sites.remove(s);
|
||
}
|
||
sites.add(new UpdateSite(UpdateCenter.ID_DEFAULT, site));
|
||
|
||
return new HttpRedirect("advanced");
|
||
}
|
||
|
||
@POST
|
||
public HttpResponse doProxyConfigure(StaplerRequest req) throws IOException, ServletException {
|
||
Jenkins jenkins = Jenkins.get();
|
||
jenkins.checkPermission(CONFIGURE_UPDATECENTER);
|
||
|
||
ProxyConfiguration pc = req.bindJSON(ProxyConfiguration.class, req.getSubmittedForm());
|
||
if (pc.name==null) {
|
||
jenkins.proxy = null;
|
||
ProxyConfiguration.getXmlFile().delete();
|
||
} else {
|
||
jenkins.proxy = pc;
|
||
jenkins.proxy.save();
|
||
}
|
||
return new HttpRedirect("advanced");
|
||
}
|
||
|
||
/**
|
||
* Uploads a plugin.
|
||
*/
|
||
@RequirePOST
|
||
public HttpResponse doUploadPlugin(StaplerRequest req) throws IOException, ServletException {
|
||
try {
|
||
Jenkins.get().checkPermission(UPLOAD_PLUGINS);
|
||
|
||
ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory());
|
||
|
||
// Parse the request
|
||
FileItem fileItem = upload.parseRequest(req).get(0);
|
||
String fileName = Util.getFileName(fileItem.getName());
|
||
if("".equals(fileName)){
|
||
return new HttpRedirect("advanced");
|
||
}
|
||
// we allow the upload of the new jpi's and the legacy hpi's
|
||
if(!fileName.endsWith(".jpi") && !fileName.endsWith(".hpi")){
|
||
throw new Failure(hudson.model.Messages.Hudson_NotAPlugin(fileName));
|
||
}
|
||
|
||
// first copy into a temporary file name
|
||
File t = File.createTempFile("uploaded", ".jpi");
|
||
t.deleteOnExit();
|
||
try {
|
||
fileItem.write(t);
|
||
} catch (Exception e) {
|
||
// Exception thrown is too generic so at least limit the scope where it can occur
|
||
throw new ServletException(e);
|
||
}
|
||
fileItem.delete();
|
||
|
||
final String baseName = identifyPluginShortName(t);
|
||
|
||
pluginUploaded = true;
|
||
|
||
JSONArray dependencies = new JSONArray();
|
||
try {
|
||
Manifest m;
|
||
try (JarFile jarFile = new JarFile(t)) {
|
||
m = jarFile.getManifest();
|
||
}
|
||
String deps = m.getMainAttributes().getValue("Plugin-Dependencies");
|
||
|
||
if (StringUtils.isNotBlank(deps)) {
|
||
// now we get to parse it!
|
||
String[] plugins = deps.split(",");
|
||
for (String p : plugins) {
|
||
// should have name:version[;resolution:=optional]
|
||
String[] attrs = p.split("[:;]");
|
||
dependencies.add(new JSONObject()
|
||
.element("name", attrs[0])
|
||
.element("version", attrs[1])
|
||
.element("optional", p.contains("resolution:=optional")));
|
||
}
|
||
}
|
||
} catch(IOException e) {
|
||
LOGGER.log(WARNING, "Unable to setup dependency list for plugin upload", e);
|
||
}
|
||
|
||
// Now create a dummy plugin that we can dynamically load (the InstallationJob will force a restart if one is needed):
|
||
JSONObject cfg = new JSONObject().
|
||
element("name", baseName).
|
||
element("version", "0"). // unused but mandatory
|
||
element("url", t.toURI().toString()).
|
||
element("dependencies", dependencies);
|
||
new UpdateSite(UpdateCenter.ID_UPLOAD, null).new Plugin(UpdateCenter.ID_UPLOAD, cfg).deploy(true);
|
||
return new HttpRedirect("../updateCenter");
|
||
} catch (FileUploadException e) {
|
||
throw new ServletException(e);
|
||
}
|
||
}
|
||
|
||
@Restricted(NoExternalUse.class)
|
||
@RequirePOST public HttpResponse doCheckUpdatesServer() throws IOException {
|
||
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
|
||
|
||
// We'll check the update servers with a try-retry mechanism. The retrier is built with a builder
|
||
Retrier<FormValidation> updateServerRetrier = new Retrier.Builder<>(
|
||
// the action to perform
|
||
this::checkUpdatesServer,
|
||
|
||
// the way we know whether this attempt was right or wrong
|
||
(currentAttempt, result) -> result.kind == FormValidation.Kind.OK,
|
||
|
||
// the action name we are trying to perform
|
||
"check updates server")
|
||
|
||
// the number of attempts to try
|
||
.withAttempts(CHECK_UPDATE_ATTEMPTS)
|
||
|
||
// the delay between attempts
|
||
.withDelay(CHECK_UPDATE_SLEEP_TIME_MILLIS)
|
||
|
||
// whatever exception raised is considered as a fail attempt (all exceptions), not a failure
|
||
.withDuringActionExceptions(new Class[] {Exception.class})
|
||
|
||
// what we do with a failed attempt due to an allowed exception, return an FormValidation.error with the message
|
||
.withDuringActionExceptionListener( (attempt, e) -> FormValidation.errorWithMarkup(e.getClass().getSimpleName() + ": " + e.getLocalizedMessage()))
|
||
|
||
// lets get our retrier object
|
||
.build();
|
||
|
||
try {
|
||
// Begin the process
|
||
FormValidation result = updateServerRetrier.start();
|
||
|
||
// Check how it went
|
||
if (!FormValidation.Kind.OK.equals(result.kind)) {
|
||
LOGGER.log(Level.SEVERE, Messages.PluginManager_UpdateSiteError(CHECK_UPDATE_ATTEMPTS, result.getMessage()));
|
||
if (CHECK_UPDATE_ATTEMPTS > 1 && !Logger.getLogger(Retrier.class.getName()).isLoggable(Level.WARNING)) {
|
||
LOGGER.log(Level.SEVERE, Messages.PluginManager_UpdateSiteChangeLogLevel(Retrier.class.getName()));
|
||
}
|
||
|
||
lastErrorCheckUpdateCenters = Messages.PluginManager_CheckUpdateServerError(result.getMessage());
|
||
} else {
|
||
lastErrorCheckUpdateCenters = null;
|
||
}
|
||
|
||
} catch (Exception e) {
|
||
// It's never going to be reached because we declared all Exceptions in the withDuringActionExceptions, so
|
||
// whatever exception is considered a expected failed attempt and the retries continue
|
||
LOGGER.log(Level.WARNING, Messages.PluginManager_UnexpectedException(), e);
|
||
|
||
// In order to leave this method as it was, rethrow as IOException
|
||
throw new IOException(e);
|
||
}
|
||
|
||
// Stay in the same page in any case
|
||
return HttpResponses.forwardToPreviousPage();
|
||
}
|
||
|
||
private FormValidation checkUpdatesServer() throws Exception {
|
||
for (UpdateSite site : Jenkins.get().getUpdateCenter().getSites()) {
|
||
FormValidation v = site.updateDirectlyNow(DownloadService.signatureCheck);
|
||
if (v.kind != FormValidation.Kind.OK) {
|
||
// Stop with an error
|
||
return v;
|
||
}
|
||
}
|
||
for (DownloadService.Downloadable d : DownloadService.Downloadable.all()) {
|
||
FormValidation v = d.updateNow();
|
||
if (v.kind != FormValidation.Kind.OK) {
|
||
// Stop with an error
|
||
return v;
|
||
}
|
||
}
|
||
return FormValidation.ok();
|
||
}
|
||
|
||
/**
|
||
* Returns the last error raised during the update sites checking.
|
||
* @return the last error message
|
||
*/
|
||
public String getLastErrorCheckUpdateCenters() {
|
||
return lastErrorCheckUpdateCenters;
|
||
}
|
||
|
||
protected String identifyPluginShortName(File t) {
|
||
try {
|
||
try (JarFile j = new JarFile(t)) {
|
||
String name = j.getManifest().getMainAttributes().getValue("Short-Name");
|
||
if (name != null) return name;
|
||
}
|
||
} catch (IOException e) {
|
||
LOGGER.log(WARNING, "Failed to identify the short name from "+t,e);
|
||
}
|
||
return FilenameUtils.getBaseName(t.getName()); // fall back to the base name of what's uploaded
|
||
}
|
||
|
||
public Descriptor<ProxyConfiguration> getProxyDescriptor() {
|
||
return Jenkins.get().getDescriptor(ProxyConfiguration.class);
|
||
}
|
||
|
||
/**
|
||
* Prepares plugins for some expected XML configuration.
|
||
* If the configuration (typically a job’s {@code config.xml})
|
||
* needs some plugins to be installed (or updated), those jobs
|
||
* will be triggered.
|
||
* Plugins are dynamically loaded whenever possible.
|
||
* Requires {@link Jenkins#ADMINISTER}.
|
||
* @param configXml configuration that might be uploaded
|
||
* @return an empty list if all is well, else a list of submitted jobs which must be completed before this configuration can be fully read
|
||
* @throws IOException if loading or parsing the configuration failed
|
||
* @see ItemGroupMixIn#createProjectFromXML
|
||
* @see AbstractItem#updateByXml(javax.xml.transform.Source)
|
||
* @see XStream2
|
||
* @see hudson.model.UpdateSite.Plugin#deploy(boolean)
|
||
* @see PluginWrapper#supportsDynamicLoad
|
||
* @see hudson.model.UpdateCenter.DownloadJob.SuccessButRequiresRestart
|
||
* @since 1.483
|
||
*/
|
||
public List<Future<UpdateCenter.UpdateCenterJob>> prevalidateConfig(InputStream configXml) throws IOException {
|
||
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
|
||
List<Future<UpdateCenter.UpdateCenterJob>> jobs = new ArrayList<>();
|
||
UpdateCenter uc = Jenkins.get().getUpdateCenter();
|
||
// TODO call uc.updateAllSites() when available? perhaps not, since we should not block on network here
|
||
for (Map.Entry<String,VersionNumber> requestedPlugin : parseRequestedPlugins(configXml).entrySet()) {
|
||
PluginWrapper pw = getPlugin(requestedPlugin.getKey());
|
||
if (pw == null) { // install new
|
||
UpdateSite.Plugin toInstall = uc.getPlugin(requestedPlugin.getKey(), requestedPlugin.getValue());
|
||
if (toInstall == null) {
|
||
LOGGER.log(WARNING, "No such plugin {0} to install", requestedPlugin.getKey());
|
||
continue;
|
||
}
|
||
logPluginWarnings(requestedPlugin, toInstall);
|
||
jobs.add(toInstall.deploy(true));
|
||
} else if (pw.isOlderThan(requestedPlugin.getValue())) { // upgrade
|
||
UpdateSite.Plugin toInstall = uc.getPlugin(requestedPlugin.getKey(), requestedPlugin.getValue());
|
||
if (toInstall == null) {
|
||
LOGGER.log(WARNING, "No such plugin {0} to upgrade", requestedPlugin.getKey());
|
||
continue;
|
||
}
|
||
if (!pw.isOlderThan(new VersionNumber(toInstall.version))) {
|
||
LOGGER.log(WARNING, "{0}@{1} is no newer than what we already have", new Object[] {toInstall.name, toInstall.version});
|
||
continue;
|
||
}
|
||
logPluginWarnings(requestedPlugin, toInstall);
|
||
if (!toInstall.isCompatibleWithInstalledVersion()) {
|
||
LOGGER.log(WARNING, "{0}@{1} is incompatible with the installed @{2}", new Object[] {toInstall.name, toInstall.version, pw.getVersion()});
|
||
}
|
||
jobs.add(toInstall.deploy(true)); // dynamicLoad=true => sure to throw RestartRequiredException, but at least message is nicer
|
||
} // else already good
|
||
}
|
||
return jobs;
|
||
}
|
||
|
||
private void logPluginWarnings(Map.Entry<String, VersionNumber> requestedPlugin, UpdateSite.Plugin toInstall) {
|
||
if (new VersionNumber(toInstall.version).compareTo(requestedPlugin.getValue()) < 0) {
|
||
LOGGER.log(WARNING, "{0} can only be satisfied in @{1}", new Object[] {requestedPlugin, toInstall.version});
|
||
}
|
||
if (toInstall.isForNewerHudson()) {
|
||
LOGGER.log(WARNING, "{0}@{1} was built for a newer Jenkins", new Object[] {toInstall.name, toInstall.version});
|
||
}
|
||
if (toInstall.isForNewerJava()) {
|
||
LOGGER.log(WARNING, "{0}@{1} was built for a newer Java", new Object[] {toInstall.name, toInstall.version});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Like {@link #doInstallNecessaryPlugins(StaplerRequest)} but only checks if everything is installed
|
||
* or if some plugins need updates or installation.
|
||
*
|
||
* This method runs without side-effect. I'm still requiring the ADMINISTER permission since
|
||
* XML file can contain various external references and we don't configure parsers properly against
|
||
* that.
|
||
*
|
||
* @since 1.483
|
||
*/
|
||
@RequirePOST
|
||
public JSONArray doPrevalidateConfig(StaplerRequest req) throws IOException {
|
||
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
|
||
|
||
JSONArray response = new JSONArray();
|
||
|
||
for (Map.Entry<String,VersionNumber> p : parseRequestedPlugins(req.getInputStream()).entrySet()) {
|
||
PluginWrapper pw = getPlugin(p.getKey());
|
||
JSONObject j = new JSONObject()
|
||
.accumulate("name", p.getKey())
|
||
.accumulate("version", p.getValue().toString());
|
||
if (pw == null) { // install new
|
||
response.add(j.accumulate("mode", "missing"));
|
||
} else if (pw.isOlderThan(p.getValue())) { // upgrade
|
||
response.add(j.accumulate("mode", "old"));
|
||
} // else already good
|
||
}
|
||
|
||
return response;
|
||
}
|
||
|
||
/**
|
||
* Runs {@link #prevalidateConfig} on posted XML and redirects to the {@link UpdateCenter}.
|
||
* @since 1.483
|
||
*/
|
||
@RequirePOST
|
||
public HttpResponse doInstallNecessaryPlugins(StaplerRequest req) throws IOException {
|
||
prevalidateConfig(req.getInputStream());
|
||
return HttpResponses.redirectViaContextPath("updateCenter");
|
||
}
|
||
|
||
/**
|
||
* Parses configuration XML files and picks up references to XML files.
|
||
*/
|
||
public Map<String,VersionNumber> parseRequestedPlugins(InputStream configXml) throws IOException {
|
||
final Map<String,VersionNumber> requestedPlugins = new TreeMap<>();
|
||
try {
|
||
SAXParserFactory.newInstance().newSAXParser().parse(configXml, new DefaultHandler() {
|
||
@Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
|
||
String plugin = attributes.getValue("plugin");
|
||
if (plugin == null) {
|
||
return;
|
||
}
|
||
if (!plugin.matches("[^@]+@[^@]+")) {
|
||
throw new SAXException("Malformed plugin attribute: " + plugin);
|
||
}
|
||
int at = plugin.indexOf('@');
|
||
String shortName = plugin.substring(0, at);
|
||
VersionNumber existing = requestedPlugins.get(shortName);
|
||
VersionNumber requested = new VersionNumber(plugin.substring(at + 1));
|
||
if (existing == null || existing.compareTo(requested) < 0) {
|
||
requestedPlugins.put(shortName, requested);
|
||
}
|
||
}
|
||
|
||
@Override public InputSource resolveEntity(String publicId, String systemId) throws IOException,
|
||
SAXException {
|
||
return RestrictiveEntityResolver.INSTANCE.resolveEntity(publicId, systemId);
|
||
}
|
||
|
||
});
|
||
} catch (SAXException x) {
|
||
throw new IOException("Failed to parse XML",x);
|
||
} catch (ParserConfigurationException e) {
|
||
throw new AssertionError(e); // impossible since we don't tweak XMLParser
|
||
}
|
||
return requestedPlugins;
|
||
}
|
||
|
||
@Restricted(DoNotUse.class) // table.jelly
|
||
public MetadataCache createCache() {
|
||
return new MetadataCache();
|
||
}
|
||
|
||
/**
|
||
* Disable a list of plugins using a strategy for their dependents plugins.
|
||
* @param strategy the strategy regarding how the dependent plugins are processed
|
||
* @param plugins the list of plugins
|
||
* @return the list of results for every plugin and their dependent plugins.
|
||
* @throws IOException see {@link PluginWrapper#disable()}
|
||
*/
|
||
public @Nonnull List<PluginWrapper.PluginDisableResult> disablePlugins(@Nonnull PluginWrapper.PluginDisableStrategy strategy, @Nonnull List<String> plugins) throws IOException {
|
||
// Where we store the results of each plugin disablement
|
||
List<PluginWrapper.PluginDisableResult> 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<String, Object> data = new HashMap<>();
|
||
public <T> T of(String key, Class<T> type, Supplier<T> func) {
|
||
return type.cast(data.computeIfAbsent(key, _ignored -> func.get()));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* {@link ClassLoader} that can see all plugins.
|
||
*/
|
||
public final class UberClassLoader extends ClassLoader {
|
||
/**
|
||
* Make generated types visible.
|
||
* Keyed by the generated class name.
|
||
*/
|
||
private ConcurrentMap<String, WeakReference<Class>> generatedClasses = new ConcurrentHashMap<>();
|
||
/** Cache of loaded, or known to be unloadable, classes. */
|
||
private final Map<String,Class<?>> loaded = new HashMap<>();
|
||
|
||
public UberClassLoader() {
|
||
super(PluginManager.class.getClassLoader());
|
||
}
|
||
|
||
public void addNamedClass(String className, Class c) {
|
||
generatedClasses.put(className, new WeakReference<>(c));
|
||
}
|
||
|
||
@Override
|
||
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
||
WeakReference<Class> wc = generatedClasses.get(name);
|
||
if (wc!=null) {
|
||
Class c = wc.get();
|
||
if (c!=null) return c;
|
||
else generatedClasses.remove(name,wc);
|
||
}
|
||
|
||
if (name.startsWith("SimpleTemplateScript")) { // cf. groovy.text.SimpleTemplateEngine
|
||
throw new ClassNotFoundException("ignoring " + name);
|
||
}
|
||
synchronized (loaded) {
|
||
if (loaded.containsKey(name)) {
|
||
Class<?> c = loaded.get(name);
|
||
if (c != null) {
|
||
return c;
|
||
} else {
|
||
throw new ClassNotFoundException("cached miss for " + name);
|
||
}
|
||
}
|
||
}
|
||
if (FAST_LOOKUP) {
|
||
for (PluginWrapper p : activePlugins) {
|
||
try {
|
||
Class<?> c = ClassLoaderReflectionToolkit._findLoadedClass(p.classLoader, name);
|
||
if (c != null) {
|
||
synchronized (loaded) {
|
||
loaded.put(name, c);
|
||
}
|
||
return c;
|
||
}
|
||
// calling findClass twice appears to cause LinkageError: duplicate class def
|
||
c = ClassLoaderReflectionToolkit._findClass(p.classLoader, name);
|
||
synchronized (loaded) {
|
||
loaded.put(name, c);
|
||
}
|
||
return c;
|
||
} catch (ClassNotFoundException e) {
|
||
//not found. try next
|
||
}
|
||
}
|
||
} else {
|
||
for (PluginWrapper p : activePlugins) {
|
||
try {
|
||
return p.classLoader.loadClass(name);
|
||
} catch (ClassNotFoundException e) {
|
||
//not found. try next
|
||
}
|
||
}
|
||
}
|
||
synchronized (loaded) {
|
||
loaded.put(name, null);
|
||
}
|
||
// not found in any of the classloader. delegate.
|
||
ClassNotFoundException cnfe = new ClassNotFoundException(name);
|
||
MissingClassTelemetry.reportException(name, cnfe);
|
||
throw cnfe;
|
||
}
|
||
|
||
@Override
|
||
protected URL findResource(String name) {
|
||
if (FAST_LOOKUP) {
|
||
for (PluginWrapper p : activePlugins) {
|
||
URL url = ClassLoaderReflectionToolkit._findResource(p.classLoader, name);
|
||
if(url!=null)
|
||
return url;
|
||
}
|
||
} else {
|
||
for (PluginWrapper p : activePlugins) {
|
||
URL url = p.classLoader.getResource(name);
|
||
if(url!=null)
|
||
return url;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
@Override
|
||
protected Enumeration<URL> findResources(String name) throws IOException {
|
||
List<URL> resources = new ArrayList<>();
|
||
if (FAST_LOOKUP) {
|
||
for (PluginWrapper p : activePlugins) {
|
||
resources.addAll(Collections.list(ClassLoaderReflectionToolkit._findResources(p.classLoader, name)));
|
||
}
|
||
} else {
|
||
for (PluginWrapper p : activePlugins) {
|
||
resources.addAll(Collections.list(p.classLoader.getResources(name)));
|
||
}
|
||
}
|
||
return Collections.enumeration(resources);
|
||
}
|
||
|
||
@Override
|
||
public String toString() {
|
||
// only for debugging purpose
|
||
return "classLoader " + getClass().getName();
|
||
}
|
||
}
|
||
public static boolean FAST_LOOKUP = !SystemProperties.getBoolean(PluginManager.class.getName()+".noFastLookup");
|
||
|
||
public static final Permission UPLOAD_PLUGINS = new Permission(Jenkins.PERMISSIONS, "UploadPlugins", Messages._PluginManager_UploadPluginsPermission_Description(),Jenkins.ADMINISTER,PermissionScope.JENKINS);
|
||
public static final Permission CONFIGURE_UPDATECENTER = new Permission(Jenkins.PERMISSIONS, "ConfigureUpdateCenter", Messages._PluginManager_ConfigureUpdateCenterPermission_Description(),Jenkins.ADMINISTER,PermissionScope.JENKINS);
|
||
|
||
/**
|
||
* Remembers why a plugin failed to deploy.
|
||
*/
|
||
public static final class FailedPlugin {
|
||
public final String name;
|
||
public final Exception cause;
|
||
|
||
public FailedPlugin(String name, Exception cause) {
|
||
this.name = name;
|
||
this.cause = cause;
|
||
}
|
||
|
||
public String getExceptionString() {
|
||
return Functions.printThrowable(cause);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Stores {@link Plugin} instances.
|
||
*/
|
||
/*package*/ static final class PluginInstanceStore {
|
||
final Map<PluginWrapper,Plugin> store = new ConcurrentHashMap<>();
|
||
}
|
||
|
||
/**
|
||
* {@link AdministrativeMonitor} that checks if there are any plugins with cycle dependencies.
|
||
*/
|
||
@Extension @Symbol("pluginCycleDependencies")
|
||
public static final class PluginCycleDependenciesMonitor extends AdministrativeMonitor {
|
||
|
||
@Override
|
||
public String getDisplayName() {
|
||
return Messages.PluginManager_PluginCycleDependenciesMonitor_DisplayName();
|
||
}
|
||
|
||
private transient volatile boolean isActive = false;
|
||
|
||
private transient volatile List<PluginWrapper> pluginsWithCycle;
|
||
|
||
public boolean isActivated() {
|
||
if(pluginsWithCycle == null){
|
||
pluginsWithCycle = new ArrayList<>();
|
||
for (PluginWrapper p : Jenkins.get().getPluginManager().getPlugins()) {
|
||
if(p.hasCycleDependency()){
|
||
pluginsWithCycle.add(p);
|
||
isActive = true;
|
||
}
|
||
}
|
||
}
|
||
return isActive;
|
||
}
|
||
|
||
public List<PluginWrapper> getPluginsWithCycle() {
|
||
return pluginsWithCycle;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* {@link AdministrativeMonitor} that informs the administrator about a required plugin update.
|
||
* @since 1.491
|
||
*/
|
||
@Extension @Symbol("pluginUpdate")
|
||
public static final class PluginUpdateMonitor extends AdministrativeMonitor {
|
||
|
||
private Map<String, PluginUpdateInfo> pluginsToBeUpdated = new HashMap<>();
|
||
|
||
/**
|
||
* Convenience method to ease access to this monitor, this allows other plugins to register required updates.
|
||
* @return this monitor.
|
||
*/
|
||
public static PluginUpdateMonitor getInstance() {
|
||
return ExtensionList.lookupSingleton(PluginUpdateMonitor.class);
|
||
}
|
||
|
||
/**
|
||
* Report to the administrator if the plugin with the given name is older then the required version.
|
||
*
|
||
* @param pluginName shortName of the plugin (artifactId)
|
||
* @param requiredVersion the lowest version which is OK (e.g. 2.2.2)
|
||
* @param message the message to show (plain text)
|
||
*/
|
||
public void ifPluginOlderThenReport(String pluginName, String requiredVersion, String message){
|
||
Plugin plugin = Jenkins.get().getPlugin(pluginName);
|
||
if(plugin != null){
|
||
if(plugin.getWrapper().getVersionNumber().isOlderThan(new VersionNumber(requiredVersion))) {
|
||
pluginsToBeUpdated.put(pluginName, new PluginUpdateInfo(pluginName, message));
|
||
}
|
||
}
|
||
}
|
||
|
||
public boolean isActivated() {
|
||
return !pluginsToBeUpdated.isEmpty();
|
||
}
|
||
|
||
@Override
|
||
public String getDisplayName() {
|
||
return Messages.PluginManager_PluginUpdateMonitor_DisplayName();
|
||
}
|
||
|
||
/**
|
||
* adds a message about a plugin to the manage screen
|
||
* @param pluginName the plugins name
|
||
* @param message the message to be displayed
|
||
*/
|
||
public void addPluginToUpdate(String pluginName, String message) {
|
||
this.pluginsToBeUpdated.put(pluginName, new PluginUpdateInfo(pluginName, message));
|
||
}
|
||
|
||
public Collection<PluginUpdateInfo> getPluginsToBeUpdated() {
|
||
return pluginsToBeUpdated.values();
|
||
}
|
||
|
||
public static class PluginUpdateInfo {
|
||
public final String pluginName;
|
||
public final String message;
|
||
private PluginUpdateInfo(String pluginName, String message) {
|
||
this.pluginName = pluginName;
|
||
this.message = message;
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
@Override
|
||
@Restricted(NoExternalUse.class)
|
||
public Object getTarget() {
|
||
if (!SKIP_PERMISSION_CHECK) {
|
||
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
|
||
}
|
||
return this;
|
||
}
|
||
|
||
/**
|
||
* Escape hatch for StaplerProxy-based access control
|
||
*/
|
||
@Restricted(NoExternalUse.class)
|
||
public static /* Script Console modifiable */ boolean SKIP_PERMISSION_CHECK = Boolean.getBoolean(PluginManager.class.getName() + ".skipPermissionCheck");
|
||
}
|