org.samba.jcifs
jcifs
diff --git a/cli/pom.xml b/cli/pom.xml
index 6309811a2e8bc..2473629d23cc8 100644
--- a/cli/pom.xml
+++ b/cli/pom.xml
@@ -15,7 +15,7 @@
https://github.com/jenkinsci/jenkins
- 2.13.2
+ 2.14.0
diff --git a/core/src/main/java/hudson/logging/LogRecorder.java b/core/src/main/java/hudson/logging/LogRecorder.java
index ad4c40028547b..c11c1bbfa57ef 100644
--- a/core/src/main/java/hudson/logging/LogRecorder.java
+++ b/core/src/main/java/hudson/logging/LogRecorder.java
@@ -556,7 +556,7 @@ public void delete() throws IOException {
loggers.forEach(Target::disable);
getParent().getRecorders().forEach(logRecorder -> logRecorder.getLoggers().forEach(Target::enable));
- SaveableListener.fireOnChange(this, getConfigFile());
+ SaveableListener.fireOnDeleted(this, getConfigFile());
}
/**
diff --git a/core/src/main/java/hudson/markup/MarkupFormatter.java b/core/src/main/java/hudson/markup/MarkupFormatter.java
index a3bf53a739a19..95af637af9749 100644
--- a/core/src/main/java/hudson/markup/MarkupFormatter.java
+++ b/core/src/main/java/hudson/markup/MarkupFormatter.java
@@ -62,9 +62,14 @@
* This is an extension point in Hudson, allowing plugins to implement different markup formatters.
*
*
- * Implement the following methods to enable and control CodeMirror syntax highlighting
- * public String getCodeMirrorMode() // return null to disable CodeMirror dynamically
- * public String getCodeMirrorConfig()
+ * Implement the following methods to enable and control CodeMirror syntax highlighting:
+ *
+ * public String getCodeMirrorMode()
(return null
to disable CodeMirror dynamically)
+ * -
+ *
public String getCodeMirrorConfig()
(JSON snippet without surrounding curly braces, e.g., "mode": "text/css"
.
+ * Historically this allowed invalid JSON, but since TODO it needs to be properly quoted etc.
+ *
+ *
*
* Views
*
diff --git a/core/src/main/java/hudson/model/AbstractItem.java b/core/src/main/java/hudson/model/AbstractItem.java
index 8ba7aafe15cdc..f31316c316569 100644
--- a/core/src/main/java/hudson/model/AbstractItem.java
+++ b/core/src/main/java/hudson/model/AbstractItem.java
@@ -59,8 +59,6 @@
import java.util.ListIterator;
import java.util.logging.Level;
import java.util.logging.Logger;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.sax.SAXSource;
@@ -70,6 +68,7 @@
import jenkins.model.Jenkins;
import jenkins.model.Loadable;
import jenkins.model.queue.ItemDeletion;
+import jenkins.security.ExtendedReadRedaction;
import jenkins.security.NotReallyRoleSensitiveCallable;
import jenkins.security.stapler.StaplerNotDispatchable;
import jenkins.util.SystemProperties;
@@ -815,6 +814,7 @@ public void delete() throws IOException, InterruptedException {
ItemDeletion.deregister(this);
}
}
+ SaveableListener.fireOnDeleted(this, getConfigFile());
getParent().onDeleted(AbstractItem.this);
Jenkins.get().rebuildDependencyGraphAsync();
}
@@ -870,11 +870,11 @@ private void doConfigDotXmlImpl(StaplerRequest2 req, StaplerResponse2 rsp)
rsp.sendError(SC_BAD_REQUEST);
}
- static final Pattern SECRET_PATTERN = Pattern.compile(">(" + Secret.ENCRYPTED_VALUE_PATTERN + ")<");
/**
* Writes {@code config.xml} to the specified output stream.
* The user must have at least {@link #EXTENDED_READ}.
- * If he lacks {@link #CONFIGURE}, then any {@link Secret}s detected will be masked out.
+ * If he lacks {@link #CONFIGURE}, then any {@link Secret}s or other sensitive information detected will be masked out.
+ * @see jenkins.security.ExtendedReadRedaction
*/
@Restricted(NoExternalUse.class)
@@ -886,15 +886,13 @@ public void writeConfigDotXml(OutputStream os) throws IOException {
} else {
String encoding = configFile.sniffEncoding();
String xml = Files.readString(Util.fileToPath(configFile.getFile()), Charset.forName(encoding));
- Matcher matcher = SECRET_PATTERN.matcher(xml);
- StringBuilder cleanXml = new StringBuilder();
- while (matcher.find()) {
- if (Secret.decrypt(matcher.group(1)) != null) {
- matcher.appendReplacement(cleanXml, ">********<");
- }
+
+ for (ExtendedReadRedaction redaction : ExtendedReadRedaction.all()) {
+ LOGGER.log(Level.FINE, () -> "Applying redaction " + redaction.getClass().getName());
+ xml = redaction.apply(xml);
}
- matcher.appendTail(cleanXml);
- org.apache.commons.io.IOUtils.write(cleanXml.toString(), os, encoding);
+
+ org.apache.commons.io.IOUtils.write(xml, os, encoding);
}
}
diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java
index 081f73dd16256..1047fe97000c1 100644
--- a/core/src/main/java/hudson/model/Computer.java
+++ b/core/src/main/java/hudson/model/Computer.java
@@ -41,7 +41,6 @@
import hudson.console.AnnotatedLargeText;
import hudson.init.Initializer;
import hudson.model.Descriptor.FormException;
-import hudson.model.Queue.FlyweightTask;
import hudson.model.labels.LabelAtom;
import hudson.model.queue.WorkUnit;
import hudson.node_monitors.AbstractDiskSpaceMonitor;
@@ -106,6 +105,9 @@
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import jenkins.model.DisplayExecutor;
+import jenkins.model.IComputer;
+import jenkins.model.IDisplayExecutor;
import jenkins.model.Jenkins;
import jenkins.security.ImpersonatingExecutorService;
import jenkins.security.MasterToSlaveCallable;
@@ -116,8 +118,6 @@
import jenkins.util.SystemProperties;
import jenkins.widgets.HasWidgets;
import net.jcip.annotations.GuardedBy;
-import org.jenkins.ui.icon.Icon;
-import org.jenkins.ui.icon.IconSet;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@@ -150,7 +150,7 @@
* if a {@link Node} is configured (probably temporarily) with 0 executors,
* you won't have a {@link Computer} object for it (except for the built-in node,
* which always gets its {@link Computer} in case we have no static executors and
- * we need to run a {@link FlyweightTask} - see JENKINS-7291 for more discussion.)
+ * we need to run a {@link Queue.FlyweightTask} - see JENKINS-7291 for more discussion.)
*
* Also, even if you remove a {@link Node}, it takes time for the corresponding
* {@link Computer} to be removed, if some builds are already in progress on that
@@ -164,7 +164,7 @@
* @author Kohsuke Kawaguchi
*/
@ExportedBean
-public /*transient*/ abstract class Computer extends Actionable implements AccessControlled, ExecutorListener, DescriptorByNameOwner, StaplerProxy, HasWidgets {
+public /*transient*/ abstract class Computer extends Actionable implements AccessControlled, IComputer, ExecutorListener, DescriptorByNameOwner, StaplerProxy, HasWidgets {
private final CopyOnWriteArrayList executors = new CopyOnWriteArrayList<>();
// TODO:
@@ -351,12 +351,6 @@ public AnnotatedLargeText getLogText() {
return new AnnotatedLargeText<>(getLogFile(), Charset.defaultCharset(), false, this);
}
- @NonNull
- @Override
- public ACL getACL() {
- return Jenkins.get().getAuthorizationStrategy().getACL(this);
- }
-
/**
* If the computer was offline (either temporarily or not),
* this method will return the cause.
@@ -369,14 +363,13 @@ public OfflineCause getOfflineCause() {
return offlineCause;
}
- /**
- * If the computer was offline (either temporarily or not),
- * this method will return the cause as a string (without user info).
- *
- * @return
- * empty string if the system was put offline without given a cause.
- */
+ @Override
+ public boolean hasOfflineCause() {
+ return offlineCause != null;
+ }
+
@Exported
+ @Override
public String getOfflineCauseReason() {
if (offlineCause == null) {
return "";
@@ -581,9 +574,6 @@ public int getNumExecutors() {
return numExecutors;
}
- /**
- * Returns {@link Node#getNodeName() the name of the node}.
- */
public @NonNull String getName() {
return nodeName != null ? nodeName : "";
}
@@ -628,6 +618,7 @@ public BuildTimelineWidget getTimeline() {
}
@Exported
+ @Override
public boolean isOffline() {
return temporarilyOffline || getChannel() == null;
}
@@ -645,12 +636,6 @@ public boolean isManualLaunchAllowed() {
return getRetentionStrategy().isManualLaunchAllowed(this);
}
-
- /**
- * Is a {@link #connect(boolean)} operation in progress?
- */
- public abstract boolean isConnecting();
-
/**
* Returns true if this computer is supposed to be launched via inbound protocol.
* @deprecated since 2008-05-18.
@@ -662,14 +647,8 @@ public boolean isJnlpAgent() {
return false;
}
- /**
- * Returns true if this computer can be launched by Hudson proactively and automatically.
- *
- *
- * For example, inbound agents return {@code false} from this, because the launch process
- * needs to be initiated from the agent side.
- */
@Exported
+ @Override
public boolean isLaunchSupported() {
return true;
}
@@ -727,14 +706,8 @@ public void setTemporarilyOffline(boolean temporarilyOffline, OfflineCause cause
}
}
- /**
- * Returns the icon for this computer.
- *
- * It is both the recommended and default implementation to serve different icons based on {@link #isOffline}
- *
- * @see #getIconClassName()
- */
@Exported
+ @Override
public String getIcon() {
// The machine was taken offline by someone
if (isTemporarilyOffline() && getOfflineCause() instanceof OfflineCause.UserCause) return "symbol-computer-disconnected";
@@ -748,19 +721,15 @@ public String getIcon() {
}
/**
- * Returns the class name that will be used to lookup the icon.
+ * {@inheritDoc}
*
- * This class name will be added as a class tag to the html img tags where the icon should
- * show up followed by a size specifier given by {@link Icon#toNormalizedIconSizeClass(String)}
- * The conversion of class tag to src tag is registered through {@link IconSet#addIcon(Icon)}
- *
- * It is both the recommended and default implementation to serve different icons based on {@link #isOffline}
- *
- * @see #getIcon()
+ *
+ * It is both the recommended and default implementation to serve different icons based on {@link #isOffline}.
*/
@Exported
+ @Override
public String getIconClassName() {
- return getIcon();
+ return IComputer.super.getIconClassName();
}
public String getIconAltText() {
@@ -780,6 +749,8 @@ public String getCaption() {
return Messages.Computer_Caption(nodeName);
}
+ @Override
+ @NonNull
public String getUrl() {
return "computer/" + Util.fullEncode(getName()) + "/";
}
@@ -947,19 +918,18 @@ public int countIdle() {
return n;
}
- /**
- * Returns the number of {@link Executor}s that are doing some work right now.
- */
+ @Override
public final int countBusy() {
return countExecutors() - countIdle();
}
/**
- * Returns the current size of the executor pool for this computer.
+ * {@inheritDoc}
* This number may temporarily differ from {@link #getNumExecutors()} if there
* are busy tasks when the configured size is decreased. OneOffExecutors are
* not included in this count.
*/
+ @Override
public final int countExecutors() {
return executors.size();
}
@@ -996,14 +966,14 @@ public List getAllExecutors() {
}
/**
- * Used to render the list of executors.
- * @return a snapshot of the executor display information
+ * {@inheritDoc}
* @since 1.607
*/
- @Restricted(NoExternalUse.class)
- public List getDisplayExecutors() {
+ @Override
+ @NonNull
+ public List getDisplayExecutors() {
// The size may change while we are populating, but let's start with a reasonable guess to minimize resizing
- List result = new ArrayList<>(executors.size() + oneOffExecutors.size());
+ List result = new ArrayList<>(executors.size() + oneOffExecutors.size());
int index = 0;
for (Executor e : executors) {
if (e.isDisplayCell()) {
@@ -1659,15 +1629,8 @@ public Object getTarget() {
return e != null ? e.getOwner() : null;
}
- /**
- * Returns {@code true} if the computer is accepting tasks. Needed to allow agents programmatic suspension of task
- * scheduling that does not overlap with being offline.
- *
- * @return {@code true} if the computer is accepting tasks
- * @see hudson.slaves.RetentionStrategy#isAcceptingTasks(Computer)
- * @see hudson.model.Node#isAcceptingTasks()
- */
@OverrideMustInvoke
+ @Override
public boolean isAcceptingTasks() {
final Node node = getNode();
return getRetentionStrategy().isAcceptingTasks(this) && (node == null || node.isAcceptingTasks());
@@ -1727,79 +1690,12 @@ public static void relocateOldLogs() {
}
}
- /**
- * A value class to provide a consistent snapshot view of the state of an executor to avoid race conditions
- * during rendering of the executors list.
- *
- * @since 1.607
- */
- @Restricted(NoExternalUse.class)
- public static class DisplayExecutor implements ModelObject {
-
- @NonNull
- private final String displayName;
- @NonNull
- private final String url;
- @NonNull
- private final Executor executor;
-
- public DisplayExecutor(@NonNull String displayName, @NonNull String url, @NonNull Executor executor) {
- this.displayName = displayName;
- this.url = url;
- this.executor = executor;
- }
-
- @Override
- @NonNull
- public String getDisplayName() {
- return displayName;
- }
-
- @NonNull
- public String getUrl() {
- return url;
- }
-
- @NonNull
- public Executor getExecutor() {
- return executor;
- }
-
- @Override
- public String toString() {
- String sb = "DisplayExecutor{" + "displayName='" + displayName + '\'' +
- ", url='" + url + '\'' +
- ", executor=" + executor +
- '}';
- return sb;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
-
- DisplayExecutor that = (DisplayExecutor) o;
-
- return executor.equals(that.executor);
- }
-
- @Extension(ordinal = Double.MAX_VALUE)
- @Restricted(DoNotUse.class)
- public static class InternalComputerListener extends ComputerListener {
- @Override
- public void onOnline(Computer c, TaskListener listener) throws IOException, InterruptedException {
- c.cachedEnvironment = null;
- }
- }
-
+ @Extension(ordinal = Double.MAX_VALUE)
+ @Restricted(DoNotUse.class)
+ public static class InternalComputerListener extends ComputerListener {
@Override
- public int hashCode() {
- return executor.hashCode();
+ public void onOnline(Computer c, TaskListener listener) throws IOException, InterruptedException {
+ c.cachedEnvironment = null;
}
}
diff --git a/core/src/main/java/hudson/model/ComputerSet.java b/core/src/main/java/hudson/model/ComputerSet.java
index f25d25a19a50c..fcc0aa4e41e73 100644
--- a/core/src/main/java/hudson/model/ComputerSet.java
+++ b/core/src/main/java/hudson/model/ComputerSet.java
@@ -30,6 +30,8 @@
import hudson.BulkChange;
import hudson.DescriptorExtensionList;
import hudson.Extension;
+import hudson.ExtensionList;
+import hudson.ExtensionPoint;
import hudson.Util;
import hudson.XmlFile;
import hudson.init.Initializer;
@@ -47,18 +49,24 @@
import java.lang.reflect.InvocationTargetException;
import java.util.AbstractList;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
+import jenkins.model.IComputer;
import jenkins.model.Jenkins;
import jenkins.model.ModelObjectWithChildren;
import jenkins.model.ModelObjectWithContextMenu.ContextMenu;
import jenkins.util.Timer;
import jenkins.widgets.HasWidgets;
import net.sf.json.JSONObject;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.Beta;
+import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest2;
@@ -106,15 +114,36 @@ public static List get_monitors() {
return monitors.toList();
}
- @Exported(name = "computer", inline = true)
+ /**
+ * @deprecated Use {@link #getComputers()} instead.
+ * @return All {@link Computer} instances managed by this set.
+ */
+ @Deprecated(since = "2.480")
public Computer[] get_all() {
- return Jenkins.get().getComputers();
+ return getComputers().stream().filter(Computer.class::isInstance).toArray(Computer[]::new);
+ }
+
+ /**
+ * @return All {@link IComputer} instances managed by this set, sorted by name.
+ */
+ @Exported(name = "computer", inline = true)
+ public Collection extends IComputer> getComputers() {
+ return ExtensionList.lookupFirst(ComputerSource.class).get().stream().sorted(Comparator.comparing(IComputer::getName)).toList();
+ }
+
+ /**
+ * Allows plugins to override the displayed list of computers.
+ *
+ */
+ @Restricted(Beta.class)
+ public interface ComputerSource extends ExtensionPoint {
+ Collection extends IComputer> get();
}
@Override
public ContextMenu doChildrenContextMenu(StaplerRequest2 request, StaplerResponse2 response) throws Exception {
ContextMenu m = new ContextMenu();
- for (Computer c : get_all()) {
+ for (IComputer c : getComputers()) {
m.add(c);
}
return m;
@@ -170,7 +199,7 @@ public int size() {
@Exported
public int getTotalExecutors() {
int r = 0;
- for (Computer c : get_all()) {
+ for (IComputer c : getComputers()) {
if (c.isOnline())
r += c.countExecutors();
}
@@ -183,7 +212,7 @@ public int getTotalExecutors() {
@Exported
public int getBusyExecutors() {
int r = 0;
- for (Computer c : get_all()) {
+ for (IComputer c : getComputers()) {
if (c.isOnline())
r += c.countBusy();
}
@@ -195,7 +224,7 @@ public int getBusyExecutors() {
*/
public int getIdleExecutors() {
int r = 0;
- for (Computer c : get_all())
+ for (IComputer c : getComputers())
if ((c.isOnline() || c.isConnecting()) && c.isAcceptingTasks())
r += c.countIdle();
return r;
@@ -214,7 +243,7 @@ public Computer getDynamic(String token, StaplerRequest2 req, StaplerResponse2 r
public void do_launchAll(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException {
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
- for (Computer c : get_all()) {
+ for (IComputer c : getComputers()) {
if (c.isLaunchSupported())
c.connect(true);
}
@@ -502,4 +531,13 @@ private static NodeMonitor createDefaultInstance(Descriptor d, bool
}
return null;
}
+
+ @Extension(ordinal = -1)
+ @Restricted(DoNotUse.class)
+ public static class ComputerSourceImpl implements ComputerSource {
+ @Override
+ public Collection extends IComputer> get() {
+ return Jenkins.get().getComputersCollection();
+ }
+ }
}
diff --git a/core/src/main/java/hudson/model/Executor.java b/core/src/main/java/hudson/model/Executor.java
index 94aa1c770d218..f86dbd7c6c0a0 100644
--- a/core/src/main/java/hudson/model/Executor.java
+++ b/core/src/main/java/hudson/model/Executor.java
@@ -61,6 +61,7 @@
import java.util.stream.Collectors;
import jenkins.model.CauseOfInterruption;
import jenkins.model.CauseOfInterruption.UserInterruption;
+import jenkins.model.IExecutor;
import jenkins.model.InterruptedBuildAction;
import jenkins.model.Jenkins;
import jenkins.model.queue.AsynchronousExecution;
@@ -88,7 +89,7 @@
* @author Kohsuke Kawaguchi
*/
@ExportedBean
-public class Executor extends Thread implements ModelObject {
+public class Executor extends Thread implements ModelObject, IExecutor {
protected final @NonNull Computer owner;
private final Queue queue;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
@@ -526,6 +527,7 @@ public void completedAsynchronous(@CheckForNull Throwable error) {
* @return
* null if the executor is idle.
*/
+ @Override
public @CheckForNull Queue.Executable getCurrentExecutable() {
lock.readLock().lock();
try {
@@ -555,14 +557,8 @@ public Queue.Executable getCurrentExecutableForApi() {
return Collections.unmodifiableCollection(causes);
}
- /**
- * Returns the current {@link WorkUnit} (of {@link #getCurrentExecutable() the current executable})
- * that this executor is running.
- *
- * @return
- * null if the executor is idle.
- */
@CheckForNull
+ @Override
public WorkUnit getCurrentWorkUnit() {
lock.readLock().lock();
try {
@@ -601,22 +597,14 @@ public String getDisplayName() {
return "Executor #" + getNumber();
}
- /**
- * Gets the executor number that uniquely identifies it among
- * other {@link Executor}s for the same computer.
- *
- * @return
- * a sequential number starting from 0.
- */
@Exported
+ @Override
public int getNumber() {
return number;
}
- /**
- * Returns true if this {@link Executor} is ready for action.
- */
@Exported
+ @Override
public boolean isIdle() {
lock.readLock().lock();
try {
@@ -705,13 +693,8 @@ public boolean isParking() {
return null;
}
- /**
- * Returns the progress of the current build in the number between 0-100.
- *
- * @return -1
- * if it's impossible to estimate the progress.
- */
@Exported
+ @Override
public int getProgress() {
long d = executableEstimatedDuration;
if (d <= 0) {
@@ -725,14 +708,8 @@ public int getProgress() {
return num;
}
- /**
- * Returns true if the current build is likely stuck.
- *
- *
- * This is a heuristics based approach, but if the build is suspiciously taking for a long time,
- * this method returns true.
- */
@Exported
+ @Override
public boolean isLikelyStuck() {
lock.readLock().lock();
try {
@@ -754,6 +731,7 @@ public boolean isLikelyStuck() {
}
}
+ @Override
public long getElapsedTime() {
lock.readLock().lock();
try {
@@ -777,20 +755,7 @@ public long getTimeSpentInQueue() {
}
}
- /**
- * Gets the string that says how long since this build has started.
- *
- * @return
- * string like "3 minutes" "1 day" etc.
- */
- public String getTimestampString() {
- return Util.getTimeSpanString(getElapsedTime());
- }
-
- /**
- * Computes a human-readable text that shows the expected remaining time
- * until the build completes.
- */
+ @Override
public String getEstimatedRemainingTime() {
long d = executableEstimatedDuration;
if (d < 0) {
@@ -911,9 +876,7 @@ public HttpResponse doYank() {
return HttpResponses.redirectViaContextPath("/");
}
- /**
- * Checks if the current user has a permission to stop this build.
- */
+ @Override
public boolean hasStopPermission() {
lock.readLock().lock();
try {
diff --git a/core/src/main/java/hudson/model/ItemGroupMixIn.java b/core/src/main/java/hudson/model/ItemGroupMixIn.java
index 404d213f73449..89945267edc54 100644
--- a/core/src/main/java/hudson/model/ItemGroupMixIn.java
+++ b/core/src/main/java/hudson/model/ItemGroupMixIn.java
@@ -31,7 +31,6 @@
import hudson.security.AccessControlled;
import hudson.util.CopyOnWriteMap;
import hudson.util.Function1;
-import hudson.util.Secret;
import io.jenkins.servlet.ServletExceptionWrapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletResponse;
@@ -44,11 +43,11 @@
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
-import java.util.regex.Matcher;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import jenkins.model.Jenkins;
+import jenkins.security.ExtendedReadRedaction;
import jenkins.security.NotReallyRoleSensitiveCallable;
import jenkins.util.xml.XMLUtils;
import org.kohsuke.stapler.StaplerRequest;
@@ -239,18 +238,17 @@ public synchronized T copy(T src, String name) throws I
src.checkPermission(Item.EXTENDED_READ);
XmlFile srcConfigFile = Items.getConfigFile(src);
if (!src.hasPermission(Item.CONFIGURE)) {
- Matcher matcher = AbstractItem.SECRET_PATTERN.matcher(srcConfigFile.asString());
- while (matcher.find()) {
- if (Secret.decrypt(matcher.group(1)) != null) {
- // AccessDeniedException2 does not permit a custom message, and anyway redirecting the user to the login screen is obviously pointless.
- throw new AccessDeniedException(
- Messages.ItemGroupMixIn_may_not_copy_as_it_contains_secrets_and_(
- src.getFullName(),
- Jenkins.getAuthentication2().getName(),
- Item.PERMISSIONS.title,
- Item.EXTENDED_READ.name,
- Item.CONFIGURE.name));
- }
+ final String originalConfigDotXml = srcConfigFile.asString();
+ final String redactedConfigDotXml = ExtendedReadRedaction.applyAll(originalConfigDotXml);
+ if (!originalConfigDotXml.equals(redactedConfigDotXml)) {
+ // AccessDeniedException2 does not permit a custom message, and anyway redirecting the user to the login screen is obviously pointless.
+ throw new AccessDeniedException(
+ Messages.ItemGroupMixIn_may_not_copy_as_it_contains_secrets_and_(
+ src.getFullName(),
+ Jenkins.getAuthentication2().getName(),
+ Item.PERMISSIONS.title,
+ Item.EXTENDED_READ.name,
+ Item.CONFIGURE.name));
}
}
src.getDescriptor().checkApplicableIn(parent);
@@ -302,8 +300,17 @@ public synchronized TopLevelItem createProjectFromXML(String name, InputStream x
}
});
- success = acl.getACL().hasCreatePermission2(Jenkins.getAuthentication2(), parent, result.getDescriptor())
- && result.getDescriptor().isApplicableIn(parent);
+ boolean hasCreatePermission = acl.getACL().hasCreatePermission2(Jenkins.getAuthentication2(), parent, result.getDescriptor());
+ boolean applicableIn = result.getDescriptor().isApplicableIn(parent);
+
+ success = hasCreatePermission && applicableIn;
+
+ if (!hasCreatePermission) {
+ throw new AccessDeniedException(Jenkins.getAuthentication2().getName() + " does not have required permissions to create " + result.getDescriptor().clazz.getName());
+ }
+ if (!applicableIn) {
+ throw new AccessDeniedException(result.getDescriptor().clazz.getName() + " is not applicable in " + parent.getFullName());
+ }
add(result);
diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java
index 08eba90b906ca..797cba56f3942 100644
--- a/core/src/main/java/hudson/model/Queue.java
+++ b/core/src/main/java/hudson/model/Queue.java
@@ -1956,24 +1956,6 @@ default void checkAbortPermission() {
}
}
- /**
- * Works just like {@link #checkAbortPermission()} except it indicates the status by a return value,
- * instead of exception.
- * Also used by default for {@link hudson.model.Queue.Item#hasCancelPermission}.
- *
- * NOTE: If you have implemented {@link AccessControlled} this returns by default
- * {@code return hasPermission(hudson.model.Item.CANCEL);}
- *
- * @return false
- * if the user doesn't have the permission.
- */
- default boolean hasAbortPermission() {
- if (this instanceof AccessControlled) {
- return ((AccessControlled) this).hasPermission(CANCEL);
- }
- return true;
- }
-
/**
* Returns the URL of this task relative to the context root of the application.
*
@@ -1984,6 +1966,7 @@ default boolean hasAbortPermission() {
* @return
* URL that ends with '/'.
*/
+ @Override
String getUrl();
/**
diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java
index 37b09fd47d25f..ec7eaabf32c60 100644
--- a/core/src/main/java/hudson/model/Run.java
+++ b/core/src/main/java/hudson/model/Run.java
@@ -1551,6 +1551,9 @@ public synchronized void deleteArtifacts() throws IOException {
* if we fail to delete.
*/
public void delete() throws IOException {
+ if (isLogUpdated()) {
+ throw new IOException("Unable to delete " + this + " because it is still running");
+ }
synchronized (this) {
// Avoid concurrent delete. See https://issues.jenkins.io/browse/JENKINS-61687
if (isPendingDelete) {
@@ -1570,6 +1573,7 @@ public void delete() throws IOException {
));
//Still firing the delete listeners; just no need to clean up rootDir
RunListener.fireDeleted(this);
+ SaveableListener.fireOnDeleted(this, getDataFile());
synchronized (this) { // avoid holding a lock while calling plugin impls of onDeleted
removeRunFromParent();
}
@@ -1578,6 +1582,7 @@ public void delete() throws IOException {
//The root dir exists and is a directory that needs to be purged
RunListener.fireDeleted(this);
+ SaveableListener.fireOnDeleted(this, getDataFile());
if (artifactManager != null) {
deleteArtifacts();
@@ -1883,12 +1888,6 @@ protected final void execute(@NonNull RunExecution job) {
LOGGER.log(Level.SEVERE, "Failed to save build record", e);
}
}
-
- try {
- getParent().logRotate();
- } catch (Exception e) {
- LOGGER.log(Level.SEVERE, "Failed to rotate log", e);
- }
} finally {
onEndBuilding();
if (logger != null) {
diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java
index 8a38ef2d470b7..0e3cc894148af 100644
--- a/core/src/main/java/hudson/model/UpdateSite.java
+++ b/core/src/main/java/hudson/model/UpdateSite.java
@@ -538,14 +538,17 @@ public String getDownloadUrl() {
/**
* Is this the legacy default update center site?
- * @deprecated
- * Will be removed, currently returns always false.
- * @since 2.343
+ * @since 1.357
*/
- @Deprecated
@Restricted(NoExternalUse.class)
public boolean isLegacyDefault() {
- return false;
+ return isJenkinsCI();
+ }
+
+ private boolean isJenkinsCI() {
+ return url != null
+ && UpdateCenter.PREDEFINED_UPDATE_SITE_ID.equals(id)
+ && url.startsWith("http://updates.jenkins-ci.org/");
}
/**
diff --git a/core/src/main/java/hudson/model/View.java b/core/src/main/java/hudson/model/View.java
index 5d53a49ef0b7d..b17077cde44d5 100644
--- a/core/src/main/java/hudson/model/View.java
+++ b/core/src/main/java/hudson/model/View.java
@@ -949,9 +949,28 @@ public void doRssLatest(StaplerRequest2 req, StaplerResponse2 rsp) throws IOExce
/**
* Accepts {@code config.xml} submission, as well as serve it.
+ *
+ * @since 2.475
*/
@WebMethod(name = "config.xml")
public HttpResponse doConfigDotXml(StaplerRequest2 req) throws IOException {
+ if (Util.isOverridden(View.class, getClass(), "doConfigDotXml", StaplerRequest.class)) {
+ return doConfigDotXml(StaplerRequest.fromStaplerRequest2(req));
+ } else {
+ return doConfigDotXmlImpl(req);
+ }
+ }
+
+ /**
+ * @deprecated use {@link #doConfigDotXml(StaplerRequest2)}
+ */
+ @Deprecated
+ @StaplerNotDispatchable
+ public HttpResponse doConfigDotXml(StaplerRequest req) throws IOException {
+ return doConfigDotXmlImpl(StaplerRequest.toStaplerRequest2(req));
+ }
+
+ private HttpResponse doConfigDotXmlImpl(StaplerRequest2 req) throws IOException {
if (req.getMethod().equals("GET")) {
// read
checkPermission(READ);
diff --git a/core/src/main/java/hudson/model/listeners/SaveableListener.java b/core/src/main/java/hudson/model/listeners/SaveableListener.java
index 46bbc6ab60be5..14877d080bf53 100644
--- a/core/src/main/java/hudson/model/listeners/SaveableListener.java
+++ b/core/src/main/java/hudson/model/listeners/SaveableListener.java
@@ -30,8 +30,7 @@
import hudson.ExtensionPoint;
import hudson.XmlFile;
import hudson.model.Saveable;
-import java.util.logging.Level;
-import java.util.logging.Logger;
+import jenkins.util.Listeners;
/**
* Receives notifications about save actions on {@link Saveable} objects in Hudson.
@@ -54,6 +53,17 @@ public abstract class SaveableListener implements ExtensionPoint {
*/
public void onChange(Saveable o, XmlFile file) {}
+ /**
+ * Called when a {@link Saveable} object gets deleted.
+ *
+ * @param o
+ * The saveable object.
+ * @param file
+ * The {@link XmlFile} for this saveable object.
+ * @since 2.480
+ */
+ public void onDeleted(Saveable o, XmlFile file) {}
+
/**
* Registers this object as an active listener so that it can start getting
* callbacks invoked.
@@ -77,13 +87,15 @@ public void unregister() {
* Fires the {@link #onChange} event.
*/
public static void fireOnChange(Saveable o, XmlFile file) {
- for (SaveableListener l : all()) {
- try {
- l.onChange(o, file);
- } catch (Throwable t) {
- Logger.getLogger(SaveableListener.class.getName()).log(Level.WARNING, null, t);
- }
- }
+ Listeners.notify(SaveableListener.class, false, l -> l.onChange(o, file));
+ }
+
+ /**
+ * Fires the {@link #onDeleted} event.
+ * @since 2.480
+ */
+ public static void fireOnDeleted(Saveable o, XmlFile file) {
+ Listeners.notify(SaveableListener.class, false, l -> l.onDeleted(o, file));
}
/**
diff --git a/core/src/main/java/hudson/model/queue/SubTask.java b/core/src/main/java/hudson/model/queue/SubTask.java
index 0690d074617c4..9a971d9ca40b7 100644
--- a/core/src/main/java/hudson/model/queue/SubTask.java
+++ b/core/src/main/java/hudson/model/queue/SubTask.java
@@ -33,20 +33,16 @@
import hudson.model.Queue;
import hudson.model.ResourceActivity;
import java.io.IOException;
+import jenkins.model.queue.ITask;
/**
* A component of {@link Queue.Task} that represents a computation carried out by a single {@link Executor}.
*
* A {@link Queue.Task} consists of a number of {@link SubTask}.
*
- *
- * Plugins are encouraged to extend from {@link AbstractSubTask}
- * instead of implementing this interface directly, to maintain
- * compatibility with future changes to this interface.
- *
* @since 1.377
*/
-public interface SubTask extends ResourceActivity {
+public interface SubTask extends ResourceActivity, ITask {
/**
* If this task needs to be run on a node with a particular label,
* return that {@link Label}. Otherwise null, indicating
@@ -115,4 +111,13 @@ default long getEstimatedDuration() {
default Object getSameNodeConstraint() {
return null;
}
+
+ /**
+ * A subtask may not be reachable by its own URL. In that case, this method should return null.
+ * @return the URL where to reach specifically this subtask, relative to Jenkins URL. If non-null, must end with '/'.
+ */
+ @Override
+ default String getUrl() {
+ return null;
+ }
}
diff --git a/core/src/main/java/hudson/security/AuthorizationStrategy.java b/core/src/main/java/hudson/security/AuthorizationStrategy.java
index db3001fc40f2c..5ca218f6a8fdb 100644
--- a/core/src/main/java/hudson/security/AuthorizationStrategy.java
+++ b/core/src/main/java/hudson/security/AuthorizationStrategy.java
@@ -43,6 +43,7 @@
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
+import jenkins.model.IComputer;
import jenkins.model.Jenkins;
import jenkins.security.stapler.StaplerAccessibleType;
import net.sf.json.JSONObject;
@@ -154,6 +155,22 @@ public abstract class AuthorizationStrategy extends AbstractDescribableImpl
+ * Default implementation delegates to {@link #getACL(Computer)} if the computer is an instance of {@link Computer},
+ * otherwise it will fall back to {@link #getRootACL()}.
+ *
+ * @since 2.480
+ **/
+ public @NonNull ACL getACL(@NonNull IComputer computer) {
+ if (computer instanceof Computer c) {
+ return getACL(c);
+ }
+ return getRootACL();
+ }
+
/**
* Implementation can choose to provide different ACL for different {@link Cloud}s.
* This can be used as a basis for more fine-grained access control.
diff --git a/core/src/main/java/hudson/tasks/LogRotator.java b/core/src/main/java/hudson/tasks/LogRotator.java
index ca95081b0385f..0b47f035d8096 100644
--- a/core/src/main/java/hudson/tasks/LogRotator.java
+++ b/core/src/main/java/hudson/tasks/LogRotator.java
@@ -250,7 +250,7 @@ private boolean shouldKeepRun(Run r, Run lsb, Run lstb) {
LOGGER.log(FINER, "{0} is not to be removed or purged of artifacts because it’s the last stable build", r);
return true;
}
- if (r.isBuilding()) {
+ if (r.isLogUpdated()) {
LOGGER.log(FINER, "{0} is not to be removed or purged of artifacts because it’s still building", r);
return true;
}
diff --git a/core/src/main/java/hudson/util/RobustCollectionConverter.java b/core/src/main/java/hudson/util/RobustCollectionConverter.java
index 64dbbc7d9e9ad..f914d909be27a 100644
--- a/core/src/main/java/hudson/util/RobustCollectionConverter.java
+++ b/core/src/main/java/hudson/util/RobustCollectionConverter.java
@@ -26,6 +26,7 @@
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.XStreamException;
+import com.thoughtworks.xstream.converters.ConversionException;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.converters.collections.CollectionConverter;
import com.thoughtworks.xstream.converters.reflection.ReflectionProvider;
@@ -34,11 +35,15 @@
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.mapper.Mapper;
import com.thoughtworks.xstream.security.InputManipulationException;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import hudson.diagnosis.OldDataMonitor;
+import java.lang.reflect.Type;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Logger;
import jenkins.util.xstream.CriticalXStreamException;
+import org.jvnet.tiger_types.Types;
/**
* {@link CollectionConverter} that ignores {@link XStreamException}.
@@ -52,14 +57,39 @@
@SuppressWarnings({"rawtypes", "unchecked"})
public class RobustCollectionConverter extends CollectionConverter {
private final SerializableConverter sc;
+ /**
+ * When available, this field holds the declared type of the collection being deserialized.
+ */
+ private final @CheckForNull Class> elementType;
public RobustCollectionConverter(XStream xs) {
- this(xs.getMapper(), xs.getReflectionProvider());
+ this(xs.getMapper(), xs.getReflectionProvider(), null);
}
public RobustCollectionConverter(Mapper mapper, ReflectionProvider reflectionProvider) {
+ this(mapper, reflectionProvider, null);
+ }
+
+ /**
+ * Creates a converter that will validate the types of collection elements during deserialization.
+ * Elements with invalid types will be omitted from deserialized collections and may result in an
+ * {@link OldDataMonitor} warning.
+ *
This type checking currently uses the erasure of the type argument, so for example, the element type for a
+ * {@code List>} is just a raw {@code Optional}, so non-integer values inside of the optional
+ * would still deserialize successfully and the resulting optional would be included in the list.
+ *
+ * @see RobustReflectionConverter#unmarshalField
+ */
+ public RobustCollectionConverter(Mapper mapper, ReflectionProvider reflectionProvider, Type collectionType) {
super(mapper);
sc = new SerializableConverter(mapper, reflectionProvider, new ClassLoaderReference(null));
+ if (collectionType != null && Collection.class.isAssignableFrom(Types.erasure(collectionType))) {
+ var baseType = Types.getBaseClass(collectionType, Collection.class);
+ var typeArg = Types.getTypeArgument(baseType, 0, Object.class);
+ this.elementType = Types.erasure(typeArg);
+ } else {
+ this.elementType = null;
+ }
}
@Override
@@ -85,9 +115,19 @@ protected void populateCollection(HierarchicalStreamReader reader, Unmarshalling
reader.moveDown();
try {
Object item = readBareItem(reader, context, collection);
- long nanoNow = System.nanoTime();
- collection.add(item);
- XStream2SecurityUtils.checkForCollectionDoSAttack(context, nanoNow);
+ if (elementType != null && item != null && !elementType.isInstance(item)) {
+ var exception = new ConversionException("Invalid type for collection element");
+ // c.f. TreeUnmarshaller.addInformationTo
+ exception.add("required-type", elementType.getName());
+ exception.add("class", item.getClass().getName());
+ exception.add("converter-type", getClass().getName());
+ reader.appendErrors(exception);
+ RobustReflectionConverter.addErrorInContext(context, exception);
+ } else {
+ long nanoNow = System.nanoTime();
+ collection.add(item);
+ XStream2SecurityUtils.checkForCollectionDoSAttack(context, nanoNow);
+ }
} catch (CriticalXStreamException e) {
throw e;
} catch (InputManipulationException e) {
diff --git a/core/src/main/java/hudson/util/RobustMapConverter.java b/core/src/main/java/hudson/util/RobustMapConverter.java
index f845e38771ccd..c802959d0d096 100644
--- a/core/src/main/java/hudson/util/RobustMapConverter.java
+++ b/core/src/main/java/hudson/util/RobustMapConverter.java
@@ -31,9 +31,13 @@
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.mapper.Mapper;
import com.thoughtworks.xstream.security.InputManipulationException;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import hudson.diagnosis.OldDataMonitor;
+import java.lang.reflect.Type;
import java.util.Map;
import java.util.logging.Logger;
import jenkins.util.xstream.CriticalXStreamException;
+import org.jvnet.tiger_types.Types;
/**
* Loads a {@link Map} while tolerating read errors on its keys and values.
@@ -42,13 +46,47 @@
final class RobustMapConverter extends MapConverter {
private static final Object ERROR = new Object();
+ /**
+ * When available, this field holds the declared type of the keys of the map being deserialized.
+ */
+ private final @CheckForNull Class> keyType;
+
+ /**
+ * When available, this field holds the declared type of the values of the map being deserialized.
+ */
+ private final @CheckForNull Class> valueType;
+
RobustMapConverter(Mapper mapper) {
+ this(mapper, null);
+ }
+
+ /**
+ * Creates a converter that will validate the types of map entry keys and values during deserialization.
+ * Map entries whose key or value has an invalid type will be omitted from deserialized maps and may result in
+ * an {@link OldDataMonitor} warning.
+ *
This type checking currently uses the erasure of the type argument, so for example, the value type for a
+ * {@code Map>} is just a raw {@code Optional}, so non-integer values inside of the
+ * optional would still deserialize successfully and the resulting map entry would be included in the map.
+ *
+ * @see RobustReflectionConverter#unmarshalField
+ */
+ RobustMapConverter(Mapper mapper, Type mapType) {
super(mapper);
+ if (mapType != null && Map.class.isAssignableFrom(Types.erasure(mapType))) {
+ var baseType = Types.getBaseClass(mapType, Map.class);
+ var keyTypeArg = Types.getTypeArgument(baseType, 0, Object.class);
+ this.keyType = Types.erasure(keyTypeArg);
+ var valueTypeArg = Types.getTypeArgument(baseType, 1, Object.class);
+ this.valueType = Types.erasure(valueTypeArg);
+ } else {
+ this.keyType = null;
+ this.valueType = null;
+ }
}
@Override protected void putCurrentEntryIntoMap(HierarchicalStreamReader reader, UnmarshallingContext context, Map map, Map target) {
- Object key = read(reader, context, map);
- Object value = read(reader, context, map);
+ Object key = read(reader, context, map, keyType);
+ Object value = read(reader, context, map, valueType);
if (key != ERROR && value != ERROR) {
try {
long nanoNow = System.nanoTime();
@@ -64,7 +102,7 @@ final class RobustMapConverter extends MapConverter {
}
}
- private Object read(HierarchicalStreamReader reader, UnmarshallingContext context, Map map) {
+ private Object read(HierarchicalStreamReader reader, UnmarshallingContext context, Map map, @CheckForNull Class> expectedType) {
if (!reader.hasMoreChildren()) {
var exception = new ConversionException("Invalid map entry");
reader.appendErrors(exception);
@@ -73,7 +111,18 @@ private Object read(HierarchicalStreamReader reader, UnmarshallingContext contex
}
reader.moveDown();
try {
- return readBareItem(reader, context, map);
+ var object = readBareItem(reader, context, map);
+ if (expectedType != null && object != null && !expectedType.isInstance(object)) {
+ var exception = new ConversionException("Invalid type for map entry key/value");
+ // c.f. TreeUnmarshaller.addInformationTo
+ exception.add("required-type", expectedType.getName());
+ exception.add("class", object.getClass().getName());
+ exception.add("converter-type", getClass().getName());
+ reader.appendErrors(exception);
+ RobustReflectionConverter.addErrorInContext(context, exception);
+ return ERROR;
+ }
+ return object;
} catch (CriticalXStreamException x) {
throw x;
} catch (XStreamException | LinkageError x) {
diff --git a/core/src/main/java/hudson/util/RobustReflectionConverter.java b/core/src/main/java/hudson/util/RobustReflectionConverter.java
index d1bc500003e12..686aad13c342a 100644
--- a/core/src/main/java/hudson/util/RobustReflectionConverter.java
+++ b/core/src/main/java/hudson/util/RobustReflectionConverter.java
@@ -48,6 +48,7 @@
import hudson.model.Saveable;
import hudson.security.ACL;
import java.lang.reflect.Field;
+import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -65,6 +66,7 @@
import jenkins.util.xstream.CriticalXStreamException;
import net.jcip.annotations.GuardedBy;
import org.acegisecurity.Authentication;
+import org.jvnet.tiger_types.Types;
/**
* Custom {@link ReflectionConverter} that handle errors more gracefully.
@@ -80,7 +82,7 @@
@SuppressWarnings({"rawtypes", "unchecked"})
public class RobustReflectionConverter implements Converter {
- private static /* non-final for Groovy */ boolean RECORD_FAILURES_FOR_ALL_AUTHENTICATIONS = SystemProperties.getBoolean(RobustReflectionConverter.class.getName() + ".recordFailuresForAllAuthentications", false);
+ static /* non-final for Groovy */ boolean RECORD_FAILURES_FOR_ALL_AUTHENTICATIONS = SystemProperties.getBoolean(RobustReflectionConverter.class.getName() + ".recordFailuresForAllAuthentications", false);
private static /* non-final for Groovy */ boolean RECORD_FAILURES_FOR_ADMINS = SystemProperties.getBoolean(RobustReflectionConverter.class.getName() + ".recordFailuresForAdmins", false);
protected final ReflectionProvider reflectionProvider;
@@ -324,7 +326,8 @@ public Object doUnmarshal(final Object result, final HierarchicalStreamReader re
}
}
- Map implicitCollectionsForCurrentObject = null;
+ Map> implicitCollectionsForCurrentObject = new HashMap<>();
+ Map> implicitCollectionElementTypesForCurrentObject = new HashMap<>();
while (reader.hasMoreChildren()) {
reader.moveDown();
@@ -365,7 +368,7 @@ public Object doUnmarshal(final Object result, final HierarchicalStreamReader re
reflectionProvider.writeField(result, fieldName, value, classDefiningField);
seenFields.add(classDefiningField, fieldName);
} else {
- implicitCollectionsForCurrentObject = writeValueToImplicitCollection(context, value, implicitCollectionsForCurrentObject, result, fieldName);
+ writeValueToImplicitCollection(reader, context, value, implicitCollectionsForCurrentObject, implicitCollectionElementTypesForCurrentObject, result, fieldName);
}
}
} catch (CriticalXStreamException e) {
@@ -451,18 +454,23 @@ private boolean fieldDefinedInClass(Object result, String attrName) {
protected Object unmarshalField(final UnmarshallingContext context, final Object result, Class type, Field field) {
Converter converter = mapper.getLocalConverter(field.getDeclaringClass(), field.getName());
+ if (converter == null) {
+ if (new RobustCollectionConverter(mapper, reflectionProvider).canConvert(type)) {
+ converter = new RobustCollectionConverter(mapper, reflectionProvider, field.getGenericType());
+ } else if (new RobustMapConverter(mapper).canConvert(type)) {
+ converter = new RobustMapConverter(mapper, field.getGenericType());
+ }
+ }
return context.convertAnother(result, type, converter);
}
- private Map writeValueToImplicitCollection(UnmarshallingContext context, Object value, Map implicitCollections, Object result, String itemFieldName) {
+ private void writeValueToImplicitCollection(HierarchicalStreamReader reader, UnmarshallingContext context, Object value, Map> implicitCollections, Map> implicitCollectionElementTypes, Object result, String itemFieldName) {
String fieldName = mapper.getFieldNameForItemTypeAndName(context.getRequiredType(), value.getClass(), itemFieldName);
if (fieldName != null) {
- if (implicitCollections == null) {
- implicitCollections = new HashMap(); // lazy instantiation
- }
- Collection collection = (Collection) implicitCollections.get(fieldName);
+ Collection collection = implicitCollections.get(fieldName);
if (collection == null) {
- Class fieldType = mapper.defaultImplementationOf(reflectionProvider.getFieldType(result, fieldName, null));
+ Field field = reflectionProvider.getField(result.getClass(), fieldName);
+ Class> fieldType = mapper.defaultImplementationOf(field.getType());
if (!Collection.class.isAssignableFrom(fieldType)) {
throw new ObjectAccessException("Field " + fieldName + " of " + result.getClass().getName() +
" is configured for an implicit Collection, but field is of type " + fieldType.getName());
@@ -473,10 +481,25 @@ private Map writeValueToImplicitCollection(UnmarshallingContext context, Object
collection = (Collection) pureJavaReflectionProvider.newInstance(fieldType);
reflectionProvider.writeField(result, fieldName, collection, null);
implicitCollections.put(fieldName, collection);
+ Type fieldGenericType = field.getGenericType();
+ Type elementGenericType = Types.getTypeArgument(Types.getBaseClass(fieldGenericType, Collection.class), 0, Object.class);
+ Class> elementType = Types.erasure(elementGenericType);
+ implicitCollectionElementTypes.put(fieldName, elementType);
+ }
+ Class> elementType = implicitCollectionElementTypes.getOrDefault(fieldName, Object.class);
+ if (!elementType.isInstance(value)) {
+ var exception = new ConversionException("Invalid element type for implicit collection for field: " + fieldName);
+ // c.f. TreeUnmarshaller.addInformationTo
+ exception.add("required-type", elementType.getName());
+ exception.add("class", value.getClass().getName());
+ exception.add("converter-type", getClass().getName());
+ reader.appendErrors(exception);
+ throw exception;
}
collection.add(value);
+ } else {
+ // TODO: Should we warn in this case? The value will be ignored.
}
- return implicitCollections;
}
private Class determineWhichClassDefinesField(HierarchicalStreamReader reader) {
diff --git a/core/src/main/java/jenkins/cli/SafeRestartCommand.java b/core/src/main/java/jenkins/cli/SafeRestartCommand.java
index 54c624bbba274..4c1a8009e44dc 100644
--- a/core/src/main/java/jenkins/cli/SafeRestartCommand.java
+++ b/core/src/main/java/jenkins/cli/SafeRestartCommand.java
@@ -32,6 +32,7 @@
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.args4j.Option;
+import org.kohsuke.stapler.StaplerRequest2;
/**
* Safe Restart Jenkins - do not accept any new jobs and try to pause existing.
@@ -53,7 +54,7 @@ public String getShortDescription() {
@Override
protected int run() throws Exception {
- Jenkins.get().doSafeRestart(null, message);
+ Jenkins.get().doSafeRestart((StaplerRequest2) null, message);
return 0;
}
}
diff --git a/core/src/main/java/jenkins/model/BackgroundGlobalBuildDiscarder.java b/core/src/main/java/jenkins/model/BackgroundGlobalBuildDiscarder.java
index 1a42c0577a479..ad33643879f57 100644
--- a/core/src/main/java/jenkins/model/BackgroundGlobalBuildDiscarder.java
+++ b/core/src/main/java/jenkins/model/BackgroundGlobalBuildDiscarder.java
@@ -31,6 +31,7 @@
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
+import java.util.stream.Stream;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@@ -56,8 +57,18 @@ protected void execute(TaskListener listener) throws IOException, InterruptedExc
}
}
+ /**
+ * Runs all globally configured build discarders against a job.
+ */
public static void processJob(TaskListener listener, Job job) {
- GlobalBuildDiscarderConfiguration.get().getConfiguredBuildDiscarders().forEach(strategy -> {
+ processJob(listener, job, GlobalBuildDiscarderConfiguration.get().getConfiguredBuildDiscarders().stream());
+ }
+
+ /**
+ * Runs the specified build discarders against a job.
+ */
+ public static void processJob(TaskListener listener, Job job, Stream strategies) {
+ strategies.forEach(strategy -> {
String displayName = strategy.getDescriptor().getDisplayName();
if (strategy.isApplicable(job)) {
try {
diff --git a/core/src/main/java/jenkins/model/DisplayExecutor.java b/core/src/main/java/jenkins/model/DisplayExecutor.java
new file mode 100644
index 0000000000000..f7559ff85799c
--- /dev/null
+++ b/core/src/main/java/jenkins/model/DisplayExecutor.java
@@ -0,0 +1,97 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2024 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.model;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.model.Executor;
+import hudson.model.ModelObject;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+/**
+ * A value class providing a consistent snapshot view of the state of an executor to avoid race conditions
+ * during rendering of the executors list.
+ */
+@Restricted(NoExternalUse.class)
+public class DisplayExecutor implements ModelObject, IDisplayExecutor {
+
+ @NonNull
+ private final String displayName;
+ @NonNull
+ private final String url;
+ @NonNull
+ private final Executor executor;
+
+ public DisplayExecutor(@NonNull String displayName, @NonNull String url, @NonNull Executor executor) {
+ this.displayName = displayName;
+ this.url = url;
+ this.executor = executor;
+ }
+
+ @Override
+ @NonNull
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ @Override
+ @NonNull
+ public String getUrl() {
+ return url;
+ }
+
+ @Override
+ @NonNull
+ public Executor getExecutor() {
+ return executor;
+ }
+
+ @Override
+ public String toString() {
+ return "DisplayExecutor{" + "displayName='" + displayName + '\'' +
+ ", url='" + url + '\'' +
+ ", executor=" + executor +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ DisplayExecutor that = (DisplayExecutor) o;
+
+ return executor.equals(that.executor);
+ }
+
+ @Override
+ public int hashCode() {
+ return executor.hashCode();
+ }
+}
diff --git a/core/src/main/java/jenkins/model/GlobalBuildDiscarderListener.java b/core/src/main/java/jenkins/model/GlobalBuildDiscarderListener.java
index 7ddea84c4241c..6d2c58b447746 100644
--- a/core/src/main/java/jenkins/model/GlobalBuildDiscarderListener.java
+++ b/core/src/main/java/jenkins/model/GlobalBuildDiscarderListener.java
@@ -35,7 +35,7 @@
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
- * Run background build discarders on an individual job once a build is finalized
+ * Run build discarders on an individual job once a build is finalized
*/
@Extension
@Restricted(NoExternalUse.class)
@@ -46,6 +46,15 @@ public class GlobalBuildDiscarderListener extends RunListener {
@Override
public void onFinalized(Run run) {
Job job = run.getParent();
- BackgroundGlobalBuildDiscarder.processJob(new LogTaskListener(LOGGER, Level.FINE), job);
+ try {
+ // Job-level build discarder execution is unconditional.
+ job.logRotate();
+ } catch (Exception e) {
+ LOGGER.log(Level.WARNING, e, () -> "Failed to rotate log for " + run);
+ }
+ // Avoid calling Job.logRotate twice in case JobGlobalBuildDiscarderStrategy is configured globally.
+ BackgroundGlobalBuildDiscarder.processJob(new LogTaskListener(LOGGER, Level.FINE), job,
+ GlobalBuildDiscarderConfiguration.get().getConfiguredBuildDiscarders().stream()
+ .filter(s -> !(s instanceof JobGlobalBuildDiscarderStrategy)));
}
}
diff --git a/core/src/main/java/jenkins/model/IComputer.java b/core/src/main/java/jenkins/model/IComputer.java
new file mode 100644
index 0000000000000..0708748f43709
--- /dev/null
+++ b/core/src/main/java/jenkins/model/IComputer.java
@@ -0,0 +1,182 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2024 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.model;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.Util;
+import hudson.model.Computer;
+import hudson.model.Node;
+import hudson.security.ACL;
+import hudson.security.AccessControlled;
+import java.util.List;
+import java.util.concurrent.Future;
+import org.jenkins.ui.icon.Icon;
+import org.jenkins.ui.icon.IconSet;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.Beta;
+
+/**
+ * Interface for computer-like objects meant to be passed to {@code t:executors} tag.
+ *
+ * @since 2.480
+ */
+@Restricted(Beta.class)
+public interface IComputer extends AccessControlled {
+ /**
+ * Returns {@link Node#getNodeName() the name of the node}.
+ */
+ @NonNull
+ String getName();
+
+ /**
+ * Used to render the list of executors.
+ * @return a snapshot of the executor display information
+ */
+ @NonNull
+ List extends IDisplayExecutor> getDisplayExecutors();
+
+ /**
+ * @return {@code true} if the node is offline. {@code false} if it is online.
+ */
+ boolean isOffline();
+
+ /**
+ * @return the node name for UI purposes.
+ */
+ @NonNull
+ String getDisplayName();
+
+ /**
+ * Returns {@code true} if the computer is accepting tasks. Needed to allow agents programmatic suspension of task
+ * scheduling that does not overlap with being offline.
+ *
+ * @return {@code true} if the computer is accepting tasks
+ * @see hudson.slaves.RetentionStrategy#isAcceptingTasks(Computer)
+ * @see hudson.model.Node#isAcceptingTasks()
+ */
+ boolean isAcceptingTasks();
+
+ /**
+ * @return the URL where to reach specifically this computer, relative to Jenkins URL.
+ */
+ @NonNull
+ String getUrl();
+
+ /**
+ * @return {@code true} if this computer has a defined offline cause, @{code false} otherwise.
+ */
+ default boolean hasOfflineCause() {
+ return Util.fixEmpty(getOfflineCauseReason()) != null;
+ }
+
+ /**
+ * If the computer was offline (either temporarily or not),
+ * this method will return the cause as a string (without user info).
+ *
+ * {@code hasOfflineCause() == true} implies this must be nonempty.
+ *
+ * @return
+ * empty string if the system was put offline without given a cause.
+ */
+ @NonNull
+ String getOfflineCauseReason();
+
+ /**
+ * @return true if the node is currently connecting to the Jenkins controller.
+ */
+ boolean isConnecting();
+
+ /**
+ * Returns the icon for this computer.
+ *
+ * It is both the recommended and default implementation to serve different icons based on {@link #isOffline}
+ *
+ * @see #getIconClassName()
+ */
+ String getIcon();
+
+ /**
+ * Returns the alternative text for the computer icon.
+ */
+ String getIconAltText();
+
+ /**
+ * Returns the class name that will be used to look up the icon.
+ *
+ * This class name will be added as a class tag to the html img tags where the icon should
+ * show up followed by a size specifier given by {@link Icon#toNormalizedIconSizeClass(String)}
+ * The conversion of class tag to src tag is registered through {@link IconSet#addIcon(Icon)}
+ *
+ * @see #getIcon()
+ */
+ default String getIconClassName() {
+ return getIcon();
+ }
+
+ /**
+ * Returns the number of {@link IExecutor}s that are doing some work right now.
+ */
+ int countBusy();
+ /**
+ * Returns the current size of the executor pool for this computer.
+ */
+ int countExecutors();
+
+ /**
+ * @return true if the computer is online.
+ */
+ boolean isOnline();
+ /**
+ * @return the number of {@link IExecutor}s that are idle right now.
+ */
+ int countIdle();
+
+ /**
+ * @return true if this computer can be launched by Jenkins proactively and automatically.
+ *
+ *
+ * For example, inbound agents return {@code false} from this, because the launch process
+ * needs to be initiated from the agent side.
+ */
+ boolean isLaunchSupported();
+
+ /**
+ * Attempts to connect this computer.
+ *
+ * @param forceReconnect If true and a connect activity is already in progress, it will be cancelled and
+ * the new one will be started. If false, and a connect activity is already in progress, this method
+ * will do nothing and just return the pending connection operation.
+ * @return A {@link Future} representing pending completion of the task. The 'completion' includes
+ * both a successful completion and a non-successful completion (such distinction typically doesn't
+ * make much sense because as soon as {@link IComputer} is connected it can be disconnected by some other threads.)
+ */
+ Future> connect(boolean forceReconnect);
+
+ @NonNull
+ @Override
+ default ACL getACL() {
+ return Jenkins.get().getAuthorizationStrategy().getACL(this);
+ }
+}
diff --git a/core/src/main/java/jenkins/model/IDisplayExecutor.java b/core/src/main/java/jenkins/model/IDisplayExecutor.java
new file mode 100644
index 0000000000000..5f959a158faa0
--- /dev/null
+++ b/core/src/main/java/jenkins/model/IDisplayExecutor.java
@@ -0,0 +1,55 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2024 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.model;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.Beta;
+
+/**
+ * A snapshot of the executor information for display purpose.
+ *
+ * @since 2.480
+ */
+@Restricted(Beta.class)
+public interface IDisplayExecutor {
+ /**
+ * @return The UI label for this executor.
+ */
+ @NonNull
+ String getDisplayName();
+
+ /**
+ * @return the URL where to reach specifically this executor, relative to Jenkins URL.
+ */
+ @NonNull
+ String getUrl();
+
+ /**
+ * @return the executor this display information is for.
+ */
+ @NonNull
+ IExecutor getExecutor();
+}
diff --git a/core/src/main/java/jenkins/model/IExecutor.java b/core/src/main/java/jenkins/model/IExecutor.java
new file mode 100644
index 0000000000000..1aec72db5be9a
--- /dev/null
+++ b/core/src/main/java/jenkins/model/IExecutor.java
@@ -0,0 +1,144 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2024 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.model;
+
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import hudson.Util;
+import hudson.model.Queue;
+import hudson.model.queue.WorkUnit;
+import jenkins.model.queue.ITask;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.Beta;
+
+/**
+ * Interface for an executor that can be displayed in the executors widget.
+ *
+ * @since 2.480
+ */
+@Restricted(Beta.class)
+public interface IExecutor {
+ /**
+ * Returns true if this {@link IExecutor} is ready for action.
+ */
+ boolean isIdle();
+
+ /**
+ * @return the {@link IComputer} that this executor belongs to.
+ */
+ IComputer getOwner();
+
+ /**
+ * @return the current executable, if any.
+ */
+ @CheckForNull Queue.Executable getCurrentExecutable();
+
+ /**
+ * Returns the current {@link WorkUnit} (of {@link #getCurrentExecutable() the current executable})
+ * that this executor is running.
+ *
+ * @return
+ * null if the executor is idle.
+ */
+ @CheckForNull WorkUnit getCurrentWorkUnit();
+
+ /**
+ * @return the current display name of the executor. Usually the name of the executable.
+ */
+ String getDisplayName();
+
+ /**
+ * @return a reference to the parent task of the current executable, if any.
+ */
+ @CheckForNull
+ default ITask getParentTask() {
+ var currentExecutable = getCurrentExecutable();
+ if (currentExecutable == null) {
+ var workUnit = getCurrentWorkUnit();
+ if (workUnit != null) {
+ return workUnit.work;
+ } else {
+ // Idle
+ return null;
+ }
+ } else {
+ return currentExecutable.getParent();
+ }
+ }
+
+ /**
+ * Checks if the current user has a permission to stop this build.
+ */
+ boolean hasStopPermission();
+
+ /**
+ * Gets the executor number that uniquely identifies it among
+ * other {@link IExecutor}s for the same computer.
+ *
+ * @return
+ * a sequential number starting from 0.
+ */
+ int getNumber();
+
+ /**
+ * Gets the elapsed time since the build has started.
+ *
+ * @return
+ * the number of milliseconds since the build has started.
+ */
+ long getElapsedTime();
+
+ /**
+ * Gets the string that says how long since this build has started.
+ *
+ * @return
+ * string like "3 minutes" "1 day" etc.
+ */
+ default String getTimestampString() {
+ return Util.getTimeSpanString(getElapsedTime());
+ }
+
+ /**
+ * Computes a human-readable text that shows the expected remaining time
+ * until the build completes.
+ */
+ String getEstimatedRemainingTime();
+
+ /**
+ * Returns true if the current build is likely stuck.
+ *
+ *
+ * This is a heuristics based approach, but if the build is suspiciously taking for a long time,
+ * this method returns true.
+ */
+ boolean isLikelyStuck();
+
+ /**
+ * Returns the progress of the current build in the number between 0-100.
+ *
+ * @return -1
+ * if it's impossible to estimate the progress.
+ */
+ int getProgress();
+}
diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java
index 705044cd8fa00..90ffceefb4564 100644
--- a/core/src/main/java/jenkins/model/Jenkins.java
+++ b/core/src/main/java/jenkins/model/Jenkins.java
@@ -2071,7 +2071,7 @@ public boolean isUpgradedFromBefore(VersionNumber v) {
* Gets the read-only list of all {@link Computer}s.
*/
public Computer[] getComputers() {
- return computers.values().stream().sorted(Comparator.comparing(Computer::getName)).toArray(Computer[]::new);
+ return getComputersCollection().stream().sorted(Comparator.comparing(Computer::getName)).toArray(Computer[]::new);
}
@CLIResolver
@@ -2080,7 +2080,7 @@ public Computer[] getComputers() {
|| name.equals("(master)")) // backwards compatibility for URLs
name = "";
- for (Computer c : computers.values()) {
+ for (Computer c : getComputersCollection()) {
if (c.getName().equals(name))
return c;
}
@@ -2247,6 +2247,14 @@ protected ConcurrentMap getComputerMap() {
return computers;
}
+ /**
+ * @return the collection of all {@link Computer}s in this instance.
+ */
+ @Restricted(NoExternalUse.class)
+ public Collection getComputersCollection() {
+ return computers.values();
+ }
+
/**
* Returns all {@link Node}s in the system, excluding {@link Jenkins} instance itself which
* represents the built-in node (in other words, this only returns agents).
@@ -2479,7 +2487,7 @@ protected Iterable allAsIterable() {
protected Computer get(String key) { return getComputer(key); }
@Override
- protected Collection all() { return computers.values(); }
+ protected Collection all() { return getComputersCollection(); }
})
.add(new CollectionSearchIndex() { // for users
@Override
@@ -3814,7 +3822,7 @@ private Set> _cleanUpDisconnectComputers(final List errors)
final Set> pending = new HashSet<>();
// JENKINS-28840 we know we will be interrupting all the Computers so get the Queue lock once for all
Queue.withLock(() -> {
- for (Computer c : computers.values()) {
+ for (Computer c : getComputersCollection()) {
try {
c.interrupt();
killComputer(c);
@@ -4665,7 +4673,7 @@ public HttpResponse doSafeRestart(StaplerRequest req) throws IOException, Servle
/**
* Queues up a safe restart of Jenkins. Jobs have to finish or pause before it can proceed. No new jobs are accepted.
*
- * @since 2.414
+ * @since 2.475
*/
public HttpResponse doSafeRestart(StaplerRequest2 req, @QueryParameter("message") String message) throws IOException, ServletException, RestartNotSupportedException {
checkPermission(MANAGE);
@@ -4684,6 +4692,20 @@ public HttpResponse doSafeRestart(StaplerRequest2 req, @QueryParameter("message"
return HttpResponses.redirectToDot();
}
+ /**
+ * @deprecated use {@link #doSafeRestart(StaplerRequest2, String)}
+ * @since 2.414
+ */
+ @Deprecated
+ @StaplerNotDispatchable
+ public HttpResponse doSafeRestart(StaplerRequest req, @QueryParameter("message") String message) throws IOException, javax.servlet.ServletException, RestartNotSupportedException {
+ try {
+ return doSafeRestart(StaplerRequest.toStaplerRequest2(req), message);
+ } catch (ServletException e) {
+ throw ServletExceptionWrapper.fromJakartaServletException(e);
+ }
+ }
+
private static Lifecycle restartableLifecycle() throws RestartNotSupportedException {
if (Main.isUnitTest) {
throw new RestartNotSupportedException("Restarting the controller JVM is not supported in JenkinsRule-based tests");
@@ -5476,6 +5498,7 @@ protected MasterComputer() {
* Returns "" to match with {@link Jenkins#getNodeName()}.
*/
@Override
+ @NonNull
public String getName() {
return "";
}
@@ -5497,6 +5520,7 @@ public String getCaption() {
}
@Override
+ @NonNull
public String getUrl() {
return "computer/(built-in)/";
}
diff --git a/core/src/main/java/jenkins/model/ModelObjectWithContextMenu.java b/core/src/main/java/jenkins/model/ModelObjectWithContextMenu.java
index f46280fef0c01..11eea481106a3 100644
--- a/core/src/main/java/jenkins/model/ModelObjectWithContextMenu.java
+++ b/core/src/main/java/jenkins/model/ModelObjectWithContextMenu.java
@@ -254,8 +254,20 @@ public ContextMenu add(Node n) {
* Adds a computer
*
* @since 1.513
+ * @deprecated use {@link #add(IComputer)} instead.
*/
+ @Deprecated(since = "2.480")
public ContextMenu add(Computer c) {
+ return add((IComputer) c);
+ }
+
+ /**
+ * Adds a {@link IComputer} instance.
+ * @param c the computer to add to the menu
+ * @return this
+ * @since 2.480
+ */
+ public ContextMenu add(IComputer c) {
return add(new MenuItem()
.withDisplayName(c.getDisplayName())
.withIconClass(c.getIconClassName())
diff --git a/core/src/main/java/jenkins/model/Nodes.java b/core/src/main/java/jenkins/model/Nodes.java
index 8f4c0e5eafc62..e15d391c4975e 100644
--- a/core/src/main/java/jenkins/model/Nodes.java
+++ b/core/src/main/java/jenkins/model/Nodes.java
@@ -295,6 +295,7 @@ public void run() {
jenkins.trimLabels(node);
}
NodeListener.fireOnDeleted(node);
+ SaveableListener.fireOnDeleted(node, getConfigFile(node));
}
}
diff --git a/core/src/main/java/jenkins/model/queue/ITask.java b/core/src/main/java/jenkins/model/queue/ITask.java
new file mode 100644
index 0000000000000..37b0e62150de0
--- /dev/null
+++ b/core/src/main/java/jenkins/model/queue/ITask.java
@@ -0,0 +1,76 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2024 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.model.queue;
+
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import hudson.model.Item;
+import hudson.model.ModelObject;
+import hudson.security.AccessControlled;
+
+/**
+ * A task that can be displayed in the executors widget.
+ *
+ * @since 2.480
+ */
+public interface ITask extends ModelObject {
+ /**
+ * @return {@code true} if the current user can cancel the current task.
+ *
+ * NOTE: If you have implemented {@link AccessControlled} this returns by default
+ * {@code hasPermission(Item.CANCEL)}
+ */
+ default boolean hasAbortPermission() {
+ if (this instanceof AccessControlled ac) {
+ return ac.hasPermission(Item.CANCEL);
+ }
+ return true;
+ }
+
+ /**
+ * @return {@code true} if the current user has read access on the task.
+ */
+ @SuppressWarnings("unused") // jelly
+ default boolean hasReadPermission() {
+ if (this instanceof AccessControlled ac) {
+ return ac.hasPermission(Item.READ);
+ }
+ return true;
+ }
+
+ /**
+ * @return the full display name of the task.
+ *
+ * Defaults to the same as {@link #getDisplayName()}.
+ */
+ default String getFullDisplayName() {
+ return getDisplayName();
+ }
+
+ /**
+ * @return the URL where to reach specifically this task, relative to Jenkins URL. If non-null, must end with '/'.
+ */
+ @CheckForNull
+ String getUrl();
+}
diff --git a/core/src/main/java/jenkins/model/queue/ItemDeletion.java b/core/src/main/java/jenkins/model/queue/ItemDeletion.java
index a2d954fbc4593..b278f4d24c930 100644
--- a/core/src/main/java/jenkins/model/queue/ItemDeletion.java
+++ b/core/src/main/java/jenkins/model/queue/ItemDeletion.java
@@ -266,12 +266,10 @@ public static void cancelBuildsInProgress(@NonNull Item initiatingItem) throws F
// comparison with executor.getCurrentExecutable() == executable currently should always be
// true as we no longer recycle Executors, but safer to future-proof in case we ever
// revisit recycling.
- if (!entry.getKey().isAlive()
+ if (!entry.getKey().isActive()
|| entry.getValue() != entry.getKey().getCurrentExecutable()) {
iterator.remove();
}
- // I don't know why, but we have to keep interrupting
- entry.getKey().interrupt(Result.ABORTED);
}
Thread.sleep(50L);
}
diff --git a/core/src/main/java/jenkins/security/ExtendedReadRedaction.java b/core/src/main/java/jenkins/security/ExtendedReadRedaction.java
new file mode 100644
index 0000000000000..7baec1962f484
--- /dev/null
+++ b/core/src/main/java/jenkins/security/ExtendedReadRedaction.java
@@ -0,0 +1,36 @@
+package jenkins.security;
+
+import hudson.ExtensionList;
+import hudson.ExtensionPoint;
+
+/**
+ * Redact {@code config.xml} contents for users with ExtendedRead permission
+ * while lacking the required Configure permission to see the full unredacted
+ * configuration.
+ *
+ * @see SECURITY-266
+ * @see Jenkins Security Advisory 2016-05-11
+ * @since 2.479
+ */
+public interface ExtendedReadRedaction extends ExtensionPoint {
+ /**
+ * Redacts sensitive information from the provided {@code config.xml} file content.
+ * Input may already have redactions applied; output may be passed through further redactions.
+ * These methods are expected to retain the basic structure of the XML document contained in input/output strings.
+ *
+ * @param configDotXml String representation of (potentially already redacted) config.xml file
+ * @return Redacted config.xml file content
+ */
+ String apply(String configDotXml);
+
+ static ExtensionList all() {
+ return ExtensionList.lookup(ExtendedReadRedaction.class);
+ }
+
+ static String applyAll(String configDotXml) {
+ for (ExtendedReadRedaction redaction : all()) {
+ configDotXml = redaction.apply(configDotXml);
+ }
+ return configDotXml;
+ }
+}
diff --git a/core/src/main/java/jenkins/security/ExtendedReadSecretRedaction.java b/core/src/main/java/jenkins/security/ExtendedReadSecretRedaction.java
new file mode 100644
index 0000000000000..91a79f354d716
--- /dev/null
+++ b/core/src/main/java/jenkins/security/ExtendedReadSecretRedaction.java
@@ -0,0 +1,28 @@
+package jenkins.security;
+
+import hudson.Extension;
+import hudson.util.Secret;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+@Restricted(NoExternalUse.class)
+@Extension
+public class ExtendedReadSecretRedaction implements ExtendedReadRedaction {
+
+ private static final Pattern SECRET_PATTERN = Pattern.compile(">(" + Secret.ENCRYPTED_VALUE_PATTERN + ")<");
+
+ @Override
+ public String apply(String configDotXml) {
+ Matcher matcher = SECRET_PATTERN.matcher(configDotXml);
+ StringBuilder cleanXml = new StringBuilder();
+ while (matcher.find()) {
+ if (Secret.decrypt(matcher.group(1)) != null) {
+ matcher.appendReplacement(cleanXml, ">********<");
+ }
+ }
+ matcher.appendTail(cleanXml);
+ return cleanXml.toString();
+ }
+}
diff --git a/core/src/main/resources/hudson/model/ComputerSet/index.jelly b/core/src/main/resources/hudson/model/ComputerSet/index.jelly
index f261e1f3f8cd2..c96346ac49490 100644
--- a/core/src/main/resources/hudson/model/ComputerSet/index.jelly
+++ b/core/src/main/resources/hudson/model/ComputerSet/index.jelly
@@ -72,8 +72,8 @@ THE SOFTWARE.
-
-
+
+
@@ -93,7 +93,7 @@ THE SOFTWARE.
-
+
| |