提交 2dc52c15 编写于 作者: K Kohsuke Kawaguchi

Merge branch 'add-plugin-without-restart'

......@@ -26,6 +26,7 @@ package hudson;
import hudson.model.Descriptor;
import hudson.model.Describable;
import hudson.model.Hudson;
import jenkins.ExtensionComponentSet;
import jenkins.model.Jenkins;
import hudson.model.ViewDescriptor;
import hudson.model.Descriptor.FormException;
......@@ -36,6 +37,7 @@ import hudson.slaves.NodeDescriptor;
import hudson.tasks.Publisher;
import hudson.tasks.Publisher.DescriptorExtensionListImpl;
import java.util.Collection;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
......@@ -177,8 +179,17 @@ public class DescriptorExtensionList<T extends Describable<T>, D extends Descrip
*/
@Override
protected List<ExtensionComponent<D>> load() {
return _load(jenkins.getExtensionList(Descriptor.class).getComponents());
}
@Override
protected Collection<ExtensionComponent<D>> load(ExtensionComponentSet delta) {
return _load(delta.find(Descriptor.class));
}
private List<ExtensionComponent<D>> _load(Iterable<ExtensionComponent<Descriptor>> set) {
List<ExtensionComponent<D>> r = new ArrayList<ExtensionComponent<D>>();
for( ExtensionComponent<Descriptor> c : hudson.getExtensionList(Descriptor.class).getComponents() ) {
for( ExtensionComponent<Descriptor> c : set ) {
Descriptor d = c.getInstance();
try {
if(d.getT()==describableType)
......
......@@ -23,6 +23,7 @@
*/
package hudson;
import jenkins.YesNoMaybe;
import net.java.sezpoz.Indexable;
import java.lang.annotation.Documented;
......@@ -31,6 +32,7 @@ import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static jenkins.YesNoMaybe.MAYBE;
/**
* Marks a field, a method, or a class for automatic discovery, so that Hudson can locate
......@@ -84,4 +86,26 @@ public @interface Extension {
* @since 1.358
*/
boolean optional() default false;
/**
* Marks whether this extension works with dynamic loading of a plugin.
*
* <p>
* "Yes" indicates an explicit sign-off from the developer indicating this component supports that.
* Similarly, "No" indicates that this extension is known not to support it, which forces Jenkins
* not to offer dynamic loading as an option.
*
* <p>
* The "MayBe" value indicates that there's no such explicit sign-off. So the dynamic loading may or may not
* work.
*
* <p>
* If your plugin contains any extension that has dynamic loadability set to NO, then Jenkins
* will prompt the user to restart Jenkins to have the plugin take effect. If every component
* is marked as YES, then Jenkins will simply dynamic load the plugin without asking the user.
* Otherwise, Jenkins will ask the user if he wants to restart, or go ahead and dynamically deploy.
*
* @since 1.442
*/
YesNoMaybe dynamicLoadable() default MAYBE;
}
......@@ -23,9 +23,9 @@
*/
package hudson;
import com.google.common.collect.Lists;
import com.google.inject.AbstractModule;
import com.google.inject.Binding;
import com.google.inject.CreationException;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
......@@ -38,6 +38,9 @@ import com.google.common.collect.ImmutableList;
import hudson.init.InitMilestone;
import hudson.model.Descriptor;
import hudson.model.Hudson;
import jenkins.ExtensionComponentSet;
import jenkins.ExtensionRefreshException;
import jenkins.ProxyInjector;
import jenkins.model.Jenkins;
import net.java.sezpoz.Index;
import net.java.sezpoz.IndexItem;
......@@ -82,6 +85,35 @@ public abstract class ExtensionFinder implements ExtensionPoint {
return Collections.emptyList();
}
/**
* Returns true if this extension finder supports the {@link #refresh()} operation.
*/
public boolean isRefreshable() {
try {
return getClass().getMethod("refresh").getDeclaringClass()!=ExtensionFinder.class;
} catch (NoSuchMethodException e) {
return false;
}
}
/**
* Rebuilds the internal index, if any, so that future {@link #find(Class, Hudson)} calls
* will discover components newly added to {@link PluginManager#uberClassLoader}.
*
* <p>
* The point of the refresh operation is not to disrupt instances of already loaded {@link ExtensionComponent}s,
* and only instantiate those that are new. Otherwise this will break the singleton semantics of various
* objects, such as {@link Descriptor}s.
*
* <p>
* The behaviour is undefined if {@link #isRefreshable()} is returning false.
*
* @since 1.442
* @see #isRefreshable()
* @return never null
*/
public abstract ExtensionComponentSet refresh() throws ExtensionRefreshException;
/**
* Discover extensions of the given type.
*
......@@ -89,18 +121,23 @@ public abstract class ExtensionFinder implements ExtensionPoint {
* This method is called only once per the given type after all the plugins are loaded,
* so implementations need not worry about caching.
*
* <p>
* This method should return all the known components at the time of the call, including
* those that are discovered later via {@link #refresh()}, even though those components
* are separately retruend in {@link ExtensionComponentSet}.
*
* @param <T>
* The type of the extension points. This is not bound to {@link ExtensionPoint} because
* of {@link Descriptor}, which by itself doesn't implement {@link ExtensionPoint} for
* a historical reason.
* @param hudson
* Hudson whose behalf this extension finder is performing lookup.
* @param jenkins
* Jenkins whose behalf this extension finder is performing lookup.
* @return
* Can be empty but never null.
* @since 1.356
* Older implementations provide {@link #findExtensions(Class,Hudson)}
*/
public abstract <T> Collection<ExtensionComponent<T>> find(Class<T> type, Hudson hudson);
public abstract <T> Collection<ExtensionComponent<T>> find(Class<T> type, Hudson jenkins);
/**
* A pointless function to work around what appears to be a HotSpot problem. See JENKINS-5756 and bug 6933067
......@@ -147,6 +184,13 @@ public abstract class ExtensionFinder implements ExtensionPoint {
public static final class GuiceFinder extends AbstractGuiceFinder<Extension> {
public GuiceFinder() {
super(Extension.class);
// expose Injector via lookup mechanism for interop with non-Guice clients
Jenkins.getInstance().lookup.set(Injector.class,new ProxyInjector() {
protected Injector resolve() {
return getContainer();
}
});
}
@Override
......@@ -164,15 +208,34 @@ public abstract class ExtensionFinder implements ExtensionPoint {
* Discovers components via sezpoz but instantiates them by using Guice.
*/
public static abstract class AbstractGuiceFinder<T extends Annotation> extends ExtensionFinder {
private Injector container;
/**
* Injector that we find components from.
* <p>
* To support refresh when Guice doesn't let us alter the bindings, we'll create
* a child container to house newly discovered components. This field points to the
* youngest such container.
*/
private volatile Injector container;
/**
* Sezpoz index we are currently using in {@link #container} (and its ancestors.)
* Needed to compute delta.
*/
private List<IndexItem<T,Object>> sezpozIndex;
private final Map<Key,T> annotations = new HashMap<Key,T>();
private final Sezpoz moduleFinder = new Sezpoz();
private final Class<T> annotationType;
public AbstractGuiceFinder(final Class<T> annotationType) {
this.annotationType = annotationType;
sezpozIndex = ImmutableList.copyOf(Index.load(annotationType, Object.class, Jenkins.getInstance().getPluginManager().uberClassLoader));
List<Module> modules = new ArrayList<Module>();
modules.add(new SezpozModule(annotationType,Jenkins.getInstance().getPluginManager().uberClassLoader));
modules.add(new SezpozModule(sezpozIndex));
for (ExtensionComponent<Module> ec : new Sezpoz().find(Module.class, Hudson.getInstance())) {
for (ExtensionComponent<Module> ec : moduleFinder.find(Module.class, Hudson.getInstance())) {
modules.add(ec.getInstance());
}
......@@ -182,7 +245,52 @@ public abstract class ExtensionFinder implements ExtensionPoint {
LOGGER.log(Level.SEVERE, "Failed to create Guice container from all the plugins",e);
// failing to load all bindings are disastrous, so recover by creating minimum that works
// by just including the core
container = Guice.createInjector(new SezpozModule(annotationType,Jenkins.class.getClassLoader()));
container = Guice.createInjector(new SezpozModule(
ImmutableList.copyOf(Index.load(annotationType, Object.class, Jenkins.class.getClassLoader()))));
}
}
public Injector getContainer() {
return container;
}
/**
* The basic idea is:
*
* <ul>
* <li>List up delta as a series of modules
* <li>
* </ul>
*/
@Override
public synchronized ExtensionComponentSet refresh() throws ExtensionRefreshException {
// figure out newly discovered sezpoz components
List<IndexItem<T, Object>> delta = Sezpoz.listDelta(annotationType,sezpozIndex);
List<IndexItem<T, Object>> l = Lists.newArrayList(sezpozIndex);
l.addAll(delta);
sezpozIndex = l;
List<Module> modules = new ArrayList<Module>();
modules.add(new SezpozModule(delta));
for (ExtensionComponent<Module> ec : moduleFinder.refresh().find(Module.class)) {
modules.add(ec.getInstance());
}
try {
final Injector child = container.createChildInjector(modules);
container = child;
return new ExtensionComponentSet() {
@Override
public <T> Collection<ExtensionComponent<T>> find(Class<T> type) {
List<ExtensionComponent<T>> result = new ArrayList<ExtensionComponent<T>>();
_find(type, result, child);
return result;
}
};
} catch (Throwable e) {
LOGGER.log(Level.SEVERE, "Failed to create Guice container from newly added plugins",e);
throw new ExtensionRefreshException(e);
}
}
......@@ -212,9 +320,16 @@ public abstract class ExtensionFinder implements ExtensionPoint {
return null;
}
public <U> Collection<ExtensionComponent<U>> find(Class<U> type, Hudson hudson) {
public <U> Collection<ExtensionComponent<U>> find(Class<U> type, Hudson jenkins) {
// the find method contract requires us to traverse all known components
List<ExtensionComponent<U>> result = new ArrayList<ExtensionComponent<U>>();
for (Injector i=container; i!=null; i=i.getParent()) {
_find(type, result, i);
}
return result;
}
private <U> void _find(Class<U> type, List<ExtensionComponent<U>> result, Injector container) {
for (Entry<Key<?>, Binding<?>> e : container.getBindings().entrySet()) {
if (type.isAssignableFrom(e.getKey().getTypeLiteral().getRawType())) {
T a = annotations.get(e.getKey());
......@@ -223,8 +338,6 @@ public abstract class ExtensionFinder implements ExtensionPoint {
result.add(new ExtensionComponent<U>(type.cast(o),a!=null?getOrdinal(a):0));
}
}
return result;
}
/**
......@@ -261,13 +374,16 @@ public abstract class ExtensionFinder implements ExtensionPoint {
private static final Logger LOGGER = Logger.getLogger(GuiceFinder.class.getName());
/**
* {@link Module} that finds components via sezpoz index.
* Instead of using SezPoz to instantiate, we'll instantiate them by using Guice,
* so that we can take advantage of dependency injection.
*/
private class SezpozModule extends AbstractModule {
private final Class<T> annotationType;
private final ClassLoader cl;
private final List<IndexItem<T,Object>> index;;
public SezpozModule(Class<T> annotationType, ClassLoader cl) {
this.annotationType = annotationType;
this.cl = cl;
public SezpozModule(List<IndexItem<T,Object>> index) {
this.index = index;
}
/**
......@@ -302,7 +418,7 @@ public abstract class ExtensionFinder implements ExtensionPoint {
protected void configure() {
int id=0;
for (final IndexItem<T,Object> item : Index.load(annotationType, Object.class, cl)) {
for (final IndexItem<T,Object> item : index) {
id++;
try {
AnnotatedElement e = item.element();
......@@ -379,10 +495,54 @@ public abstract class ExtensionFinder implements ExtensionPoint {
return indices;
}
public <T> Collection<ExtensionComponent<T>> find(Class<T> type, Hudson hudson) {
/**
* {@inheritDoc}
*
* <p>
* SezPoz implements value-equality of {@link IndexItem}, so
*/
@Override
public synchronized ExtensionComponentSet refresh() {
final List<IndexItem<Extension,Object>> old = indices;
if (old==null) return ExtensionComponentSet.EMPTY; // we haven't loaded anything
final List<IndexItem<Extension, Object>> delta = listDelta(Extension.class,old);
List<IndexItem<Extension,Object>> r = Lists.newArrayList(old);
r.addAll(delta);
indices = ImmutableList.copyOf(r);
return new ExtensionComponentSet() {
@Override
public <T> Collection<ExtensionComponent<T>> find(Class<T> type) {
return _find(type,delta);
}
};
}
static <T extends Annotation> List<IndexItem<T, Object>> listDelta(Class<T> annotationType, List<IndexItem<T, Object>> old) {
// list up newly discovered components
final List<IndexItem<T,Object>> delta = Lists.newArrayList();
ClassLoader cl = Jenkins.getInstance().getPluginManager().uberClassLoader;
for (IndexItem<T,Object> ii : Index.load(annotationType, Object.class, cl)) {
if (!old.contains(ii)) {
delta.add(ii);
}
}
return delta;
}
public <T> Collection<ExtensionComponent<T>> find(Class<T> type, Hudson jenkins) {
return _find(type,getIndices());
}
/**
* Finds all the matching {@link IndexItem}s that match the given type and instantiate them.
*/
private <T> Collection<ExtensionComponent<T>> _find(Class<T> type, List<IndexItem<Extension,Object>> indices) {
List<ExtensionComponent<T>> result = new ArrayList<ExtensionComponent<T>>();
for (IndexItem<Extension,Object> item : getIndices()) {
for (IndexItem<Extension,Object> item : indices) {
try {
AnnotatedElement e = item.element();
Class<?> extType;
......
......@@ -23,8 +23,10 @@
*/
package hudson;
import com.google.common.collect.Lists;
import hudson.init.InitMilestone;
import hudson.model.Hudson;
import jenkins.ExtensionComponentSet;
import jenkins.model.Jenkins;
import hudson.util.AdaptedIterator;
import hudson.util.DescriptorList;
......@@ -241,6 +243,24 @@ public class ExtensionList<T> extends AbstractList<T> {
return jenkins.lookup.setIfNull(Lock.class,new Lock());
}
/**
* Used during {@link Jenkins#refreshExtensions()} to add new components into existing {@link ExtensionList}s.
* Do not call from anywhere else.
*/
public void refresh(ExtensionComponentSet delta) {
synchronized (getLoadLock()) {
if (extensions==null)
return; // not yet loaded. when we load it, we'll load everything visible by then, so no work needed
Collection<ExtensionComponent<T>> found = load(delta);
if (!found.isEmpty()) {
List<ExtensionComponent<T>> l = Lists.newArrayList(extensions);
l.addAll(found);
extensions = sort(l);
}
}
}
/**
* Loading an {@link ExtensionList} can result in a nested loading of another {@link ExtensionList}.
* What that means is that we need a single lock that spans across all the {@link ExtensionList}s,
......@@ -258,6 +278,14 @@ public class ExtensionList<T> extends AbstractList<T> {
return jenkins.getPluginManager().getPluginStrategy().findComponents(extensionType, hudson);
}
/**
* Picks up extensions that we care from the given list.
*/
protected Collection<ExtensionComponent<T>> load(ExtensionComponentSet delta) {
return delta.find(extensionType);
}
/**
* If the {@link ExtensionList} implementation requires sorting extensions,
* override this method to do so.
......
......@@ -23,26 +23,25 @@
*/
package hudson;
import static hudson.init.InitMilestone.PLUGINS_PREPARED;
import static hudson.init.InitMilestone.PLUGINS_STARTED;
import static hudson.init.InitMilestone.PLUGINS_LISTED;
import com.google.inject.Guice;
import com.google.inject.Injector;
import hudson.PluginWrapper.Dependency;
import hudson.init.InitMilestone;
import hudson.init.InitStrategy;
import hudson.init.InitializerFinder;
import hudson.model.AbstractModelObject;
import hudson.model.Failure;
import jenkins.ClassLoaderReflectionToolkit;
import jenkins.model.Jenkins;
import hudson.model.UpdateCenter;
import hudson.model.UpdateSite;
import hudson.util.CyclicGraphDetector;
import hudson.util.CyclicGraphDetector.CycleDetectedException;
import hudson.util.FormValidation;
import hudson.util.IOException2;
import hudson.util.PersistedList;
import hudson.util.Service;
import jenkins.ClassLoaderReflectionToolkit;
import jenkins.InitReactorRunner;
import jenkins.RestartRequiredException;
import jenkins.YesNoMaybe;
import jenkins.model.Jenkins;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
......@@ -51,6 +50,7 @@ import org.apache.commons.io.FilenameUtils;
import org.apache.commons.logging.LogFactory;
import org.jvnet.hudson.reactor.Executable;
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.stapler.HttpRedirect;
......@@ -72,19 +72,21 @@ 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.Hashtable;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import static hudson.init.InitMilestone.*;
/**
* Manages {@link PluginWrapper}s.
*
......@@ -126,7 +128,7 @@ public abstract class PluginManager extends AbstractModelObject {
/**
* Once plugin is uploaded, this flag becomes true.
* This is used to report a message that Hudson needs to be restarted
* This is used to report a message that Jenkins needs to be restarted
* for new plugins to take effect.
*/
public volatile boolean pluginUploaded = false;
......@@ -282,8 +284,6 @@ public abstract class PluginManager extends AbstractModelObject {
Jenkins.getInstance().lookup.set(PluginInstanceStore.class,new PluginInstanceStore());
TaskGraphBuilder g = new TaskGraphBuilder();
Jenkins.getInstance().lookup.set(Injector.class,Guice.createInjector());
// schedule execution of loading plugins
for (final PluginWrapper p : activePlugins.toArray(new PluginWrapper[activePlugins.size()])) {
g.followedBy().notFatal().attains(PLUGINS_PREPARED).add("Loading plugin " + p.getShortName(), new Executable() {
......@@ -331,6 +331,55 @@ public abstract class PluginManager extends AbstractModelObject {
}});
}
/**
* TODO: revisit where/how to expose this. This is an experiment.
*/
public void dynamicLoad(File arc) throws IOException, InterruptedException, RestartRequiredException {
LOGGER.info("Attempting to dynamic load "+arc);
final PluginWrapper p = strategy.createPluginWrapper(arc);
String sn = p.getShortName();
if (getPlugin(sn)!=null)
throw new RestartRequiredException(Messages._PluginManager_PluginIsAlreadyInstalled_RestartRequired(sn));
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);
activePlugins.add(p);
try {
p.resolvePluginDependencies();
strategy.load(p);
Jenkins.getInstance().refreshExtensions();
p.getPlugin().postInitialize();
} catch (Exception e) {
failedPlugins.add(new FailedPlugin(sn, e));
activePlugins.remove(p);
plugins.remove(p);
throw new IOException2("Failed to install "+ sn +" plugin",e);
}
// run initializers in the added plugin
Reactor r = new Reactor(InitMilestone.ordering());
r.addAll(new InitializerFinder(p.classLoader) {
@Override
protected boolean filter(Method e) {
return e.getDeclaringClass().getClassLoader()!=p.classLoader || super.filter(e);
}
}.discoverTasks(r));
try {
new InitReactorRunner().run(r);
} catch (ReactorException e) {
throw new IOException2("Failed to initialize "+ sn +" plugin",e);
}
LOGGER.info("Plugin " + sn + " dynamically installed");
}
/**
* If the war file has any "/WEB-INF/plugins/*.hpi", extract them into the plugin directory.
*
......@@ -524,6 +573,8 @@ public abstract class PluginManager extends AbstractModelObject {
* Performs the installation of the plugins.
*/
public void doInstall(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
boolean dynamicLoad = req.getParameter("dynamicLoad")!=null;
Enumeration<String> en = req.getParameterNames();
while (en.hasMoreElements()) {
String n = en.nextElement();
......@@ -534,7 +585,7 @@ public abstract class PluginManager extends AbstractModelObject {
UpdateSite.Plugin p = Jenkins.getInstance().getUpdateCenter().getById(pluginInfo[1]).getPlugin(pluginInfo[0]);
if(p==null)
throw new Failure("No such plugin: "+n);
p.deploy();
p.deploy(dynamicLoad);
}
}
}
......
......@@ -25,6 +25,7 @@
package hudson;
import hudson.PluginManager.PluginInstanceStore;
import jenkins.YesNoMaybe;
import jenkins.model.Jenkins;
import hudson.model.UpdateCenter;
import hudson.model.UpdateSite;
......@@ -290,6 +291,15 @@ public class PluginWrapper implements Comparable<PluginWrapper> {
return shortName;
}
/**
* Does this plugin supports dynamic loading?
*/
public YesNoMaybe supportsDynamicLoad() {
String v = manifest.getMainAttributes().getValue("Support-Dynamic-Loading");
if (v==null) return YesNoMaybe.MAYBE;
return Boolean.parseBoolean(v) ? YesNoMaybe.YES : YesNoMaybe.NO;
}
/**
* Returns the version number of this plugin
*/
......
......@@ -25,6 +25,7 @@ package hudson.cli;
import hudson.Extension;
import hudson.FilePath;
import hudson.PluginManager;
import hudson.util.IOException2;
import jenkins.model.Jenkins;
import hudson.model.UpdateSite;
......@@ -65,9 +66,13 @@ public class InstallPluginCommand extends CLICommand {
@Option(name="-restart",usage="Restart Jenkins upon successful installation")
public boolean restart;
@Option(name="-deploy",usage="Deploy plugins right away without postponing them until the reboot.")
public boolean dynamicLoad;
protected int run() throws Exception {
Jenkins h = Jenkins.getInstance();
h.checkPermission(Jenkins.ADMINISTER);
PluginManager pm = h.getPluginManager();
for (String source : sources) {
// is this a file?
......@@ -76,7 +81,9 @@ public class InstallPluginCommand extends CLICommand {
stdout.println(Messages.InstallPluginCommand_InstallingPluginFromLocalFile(f));
if (name==null)
name = f.getBaseName();
f.copyTo(getTargetFile());
f.copyTo(getTargetFilePath());
if (dynamicLoad)
pm.dynamicLoad(getTargetFile());
continue;
}
......@@ -91,7 +98,9 @@ public class InstallPluginCommand extends CLICommand {
int idx = name.lastIndexOf('.');
if (idx>0) name = name.substring(0,idx);
}
getTargetFile().copyFrom(u);
getTargetFilePath().copyFrom(u);
if (dynamicLoad)
pm.dynamicLoad(getTargetFile());
continue;
} catch (MalformedURLException e) {
// not an URL
......@@ -101,7 +110,7 @@ public class InstallPluginCommand extends CLICommand {
UpdateSite.Plugin p = h.getUpdateCenter().getPlugin(source);
if (p!=null) {
stdout.println(Messages.InstallPluginCommand_InstallingFromUpdateCenter(source));
Throwable e = p.deploy().get().getError();
Throwable e = p.deploy(dynamicLoad).get().getError();
if (e!=null)
throw new IOException2("Failed to install plugin "+source,e);
continue;
......@@ -134,7 +143,11 @@ public class InstallPluginCommand extends CLICommand {
return 0; // all success
}
private FilePath getTargetFile() {
return new FilePath(new File(Jenkins.getInstance().getPluginManager().rootDir,name+".hpi"));
private FilePath getTargetFilePath() {
return new FilePath(getTargetFile());
}
private File getTargetFile() {
return new File(Jenkins.getInstance().getPluginManager().rootDir,name+".hpi");
}
}
......@@ -30,6 +30,8 @@ import hudson.Util;
import hudson.cli.CLICommand;
import hudson.cli.CloneableCLICommand;
import hudson.model.Hudson;
import jenkins.ExtensionComponentSet;
import jenkins.ExtensionRefreshException;
import jenkins.model.Jenkins;
import hudson.remoting.Channel;
import hudson.security.CliAuthenticator;
......@@ -64,9 +66,15 @@ import java.util.logging.Logger;
*/
@Extension
public class CLIRegisterer extends ExtensionFinder {
public <T> Collection<ExtensionComponent<T>> find(Class<T> type, Hudson hudson) {
@Override
public ExtensionComponentSet refresh() throws ExtensionRefreshException {
// TODO: this is not complex. just bit tedious.
return ExtensionComponentSet.EMPTY;
}
public <T> Collection<ExtensionComponent<T>> find(Class<T> type, Hudson jenkins) {
if (type==CLICommand.class)
return (List)discover(hudson);
return (List)discover(jenkins);
else
return Collections.emptyList();
}
......
......@@ -68,8 +68,7 @@ public class InitializerFinder extends TaskBuilder {
public Collection<Task> discoverTasks(Reactor session) throws IOException {
List<Task> result = new ArrayList<Task>();
for (Method e : Index.list(Initializer.class,cl,Method.class)) {
if (!discovered.add(e))
continue; // already reported once
if (filter(e)) continue; // already reported once
if (!Modifier.isStatic(e.getModifiers()))
throw new IOException(e+" is not a static method");
......@@ -82,6 +81,13 @@ public class InitializerFinder extends TaskBuilder {
return result;
}
/**
* Return true to ignore this method.
*/
protected boolean filter(Method e) {
return !discovered.add(e);
}
/**
* Obtains the display name of the given initialization task
*/
......
......@@ -36,6 +36,7 @@ import static hudson.init.InitMilestone.PLUGINS_STARTED;
import hudson.init.Initializer;
import hudson.lifecycle.Lifecycle;
import hudson.lifecycle.RestartNotSupportedException;
import hudson.model.UpdateCenter.DownloadJob;
import hudson.model.UpdateSite.Data;
import hudson.model.UpdateSite.Plugin;
import hudson.model.listeners.SaveableListener;
......@@ -45,10 +46,12 @@ import hudson.util.HttpResponses;
import hudson.util.IOException2;
import hudson.util.PersistedList;
import hudson.util.XStream2;
import jenkins.RestartRequiredException;
import jenkins.model.Jenkins;
import org.acegisecurity.Authentication;
import org.apache.commons.io.input.CountingInputStream;
import org.apache.commons.io.output.NullOutputStream;
import org.jvnet.localizer.Localizable;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
......@@ -951,6 +954,9 @@ public class UpdateCenter extends AbstractModelObject implements Saveable {
LOGGER.info("Installation successful: "+getName());
status = new Success();
onSuccess();
} catch (InstallationStatus e) {
status = e;
if (status.isSuccess()) onSuccess();
} catch (Throwable e) {
LOGGER.log(Level.SEVERE, "Failed to install "+getName(),e);
status = new Failure(e);
......@@ -958,7 +964,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable {
}
}
protected void _run() throws IOException {
protected void _run() throws IOException, InstallationStatus {
URL src = getURL();
config.preValidate(this, src);
......@@ -989,7 +995,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable {
* <p>
* Instances of this class is immutable.
*/
public abstract class InstallationStatus {
public abstract class InstallationStatus extends Throwable {
public final int id = iota.incrementAndGet();
public boolean isSuccess() {
return false;
......@@ -1006,11 +1012,28 @@ public class UpdateCenter extends AbstractModelObject implements Saveable {
this.problem = problem;
}
public String getStackTrace() {
public String getProblemStackTrace() {
return Functions.printThrowable(problem);
}
}
/**
* Indicates that the installation was successful but a restart is needed.
*
* @see
*/
public class SuccessButRequiresRestart extends Success {
private final Localizable message;
public SuccessButRequiresRestart(Localizable message) {
this.message = message;
}
public String getMessage() {
return message.toString();
}
}
/**
* Indicates that the plugin was successfully installed.
*/
......@@ -1052,9 +1075,22 @@ public class UpdateCenter extends AbstractModelObject implements Saveable {
private final PluginManager pm = Jenkins.getInstance().getPluginManager();
/**
* True to load the plugin into this Jenkins, false to wait until restart.
*/
private final boolean dynamicLoad;
/**
* @deprecated as of 1.442
*/
public InstallationJob(Plugin plugin, UpdateSite site, Authentication auth) {
this(plugin,site,auth,false);
}
public InstallationJob(Plugin plugin, UpdateSite site, Authentication auth, boolean dynamicLoad) {
super(site, auth);
this.plugin = plugin;
this.dynamicLoad = dynamicLoad;
}
protected URL getURL() throws MalformedURLException {
......@@ -1071,7 +1107,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable {
}
@Override
public void _run() throws IOException {
public void _run() throws IOException, InstallationStatus {
super._run();
// if this is a bundled plugin, make sure it won't get overwritten
......@@ -1083,6 +1119,18 @@ public class UpdateCenter extends AbstractModelObject implements Saveable {
} finally {
SecurityContextHolder.clearContext();
}
if (dynamicLoad) {
try {
pm.dynamicLoad(getDestination());
} catch (RestartRequiredException e) {
throw new SuccessButRequiresRestart(e.message);
} catch (Exception e) {
throw new IOException2("Failed to dynamically deploy this plugin",e);
}
} else {
throw new SuccessButRequiresRestart(Messages._UpdateCenter_DownloadButNotActivated());
}
}
protected void onSuccess() {
......
......@@ -45,6 +45,7 @@ import org.jvnet.hudson.crypto.CertificateUtil;
import org.jvnet.hudson.crypto.SignatureOutputStream;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
......@@ -689,21 +690,29 @@ public class UpdateSite {
deploy();
}
public Future<UpdateCenterJob> deploy() {
return deploy(false);
}
/**
* Schedules the installation of this plugin.
*
* <p>
* This is mainly intended to be called from the UI. The actual installation work happens
* asynchronously in another thread.
*
* @param dynamicLoad
* If true, the plugin will be dynamically loaded into this Jenkins. If false,
* the plugin will only take effect after the reboot.
*/
public Future<UpdateCenterJob> deploy() {
public Future<UpdateCenterJob> deploy(boolean dynamicLoad) {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
UpdateCenter uc = Jenkins.getInstance().getUpdateCenter();
for (Plugin dep : getNeededDependencies()) {
LOGGER.log(Level.WARNING, "Adding dependent install of " + dep.name + " for plugin " + name);
dep.deploy();
dep.deploy(dynamicLoad);
}
return uc.addJob(uc.new InstallationJob(this, UpdateSite.this, Jenkins.getAuthentication()));
return uc.addJob(uc.new InstallationJob(this, UpdateSite.this, Jenkins.getAuthentication(), dynamicLoad));
}
/**
......@@ -717,17 +726,22 @@ public class UpdateSite {
/**
* Making the installation web bound.
*/
public void doInstall(StaplerResponse rsp) throws IOException {
deploy();
rsp.sendRedirect2("../..");
public HttpResponse doInstall() throws IOException {
deploy(false);
return HttpResponses.redirectTo("../..");
}
public HttpResponse doInstallNow() throws IOException {
deploy(true);
return HttpResponses.redirectTo("../..");
}
/**
* Performs the downgrade of the plugin.
*/
public void doDowngrade(StaplerResponse rsp) throws IOException {
public HttpResponse doDowngrade() throws IOException {
deployBackup();
rsp.sendRedirect2("../..");
return HttpResponses.redirectTo("../..");
}
}
......
/*
* The MIT License
*
* Copyright (c) 2011, 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 jenkins;
import com.google.common.collect.Lists;
import hudson.ExtensionComponent;
import hudson.ExtensionFinder;
import hudson.ExtensionPoint;
import hudson.model.Descriptor;
import hudson.model.Hudson;
import jenkins.model.Jenkins;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Represents the components that's newly discovered during {@link ExtensionFinder#refresh()}.
*
* @author Kohsuke Kawaguchi
* @since 1.442
*/
public abstract class ExtensionComponentSet {
/**
* Discover extensions of the given type.
*
* <p>
* This method is called only once per the given type after all the plugins are loaded,
* so implementations need not worry about caching.
*
* @param <T>
* The type of the extension points. This is not bound to {@link ExtensionPoint} because
* of {@link Descriptor}, which by itself doesn't implement {@link ExtensionPoint} for
* a historical reason.
* @return
* Can be empty but never null.
*/
public abstract <T> Collection<ExtensionComponent<T>> find(Class<T> type);
/**
* Constant that has zero component in it.
*/
public static final ExtensionComponentSet EMPTY = new ExtensionComponentSet() {
@Override
public <T> Collection<ExtensionComponent<T>> find(Class<T> type) {
return Collections.emptyList();
}
};
/**
* Computes the union of all the given delta.
*/
public static ExtensionComponentSet union(final Collection<? extends ExtensionComponentSet> base) {
return new ExtensionComponentSet() {
@Override
public <T> Collection<ExtensionComponent<T>> find(Class<T> type) {
List<ExtensionComponent<T>> r = Lists.newArrayList();
for (ExtensionComponentSet d : base)
r.addAll(d.find(type));
return r;
}
};
}
public static ExtensionComponentSet union(ExtensionComponentSet... members) {
return union(Arrays.asList(members));
}
/**
* Wraps {@link ExtensionFinder} into {@link ExtensionComponentSet}.
*/
public static ExtensionComponentSet allOf(final ExtensionFinder f) {
return new ExtensionComponentSet() {
@Override
public <T> Collection<ExtensionComponent<T>> find(Class<T> type) {
return f.find(type,Hudson.getInstance());
}
};
}
}
/*
* The MIT License
*
* Copyright (c) 2011, 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 jenkins;
import hudson.ExtensionFinder;
/**
* Signals that {@link ExtensionFinder#refresh()} had failed.
*
* @author Kohsuke Kawaguchi
*/
public class ExtensionRefreshException extends Exception {
public ExtensionRefreshException() {
}
public ExtensionRefreshException(String message) {
super(message);
}
public ExtensionRefreshException(String message, Throwable cause) {
super(message, cause);
}
public ExtensionRefreshException(Throwable cause) {
super(cause);
}
}
package jenkins;
import hudson.init.InitMilestone;
import hudson.init.InitReactorListener;
import hudson.util.DaemonThreadFactory;
import hudson.util.Service;
import jenkins.model.Configuration;
import jenkins.model.Jenkins;
import org.jvnet.hudson.reactor.Milestone;
import org.jvnet.hudson.reactor.Reactor;
import org.jvnet.hudson.reactor.ReactorException;
import org.jvnet.hudson.reactor.ReactorListener;
import org.jvnet.hudson.reactor.Task;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import static java.util.logging.Level.SEVERE;
/**
* Executes the {@link Reactor} for the purpose of bootup.
*
* @author Kohsuke Kawaguchi
*/
public class InitReactorRunner {
public void run(Reactor reactor) throws InterruptedException, ReactorException, IOException {
reactor.addAll(InitMilestone.ordering().discoverTasks(reactor));
ExecutorService es;
if (Jenkins.PARALLEL_LOAD)
es = new ThreadPoolExecutor(
TWICE_CPU_NUM, TWICE_CPU_NUM, 5L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new DaemonThreadFactory());
else
es = Executors.newSingleThreadExecutor(new DaemonThreadFactory());
try {
reactor.execute(es,buildReactorListener());
} finally {
es.shutdownNow(); // upon a successful return the executor queue should be empty. Upon an exception, we want to cancel all pending tasks
}
}
/**
* Aggregates all the listeners into one and returns it.
*
* <p>
* At this point plugins are not loaded yet, so we fall back to the META-INF/services look up to discover implementations.
* As such there's no way for plugins to participate into this process.
*/
private ReactorListener buildReactorListener() throws IOException {
List<ReactorListener> r = (List) Service.loadInstances(Thread.currentThread().getContextClassLoader(), InitReactorListener.class);
r.add(new ReactorListener() {
final Level level = Level.parse( Configuration.getStringConfigParameter("initLogLevel", "FINE") );
public void onTaskStarted(Task t) {
LOGGER.log(level,"Started "+t.getDisplayName());
}
public void onTaskCompleted(Task t) {
LOGGER.log(level,"Completed "+t.getDisplayName());
}
public void onTaskFailed(Task t, Throwable err, boolean fatal) {
LOGGER.log(SEVERE, "Failed "+t.getDisplayName(),err);
}
public void onAttained(Milestone milestone) {
Level lv = level;
String s = "Attained "+milestone.toString();
if (milestone instanceof InitMilestone) {
lv = Level.INFO; // noteworthy milestones --- at least while we debug problems further
onInitMilestoneAttained((InitMilestone) milestone);
s = milestone.toString();
}
LOGGER.log(lv,s);
}
});
return new ReactorListener.Aggregator(r);
}
/**
* Called when the init milestone is attained.
*/
protected void onInitMilestoneAttained(InitMilestone milestone) {
}
private static final int TWICE_CPU_NUM = Runtime.getRuntime().availableProcessors() * 2;
private static final Logger LOGGER = Logger.getLogger(InitReactorRunner.class.getName());
}
/*
* The MIT License
*
* Copyright (c) 2011, 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 jenkins;
import com.google.inject.Binding;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.MembersInjector;
import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.Scope;
import com.google.inject.TypeLiteral;
import com.google.inject.spi.TypeConverterBinding;
import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* {@link Injector} that delegates to another one.
*
* @author Kohsuke Kawaguchi
*/
public abstract class ProxyInjector implements Injector {
protected abstract Injector resolve();
public void injectMembers(Object instance) {
resolve().injectMembers(instance);
}
public <T> MembersInjector<T> getMembersInjector(TypeLiteral<T> typeLiteral) {
return resolve().getMembersInjector(typeLiteral);
}
public <T> MembersInjector<T> getMembersInjector(Class<T> type) {
return resolve().getMembersInjector(type);
}
public Map<Key<?>, Binding<?>> getBindings() {
return resolve().getBindings();
}
public Map<Key<?>, Binding<?>> getAllBindings() {
return resolve().getAllBindings();
}
public <T> Binding<T> getBinding(Key<T> key) {
return resolve().getBinding(key);
}
public <T> Binding<T> getBinding(Class<T> type) {
return resolve().getBinding(type);
}
public <T> Binding<T> getExistingBinding(Key<T> key) {
return resolve().getExistingBinding(key);
}
public <T> List<Binding<T>> findBindingsByType(TypeLiteral<T> type) {
return resolve().findBindingsByType(type);
}
public <T> Provider<T> getProvider(Key<T> key) {
return resolve().getProvider(key);
}
public <T> Provider<T> getProvider(Class<T> type) {
return resolve().getProvider(type);
}
public <T> T getInstance(Key<T> key) {
return resolve().getInstance(key);
}
public <T> T getInstance(Class<T> type) {
return resolve().getInstance(type);
}
public Injector getParent() {
return resolve().getParent();
}
public Injector createChildInjector(Iterable<? extends Module> modules) {
return resolve().createChildInjector(modules);
}
public Injector createChildInjector(Module... modules) {
return resolve().createChildInjector(modules);
}
public Map<Class<? extends Annotation>, Scope> getScopeBindings() {
return resolve().getScopeBindings();
}
public Set<TypeConverterBinding> getTypeConverterBindings() {
return resolve().getTypeConverterBindings();
}
}
/*
* The MIT License
*
* Copyright (c) 2011, 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 jenkins;
import org.jvnet.localizer.Localizable;
/**
* Indicates that the plugin cannot be deployed without a restart.
*
* @author Kohsuke Kawaguchi
*/
public class RestartRequiredException extends Exception {
public final Localizable message;
public RestartRequiredException(Localizable message) {
this.message = message;
}
public RestartRequiredException(Localizable message, Throwable cause) {
super(cause);
this.message = message;
}
}
/*
* The MIT License
*
* Copyright (c) 2011, 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 jenkins;
/**
* Enum that represents {@link Boolean} state (including null for the absence.)
*
* <p>
* This is for situations where we can't use {@link Boolean}, such as annotation elements.
*
* @author Kohsuke Kawaguchi
*/
public enum YesNoMaybe {
YES,
NO,
MAYBE;
public static Boolean toBoolean(YesNoMaybe v) {
if (v==null) return null;
return v.toBool();
}
public Boolean toBool() {
switch (this) {
case YES:
return true;
case NO:
return false;
default:
return null;
}
}
}
......@@ -25,7 +25,10 @@
*/
package jenkins.model;
import com.google.common.collect.Lists;
import com.google.inject.Injector;
import hudson.ExtensionComponent;
import hudson.ExtensionFinder;
import hudson.model.Messages;
import hudson.model.Node;
import hudson.model.AbstractCIBase;
......@@ -184,6 +187,9 @@ import hudson.views.DefaultViewsTabBar;
import hudson.views.MyViewsTabBar;
import hudson.views.ViewsTabBar;
import hudson.widgets.Widget;
import jenkins.ExtensionComponentSet;
import jenkins.ExtensionRefreshException;
import jenkins.InitReactorRunner;
import net.sf.json.JSONObject;
import org.acegisecurity.AccessDeniedException;
import org.acegisecurity.AcegiSecurityException;
......@@ -806,56 +812,15 @@ public class Jenkins extends AbstractCIBase implements ModifiableItemGroup<TopLe
}
};
ExecutorService es;
if (PARALLEL_LOAD)
es = new ThreadPoolExecutor(
TWICE_CPU_NUM, TWICE_CPU_NUM, 5L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new DaemonThreadFactory());
else
es = Executors.newSingleThreadExecutor(new DaemonThreadFactory());
try {
reactor.execute(es,buildReactorListener());
} finally {
es.shutdownNow(); // upon a successful return the executor queue should be empty. Upon an exception, we want to cancel all pending tasks
}
}
/**
* Aggregates all the listeners into one and returns it.
*
* <p>
* At this point plugins are not loaded yet, so we fall back to the META-INF/services look up to discover implementations.
* As such there's no way for plugins to participate into this process.
*/
private ReactorListener buildReactorListener() throws IOException {
List<ReactorListener> r = (List) Service.loadInstances(Thread.currentThread().getContextClassLoader(), InitReactorListener.class);
r.add(new ReactorListener() {
final Level level = Level.parse( Configuration.getStringConfigParameter("initLogLevel", "FINE") );
public void onTaskStarted(Task t) {
LOGGER.log(level,"Started "+t.getDisplayName());
}
public void onTaskCompleted(Task t) {
LOGGER.log(level,"Completed "+t.getDisplayName());
}
public void onTaskFailed(Task t, Throwable err, boolean fatal) {
LOGGER.log(SEVERE, "Failed "+t.getDisplayName(),err);
}
public void onAttained(Milestone milestone) {
Level lv = level;
String s = "Attained "+milestone.toString();
if (milestone instanceof InitMilestone) {
lv = Level.INFO; // noteworthy milestones --- at least while we debug problems further
initLevel = (InitMilestone)milestone;
s = initLevel.toString();
}
LOGGER.log(lv,s);
new InitReactorRunner() {
@Override
protected void onInitMilestoneAttained(InitMilestone milestone) {
initLevel = milestone;
}
});
return new ReactorListener.Aggregator(r);
}.run(reactor);
}
public TcpSlaveAgentListener getTcpSlaveAgentListener() {
return tcpSlaveAgentListener;
}
......@@ -1985,6 +1950,48 @@ public class Jenkins extends AbstractCIBase implements ModifiableItemGroup<TopLe
return descriptorLists.get(type);
}
/**
* Refresh {@link ExtensionList}s by adding all the newly discovered extensions.
*
* Exposed only for {@link PluginManager#dynamicLoad(File)}.
*/
public void refreshExtensions() throws ExtensionRefreshException {
ExtensionList<ExtensionFinder> finders = getExtensionList(ExtensionFinder.class);
for (ExtensionFinder ef : finders) {
if (!ef.isRefreshable())
throw new ExtensionRefreshException(ef+" doesn't support refresh");
}
List<ExtensionComponentSet> fragments = Lists.newArrayList();
for (ExtensionFinder ef : finders) {
fragments.add(ef.refresh());
}
ExtensionComponentSet delta = ExtensionComponentSet.union(fragments);
// if we find a new ExtensionFinder, we need it to list up all the extension points as well
List<ExtensionComponent<ExtensionFinder>> newFinders = Lists.newArrayList(delta.find(ExtensionFinder.class));
while (!newFinders.isEmpty()) {
ExtensionFinder f = newFinders.remove(newFinders.size()-1).getInstance();
ExtensionComponentSet ecs = ExtensionComponentSet.allOf(f);
newFinders.addAll(ecs.find(ExtensionFinder.class));
delta = ExtensionComponentSet.union(delta, ecs);
}
for (ExtensionList el : extensionLists.values()) {
el.refresh(delta);
}
for (ExtensionList el : descriptorLists.values()) {
el.refresh(delta);
}
// TODO: we need some generalization here so that extension points can be notified when a refresh happens?
for (ExtensionComponent<RootAction> ea : delta.find(RootAction.class)) {
Action a = ea.getInstance();
if (!actions.contains(a)) actions.add(a);
}
}
/**
* Returns the root {@link ACL}.
*
......
......@@ -35,6 +35,8 @@ FilePath.validateRelativePath.notDirectory=''{0}'' is not a directory
FilePath.validateRelativePath.noSuchFile=No such file: ''{0}''
FilePath.validateRelativePath.noSuchDirectory=No such directory: ''{0}''
PluginManager.PluginDoesntSupportDynamicLoad.RestartRequired={0} plugin doesn''t support dynamic loading. Jenkins needs to be restarted for the update to take effect
PluginManager.PluginIsAlreadyInstalled.RestartRequired={0} plugin is already installed. Jenkins needs to be restarted for the update to take effect
Util.millisecond={0} ms
Util.second={0} sec
Util.minute={0} min
......
......@@ -137,7 +137,9 @@ THE SOFTWARE.
<j:if test="${!empty(list)}">
<div style="margin-top:1em">
<f:submit value="${%Install}" />
<f:submit value="${%Install without restart}" name="dynamicLoad" />
<span style="margin-left:2em;"></span>
<f:submit value="${%Download now and install after restart}" />
</div>
</j:if>
<d:invokeBody />
......
......@@ -228,6 +228,7 @@ Slave.UnableToLaunch=Unable to launch the slave agent for {0}{1}
Slave.UnixSlave=This is a Unix slave
Slave.WindowsSlave=This is a Windows slave
UpdateCenter.DownloadButNotActivated=Downloaded Successfully. Will be activated during the next boot
View.Permissions.Title=View
View.CreatePermission.Description=\
This permission allows users to create new views.
......
......@@ -30,5 +30,5 @@ THE SOFTWARE.
<a href="" onclick="var n=findNext(this,function(e){return e.tagName=='PRE'});
n.style.display='block';this.style.display='none';return false">${%Details}</a>
</div>
<pre style="display:none">${it.stackTrace}</pre>
<pre style="display:none">${it.problemStackTrace}</pre>
</j:jelly>
/*
* The MIT License
*
* Copyright (c) 2011, 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.
*/
img(src:"${imagesURL}/24x24/yellow.png",height:24,width:24)
text(" ");
text(my.message)
......@@ -27,7 +27,7 @@ THE SOFTWARE.
<tr id="row${it.id}">
<td style="vertical-align: top; padding-right:1em">${it.name}</td>
<j:set var="status" value="${it.status}" /><!-- so that two reference to this variable resolve to the same value. -->
<td id="status${status.id}">
<td id="status${status.id}" style="vertical-align:middle">
<st:include it="${status}" page="status.jelly" />
</td>
</tr>
......
......@@ -36,9 +36,13 @@ THE SOFTWARE.
</j:forEach>
</table>
<div id="scheduleRestartBlock">
<div id="scheduleRestartBlock" style="padding-top:2em">
<form method="post" id="scheduleRestart" action="">
<p class="info">
<p class="info" style="font-weight: normal">
<a href="${rootURL}/">${%Go back to the top page}</a> <br/>
(you can start using the installed plugins right away) <!-- leave this text in for a while until users are retrained to expect plugins to work right away without restart -->
</p>
<p class="info" style="font-weight: normal">
<j:if test="${app.lifecycle.canRestart()}">
<j:if test="${app.isQuietingDown() or it.isRestartScheduled()}">
<input id="scheduleRestartCheckbox" onchange="submitScheduleForm(this)" type="checkbox" checked="checked" />
......
此差异由.gitattributes 抑制。
......@@ -156,4 +156,14 @@ public class PluginManagerTest extends HudsonTestCase {
t.setContextClassLoader(old);
}
}
public void testInstallWithoutRestart() throws Exception {
URL res = getClass().getClassLoader().getResource("plugins/htmlpublisher.hpi");
File f = new File(jenkins.getRootDir(), "plugins/htmlpublisher.hpi");
FileUtils.copyURLToFile(res, f);
jenkins.pluginManager.dynamicLoad(f);
Class c = jenkins.getPluginManager().uberClassLoader.loadClass("htmlpublisher.HtmlPublisher$DescriptorImpl");
assertNotNull(jenkins.getDescriptorByType(c));
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册