提交 5169f140 编写于 作者: J Jesse Glick 提交者: Oleg Nenashev

Batch up dynamic plugin installations from setup wizard (#4124)

* Batch up dynamic plugin installations from setup wizard.

* Reworked to thread the list of batched-up plugins properly and introduced CompleteBatchJob to force the setup wizard to wait.

* UpdateCenterPluginInstallTest.test_installKnownPlugins failure.

* @amuniz notes that an existing message could now be considered misleading.

* Introduced UpdateCenter.DownloadJob.getDisplayName for better consistency in logging, as suggested by @Vlatombe.

* Reducing log levels of messages from dynamicLoad which are not of great interest, as suggested by @Vlatombe.

* Reducing log level of another message of little interest.

* Adding classic UI for CompleteBatchJob.

* CompleteBatchJob should be used only when dynamicLoad.

* Null defense on UpdateCenterJob.site.
Otherwise can get:
java.lang.NullPointerException
	at hudson.model.UpdateCenter.getConnectionCheckJob(UpdateCenter.java:850)
	at hudson.model.UpdateCenter.addConnectionCheckJob(UpdateCenter.java:817)
	at hudson.model.UpdateCenter.addJob(UpdateCenter.java:804)
	at hudson.model.UpdateSite$Plugin.deploy(UpdateSite.java:1367)
	at hudson.PluginManager.install(PluginManager.java:1537)
	at hudson.PluginManager.install(PluginManager.java:1496)
	at hudson.PluginManager.doInstall(PluginManager.java:1445)
	at …
上级 551cbd78
......@@ -84,7 +84,6 @@ 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.ReactorException;
import org.jvnet.hudson.reactor.TaskBuilder;
import org.jvnet.hudson.reactor.TaskGraphBuilder;
import org.kohsuke.accmod.Restricted;
......@@ -150,6 +149,7 @@ 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.*;
......@@ -872,16 +872,16 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
* TODO: revisit where/how to expose this. This is an experiment.
*/
public void dynamicLoad(File arc) throws IOException, InterruptedException, RestartRequiredException {
dynamicLoad(arc, false);
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) throws IOException, InterruptedException, RestartRequiredException {
public void dynamicLoad(File arc, boolean removeExisting, @CheckForNull List<PluginWrapper> batch) throws IOException, InterruptedException, RestartRequiredException {
try (ACLContext context = ACL.as(ACL.SYSTEM)) {
LOGGER.info("Attempting to dynamic load "+arc);
LOGGER.log(FINE, "Attempting to dynamic load {0}", arc);
PluginWrapper p = null;
String sn;
try {
......@@ -928,9 +928,12 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
p.resolvePluginDependencies();
strategy.load(p);
Jenkins.get().refreshExtensions();
if (batch != null) {
batch.add(p);
} else {
start(Collections.singletonList(p));
}
p.getPlugin().postInitialize();
} catch (Exception e) {
failedPlugins.add(new FailedPlugin(sn, e));
activePlugins.remove(p);
......@@ -938,46 +941,55 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
throw new IOException("Failed to install "+ sn +" plugin",e);
}
// run initializers in the added plugin
Reactor r = new Reactor(InitMilestone.ordering());
final ClassLoader loader = p.classLoader;
r.addAll(new InitializerFinder(loader) {
@Override
protected boolean filter(Method e) {
return e.getDeclaringClass().getClassLoader() != loader || super.filter(e);
}
}.discoverTasks(r));
try {
new InitReactorRunner().run(r);
} catch (ReactorException e) {
throw new IOException("Failed to initialize "+ 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 {
Jenkins.get().refreshExtensions();
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);
// recalculate dependencies of plugins optionally depending the newly deployed one.
for (PluginWrapper depender: plugins) {
if (depender.equals(p)) {
// skip itself.
continue;
}
for (Dependency d: depender.getOptionalDependencies()) {
if (d.shortName.equals(p.getShortName())) {
// this plugin depends on the newly loaded one!
// recalculate dependencies!
getPluginStrategy().updateDependency(depender, p);
break;
}
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();
// Redo who depends on who.
resolveDependentPlugins();
try {
Jenkins.get().refreshExtensions();
} catch (ExtensionRefreshException e) {
throw new IOException("Failed to refresh extensions after installing " + sn + " plugin", e);
}
LOGGER.info("Plugin " + p.getShortName()+":"+p.getVersion() + " dynamically installed");
try {
Jenkins.get().refreshExtensions();
} catch (ExtensionRefreshException e) {
throw new IOException("Failed to refresh extensions after installing some plugins", e);
}
}
......@@ -1487,6 +1499,10 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
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('.');
......@@ -1518,18 +1534,17 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
if (p == null) {
throw new Failure("No such plugin: " + n);
}
Future<UpdateCenter.UpdateCenterJob> jobFuture = p.deploy(dynamicLoad, correlationId);
Future<UpdateCenter.UpdateCenterJob> jobFuture = p.deploy(dynamicLoad, correlationId, batch);
installJobs.add(jobFuture);
}
trackInitialPluginInstall(installJobs);
return installJobs;
}
private void trackInitialPluginInstall(@Nonnull final List<Future<UpdateCenter.UpdateCenterJob>> installJobs) {
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()) {
......@@ -1567,32 +1582,8 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
}
}.start();
}
// Fire a one-off thread to wait for the plugins to be deployed and then
// refresh the dependent plugins list.
new Thread() {
@Override
public void run() {
INSTALLING: while (true) {
for (Future<UpdateCenter.UpdateCenterJob> deployJob : installJobs) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
LOGGER.log(SEVERE, "Unexpected error while waiting for some plugins to install. Plugin Manager state may be invalid. Please restart Jenkins ASAP.", e);
}
if (!deployJob.isCancelled() && !deployJob.isDone()) {
// One of the plugins is not installing/canceled, so
// go back to sleep and try again in a while.
continue INSTALLING;
}
}
// All the plugins are installed. It's now safe to refresh.
resolveDependentPlugins();
break;
}
}
}.start();
return installJobs;
}
private UpdateSite.Plugin getPlugin(String pluginName, String siteName) {
......
......@@ -799,8 +799,11 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
}
/*package*/ synchronized Future<UpdateCenterJob> addJob(UpdateCenterJob job) {
addConnectionCheckJob(job.site);
@Restricted(NoExternalUse.class)
public synchronized Future<UpdateCenterJob> addJob(UpdateCenterJob job) {
if (job.site != null) {
addConnectionCheckJob(job.site);
}
return job.submit();
}
......@@ -846,7 +849,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
private @CheckForNull ConnectionCheckJob getConnectionCheckJob(@Nonnull UpdateSite site) {
synchronized (jobs) {
for (UpdateCenterJob job : jobs) {
if (job instanceof ConnectionCheckJob && job.site.getId().equals(site.getId())) {
if (job instanceof ConnectionCheckJob && job.site != null && job.site.getId().equals(site.getId())) {
return (ConnectionCheckJob) job;
}
}
......@@ -1331,7 +1334,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
/**
* Which {@link UpdateSite} does this belong to?
*/
public final UpdateSite site;
public final @CheckForNull UpdateSite site;
/**
* Simple correlation ID that can be used to associated a batch of jobs e.g. the
......@@ -1344,7 +1347,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
*/
protected Throwable error;
protected UpdateCenterJob(UpdateSite site) {
protected UpdateCenterJob(@CheckForNull UpdateSite site) {
this.site = site;
}
......@@ -1492,7 +1495,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
public void run() {
connectionStates.put(ConnectionStatus.INTERNET, ConnectionStatus.UNCHECKED);
connectionStates.put(ConnectionStatus.UPDATE_SITE, ConnectionStatus.UNCHECKED);
if (ID_UPLOAD.equals(site.getId())) {
if (site == null || ID_UPLOAD.equals(site.getId())) {
return;
}
LOGGER.fine("Doing a connectivity check");
......@@ -1595,7 +1598,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
if (dynamicLoad) {
try {
// remove the existing, disabled inactive plugin to force a new one to load
pm.dynamicLoad(getDestination(), true);
pm.dynamicLoad(getDestination(), true, null);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Failed to dynamically load " + plugin.getDisplayName(), e);
error = e;
......@@ -1660,9 +1663,20 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
*/
protected abstract File getDestination();
/**
* Code name used for logging.
*/
@Exported
public abstract String getName();
/**
* Display name used for the GUI.
* @since TODO
*/
public String getDisplayName() {
return getName();
}
/**
* Called when the whole thing went successfully.
*/
......@@ -1971,6 +1985,8 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
*/
protected final boolean dynamicLoad;
@CheckForNull List<PluginWrapper> batch;
/**
* @deprecated as of 1.442
*/
......@@ -1999,7 +2015,13 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
return new File(baseDir, plugin.name + ".hpi");
}
@Override
public String getName() {
return plugin.name;
}
@Override
public String getDisplayName() {
return plugin.getDisplayName();
}
......@@ -2027,7 +2049,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
if (dynamicLoad) {
try {
pm.dynamicLoad(getDestination());
pm.dynamicLoad(getDestination(), false, batch);
} catch (RestartRequiredException e) {
throw new SuccessButRequiresRestart(e.message);
} catch (Exception e) {
......@@ -2100,7 +2122,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
*/
@Override
protected void replace(File dst, File src) throws IOException {
if (!site.getId().equals(ID_UPLOAD)) {
if (site == null || !site.getId().equals(ID_UPLOAD)) {
verifyChecksums(this, plugin, src);
}
......@@ -2123,6 +2145,61 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
throw new IOException("Failed to rename "+src+" to "+dst);
}
}
void setBatch(List<PluginWrapper> batch) {
this.batch = batch;
}
}
@Restricted(NoExternalUse.class)
public final class CompleteBatchJob extends UpdateCenterJob {
private final List<PluginWrapper> batch;
private final long start;
@Exported(inline = true)
public volatile CompleteBatchJobStatus status = new Pending();
public CompleteBatchJob(List<PluginWrapper> batch, long start, UUID correlationId) {
super(getCoreSource());
this.batch = batch;
this.start = start;
setCorrelationId(correlationId);
}
@Override
public void run() {
LOGGER.info("Completing installing of plugin batch…");
status = new Running();
try {
Jenkins.get().getPluginManager().start(batch);
status = new Success();
} catch (Exception x) {
status = new Failure(x);
LOGGER.log(Level.WARNING, "Failed to start some plugins", x);
}
LOGGER.log(INFO, "Completed installation of {0} plugins in {1}", new Object[] {batch.size(), Util.getTimeSpanString((System.nanoTime() - start) / 1_000_000)});
}
@ExportedBean
public abstract class CompleteBatchJobStatus {
@Exported
public final int id = iota.incrementAndGet();
}
public class Pending extends CompleteBatchJobStatus {}
public class Running extends CompleteBatchJobStatus {}
public class Success extends CompleteBatchJobStatus {}
public class Failure extends CompleteBatchJobStatus {
Failure(Throwable problemStackTrace) {
this.problemStackTrace = problemStackTrace;
}
public final Throwable problemStackTrace;
}
}
/**
......@@ -2159,7 +2236,13 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
return new File(baseDir, plugin.name + ".bak");
}
@Override
public String getName() {
return plugin.name;
}
@Override
public String getDisplayName() {
return plugin.getDisplayName();
}
......@@ -2219,6 +2302,9 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
}
protected URL getURL() throws MalformedURLException {
if (site == null) {
throw new MalformedURLException("no update site defined");
}
return new URL(site.getData().core.url);
}
......@@ -2236,6 +2322,9 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
@Override
protected void replace(File dst, File src) throws IOException {
if (site == null) {
throw new IOException("no update site defined");
}
verifyChecksums(this, site.getData().core, src);
Lifecycle.get().rewriteHudsonWar(src);
}
......@@ -2247,6 +2336,9 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
}
protected URL getURL() throws MalformedURLException {
if (site == null) {
throw new MalformedURLException("no update site defined");
}
return new URL(site.getData().core.url);
}
......
......@@ -1319,7 +1319,7 @@ public class UpdateSite {
* See {@link UpdateCenter#isRestartRequiredForCompletion()}
*/
public Future<UpdateCenterJob> deploy(boolean dynamicLoad) {
return deploy(dynamicLoad, null);
return deploy(dynamicLoad, null, null);
}
/**
......@@ -1334,18 +1334,19 @@ public class UpdateSite {
* the plugin will only take effect after the reboot.
* See {@link UpdateCenter#isRestartRequiredForCompletion()}
* @param correlationId A correlation ID to be set on the job.
* @param batch if defined, a list of plugins to add to, which will be started later
*/
@Restricted(NoExternalUse.class)
public Future<UpdateCenterJob> deploy(boolean dynamicLoad, @CheckForNull UUID correlationId) {
public Future<UpdateCenterJob> deploy(boolean dynamicLoad, @CheckForNull UUID correlationId, @CheckForNull List<PluginWrapper> batch) {
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
UpdateCenter uc = Jenkins.get().getUpdateCenter();
for (Plugin dep : getNeededDependencies()) {
UpdateCenter.InstallationJob job = uc.getJob(dep);
if (job == null || job.status instanceof UpdateCenter.DownloadJob.Failure) {
LOGGER.log(Level.INFO, "Adding dependent install of " + dep.name + " for plugin " + name);
dep.deploy(dynamicLoad);
dep.deploy(dynamicLoad, /* UpdateCenterPluginInstallTest.test_installKnownPlugins specifically asks that these not be correlated */ null, batch);
} else {
LOGGER.log(Level.INFO, "Dependent install of " + dep.name + " for plugin " + name + " already added, skipping");
LOGGER.log(Level.FINE, "Dependent install of {0} for plugin {1} already added, skipping", new Object[] {dep.name, name});
}
}
PluginWrapper pw = getInstalled();
......@@ -1362,6 +1363,7 @@ public class UpdateSite {
}
UpdateCenter.InstallationJob job = createInstallationJob(this, uc, dynamicLoad);
job.setCorrelationId(correlationId);
job.setBatch(batch);
return uc.addJob(job);
}
......
<!--
The MIT License
Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Alan Harder
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.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<div>
<l:icon class="icon-red icon-md"/> ${%Failure}
-
<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.problemStackTrace}</pre>
</j:jelly>
<!--
The MIT License
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
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.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<l:icon class="icon-grey icon-md"/> ${%Pending}
</j:jelly>
<!--
The MIT License
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
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.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<l:icon class="icon-grey-anime icon-md"/> ${%Running}
</j:jelly>
<!--
The MIT License
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
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.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<l:icon class="icon-blue icon-md"/> ${%Success}
</j:jelly>
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<tr id="row${it.id}">
<td style="vertical-align: top; padding-right:1em">${%Loading plugin extensions}</td>
<j:set var="status" value="${it.status}"/>
<td id="status${status.id}">
<st:include it="${status}" page="status.jelly"/>
</td>
</tr>
</j:jelly>
......@@ -25,7 +25,7 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<tr id="row${it.id}">
<td style="vertical-align: top; padding-right:1em">${it.name}</td>
<td style="vertical-align: top; padding-right:1em">${it.displayName}</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}" style="vertical-align:middle">
<st:include it="${status}" page="status.jelly" />
......
......@@ -81,7 +81,7 @@ public class UpdateCenterPluginInstallTest {
Assert.assertEquals("ok", json.get("status"));
JSONObject status = installStatus.getJSONObject("data");
JSONArray states = status.getJSONArray("jobs");
Assert.assertEquals(2, states.size());
Assert.assertEquals(states.toString(), 2, states.size());
JSONObject pluginInstallState = states.getJSONObject(0);
Assert.assertEquals("changelog-history", pluginInstallState.get("name"));
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册