org.samba.jcifs
jcifs
diff --git a/cli/pom.xml b/cli/pom.xml
index 6309811a2e8b..2473629d23cc 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/cli/src/main/java/hudson/cli/FlightRecorderInputStream.java b/cli/src/main/java/hudson/cli/FlightRecorderInputStream.java
index 5a1167c4fdc5..41d7a719dd09 100644
--- a/cli/src/main/java/hudson/cli/FlightRecorderInputStream.java
+++ b/cli/src/main/java/hudson/cli/FlightRecorderInputStream.java
@@ -21,7 +21,7 @@ class FlightRecorderInputStream extends InputStream {
* Size (in bytes) of the flight recorder ring buffer used for debugging remoting issues.
* @since 2.41
*/
- static final int BUFFER_SIZE = Integer.getInteger("hudson.remoting.FlightRecorderInputStream.BUFFER_SIZE", 1024 * 1024);
+ static final int BUFFER_SIZE = Integer.getInteger("hudson.remoting.FlightRecorderInputStream.BUFFER_SIZE", 1024);
private final InputStream source;
private ByteArrayRingBuffer recorder = new ByteArrayRingBuffer(BUFFER_SIZE);
diff --git a/core/src/main/java/hudson/TcpSlaveAgentListener.java b/core/src/main/java/hudson/TcpSlaveAgentListener.java
index 3936071de2ef..e7f037652c9b 100644
--- a/core/src/main/java/hudson/TcpSlaveAgentListener.java
+++ b/core/src/main/java/hudson/TcpSlaveAgentListener.java
@@ -271,14 +271,11 @@ public void run() {
String protocol = s.substring(9);
AgentProtocol p = AgentProtocol.of(protocol);
if (p != null) {
- if (Jenkins.get().getAgentProtocols().contains(protocol)) {
- LOGGER.log(p instanceof PingAgentProtocol ? Level.FINE : Level.INFO, () -> "Accepted " + protocol + " connection " + connectionInfo);
- p.handle(this.s);
- } else {
- error("Disabled protocol:" + s, this.s);
- }
- } else
+ LOGGER.log(p instanceof PingAgentProtocol ? Level.FINE : Level.INFO, () -> "Accepted " + protocol + " connection " + connectionInfo);
+ p.handle(this.s);
+ } else {
error("Unknown protocol:", this.s);
+ }
} else {
error("Unrecognized protocol: " + s, this.s);
}
@@ -364,21 +361,11 @@ public PingAgentProtocol() {
ping = "Ping\n".getBytes(StandardCharsets.UTF_8);
}
- @Override
- public boolean isRequired() {
- return true;
- }
-
@Override
public String getName() {
return "Ping";
}
- @Override
- public String getDisplayName() {
- return Messages.TcpSlaveAgentListener_PingAgentProtocol_displayName();
- }
-
@Override
public void handle(Socket socket) throws IOException, InterruptedException {
try (socket) {
diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java
index b4529a0ea279..07d00a44d006 100644
--- a/core/src/main/java/hudson/Util.java
+++ b/core/src/main/java/hudson/Util.java
@@ -1525,6 +1525,10 @@ public static Number tryParseNumber(@CheckForNull String numberStr, @CheckForNul
* does not contain the specified method.
*/
public static boolean isOverridden(@NonNull Class> base, @NonNull Class> derived, @NonNull String methodName, @NonNull Class>... types) {
+ if (base == derived) {
+ // If base and derived are the same type, the method is not overridden by definition
+ return false;
+ }
// If derived is not a subclass or implementor of base, it can't override any method
// Technically this should also be triggered when base == derived, because it can't override its own method, but
// the unit tests explicitly test for that as working.
diff --git a/core/src/main/java/hudson/logging/LogRecorder.java b/core/src/main/java/hudson/logging/LogRecorder.java
index ad4c40028547..a4c42bdfcba7 100644
--- a/core/src/main/java/hudson/logging/LogRecorder.java
+++ b/core/src/main/java/hudson/logging/LogRecorder.java
@@ -44,6 +44,7 @@
import hudson.remoting.VirtualChannel;
import hudson.slaves.ComputerListener;
import hudson.util.CopyOnWriteList;
+import hudson.util.FormApply;
import hudson.util.FormValidation;
import hudson.util.HttpResponses;
import hudson.util.RingBufferLogHandler;
@@ -463,7 +464,7 @@ public synchronized void doConfigSubmit(StaplerRequest2 req, StaplerResponse2 rs
save();
if (oldFile != null) oldFile.delete();
- rsp.sendRedirect2(redirect);
+ FormApply.success(redirect).generateResponse(req, rsp, null);
}
@RequirePOST
@@ -556,7 +557,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 a3bf53a739a1..95af637af974 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 8ba7aafe15cd..f31316c31656 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 081f73dd1625..0c525dfabe84 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;
@@ -67,6 +66,7 @@
import hudson.util.DaemonThreadFactory;
import hudson.util.EditDistance;
import hudson.util.ExceptionCatchingThreadFactory;
+import hudson.util.FormApply;
import hudson.util.Futures;
import hudson.util.IOUtils;
import hudson.util.NamingThreadFactory;
@@ -106,6 +106,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 +119,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 +151,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 +165,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:
@@ -179,11 +180,6 @@
private long connectTime = 0;
- /**
- * True if Jenkins shouldn't start new builds on this node.
- */
- private boolean temporarilyOffline;
-
/**
* {@link Node} object may be created and deleted independently
* from this object.
@@ -351,12 +347,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.
@@ -365,31 +355,27 @@ public ACL getACL() {
* null if the system was put offline without given a cause.
*/
@Exported
+ @Override
public OfflineCause getOfflineCause() {
+ var node = getNode();
+ if (node != null) {
+ var temporaryOfflineCause = node.getTemporaryOfflineCause();
+ if (temporaryOfflineCause != null) {
+ return temporaryOfflineCause;
+ }
+ }
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 "";
- }
- // fetch the localized string for "Disconnected By"
- String gsub_base = hudson.slaves.Messages.SlaveComputer_DisconnectedBy("", "");
- // regex to remove commented reason base string
- String gsub1 = "^" + gsub_base + "[\\w\\W]* \\: ";
- // regex to remove non-commented reason base string
- String gsub2 = "^" + gsub_base + "[\\w\\W]*";
-
- String newString = offlineCause.toString().replaceAll(gsub1, "");
- return newString.replaceAll(gsub2, "");
+ return IComputer.super.getOfflineCauseReason();
}
/**
@@ -556,7 +542,7 @@ public void cliDisconnect(String cause) throws ExecutionException, InterruptedEx
@Deprecated
public void cliOffline(String cause) throws ExecutionException, InterruptedException {
checkPermission(DISCONNECT);
- setTemporarilyOffline(true, new ByCLI(cause));
+ setTemporaryOfflineCause(new ByCLI(cause));
}
/**
@@ -565,7 +551,7 @@ public void cliOffline(String cause) throws ExecutionException, InterruptedExcep
@Deprecated
public void cliOnline() throws ExecutionException, InterruptedException {
checkPermission(CONNECT);
- setTemporarilyOffline(false, null);
+ setTemporaryOfflineCause(null);
}
/**
@@ -581,9 +567,6 @@ public int getNumExecutors() {
return numExecutors;
}
- /**
- * Returns {@link Node#getNodeName() the name of the node}.
- */
public @NonNull String getName() {
return nodeName != null ? nodeName : "";
}
@@ -628,8 +611,9 @@ public BuildTimelineWidget getTimeline() {
}
@Exported
+ @Override
public boolean isOffline() {
- return temporarilyOffline || getChannel() == null;
+ return isTemporarilyOffline() || getChannel() == null;
}
public final boolean isOnline() {
@@ -645,12 +629,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 +640,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;
}
@@ -690,85 +662,89 @@ public boolean isLaunchSupported() {
@Exported
@Deprecated
public boolean isTemporarilyOffline() {
- return temporarilyOffline;
+ var node = getNode();
+ return node != null && node.isTemporarilyOffline();
+ }
+
+ /**
+ * Allows a caller to define an {@link OfflineCause} for a computer that has never been online.
+ * @since 2.483
+ */
+ public void setOfflineCause(OfflineCause cause) {
+ this.offlineCause = cause;
}
/**
* @deprecated as of 1.320.
- * Use {@link #setTemporarilyOffline(boolean, OfflineCause)}
+ * Use {@link #setTemporaryOfflineCause(OfflineCause)}
*/
@Deprecated
public void setTemporarilyOffline(boolean temporarilyOffline) {
- setTemporarilyOffline(temporarilyOffline, null);
+ setTemporaryOfflineCause(temporarilyOffline ? new OfflineCause.LegacyOfflineCause() : null);
}
/**
- * Marks the computer as temporarily offline. This retains the underlying
- * {@link Channel} connection, but prevent builds from executing.
- *
- * @param cause
- * If the first argument is true, specify the reason why the node is being put
- * offline.
+ * @deprecated
+ * Use {@link #setTemporaryOfflineCause(OfflineCause)} instead.
*/
+ @Deprecated(since = "2.482")
public void setTemporarilyOffline(boolean temporarilyOffline, OfflineCause cause) {
- offlineCause = temporarilyOffline ? cause : null;
- this.temporarilyOffline = temporarilyOffline;
- Node node = getNode();
- if (node != null) {
- node.setTemporaryOfflineCause(offlineCause);
- }
- synchronized (statusChangeLock) {
- statusChangeLock.notifyAll();
- }
- if (temporarilyOffline) {
- Listeners.notify(ComputerListener.class, false, l -> l.onTemporarilyOffline(this, cause));
+ if (cause == null) {
+ setTemporarilyOffline(temporarilyOffline);
} else {
- Listeners.notify(ComputerListener.class, false, l -> l.onTemporarilyOnline(this));
+ setTemporaryOfflineCause(temporarilyOffline ? cause : null);
}
}
/**
- * Returns the icon for this computer.
- *
- * It is both the recommended and default implementation to serve different icons based on {@link #isOffline}
+ * Marks the computer as temporarily offline. This retains the underlying
+ * {@link Channel} connection, but prevent builds from executing.
*
- * @see #getIconClassName()
+ * @param temporaryOfflineCause The reason why the node is being put offline.
+ * If null, this cancels the status
+ * @since 2.482
*/
+ public void setTemporaryOfflineCause(@CheckForNull OfflineCause temporaryOfflineCause) {
+ var node = getNode();
+ if (node == null) {
+ throw new IllegalStateException("Can't set a temporary offline cause if the node has been removed");
+ }
+ node.setTemporaryOfflineCause(temporaryOfflineCause);
+ }
+
+ /**
+ * @since 2.482
+ * @return If the node is temporarily offline, the reason why.
+ */
+ @SuppressWarnings("unused") // used by setOfflineCause.jelly
+ public String getTemporaryOfflineCauseReason() {
+ var node = getNode();
+ if (node == null) {
+ // Node was deleted; computer still exists
+ return null;
+ }
+ var cause = node.getTemporaryOfflineCause();
+ if (cause instanceof OfflineCause.UserCause userCause) {
+ return userCause.getMessage();
+ }
+ return cause != null ? cause.toString() : "";
+ }
+
@Exported
+ @Override
public String getIcon() {
- // The machine was taken offline by someone
- if (isTemporarilyOffline() && getOfflineCause() instanceof OfflineCause.UserCause) return "symbol-computer-disconnected";
- // The computer is not accepting tasks, e.g. because the availability demands it being offline.
- if (!isAcceptingTasks()) {
- return "symbol-computer-not-accepting";
- }
- // The computer is not connected or it is temporarily offline due to a node monitor
- if (isOffline()) return "symbol-computer-offline";
- return "symbol-computer";
+ return IComputer.super.getIcon();
}
/**
- * Returns the class name that will be used to lookup 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)}
- *
- * It is both the recommended and default implementation to serve different icons based on {@link #isOffline}
+ * {@inheritDoc}
*
* @see #getIcon()
*/
@Exported
+ @Override
public String getIconClassName() {
- return getIcon();
- }
-
- public String getIconAltText() {
- // The machine was taken offline by someone
- if (isTemporarilyOffline() && getOfflineCause() instanceof OfflineCause.UserCause) return "[temporarily offline by user]";
- // There is a "technical" reason the computer will not accept new builds
- if (isOffline() || !isAcceptingTasks()) return "[offline]";
- return "[online]";
+ return IComputer.super.getIconClassName();
}
@Exported
@@ -780,6 +756,8 @@ public String getCaption() {
return Messages.Computer_Caption(nodeName);
}
+ @Override
+ @NonNull
public String getUrl() {
return "computer/" + Util.fullEncode(getName()) + "/";
}
@@ -814,16 +792,6 @@ protected void setNode(Node node) {
this.nodeName = null;
setNumExecutors(node.getNumExecutors());
- if (this.temporarilyOffline) {
- // When we get a new node, push our current temp offline
- // status to it (as the status is not carried across
- // configuration changes that recreate the node).
- // Since this is also called the very first time this
- // Computer is created, avoid pushing an empty status
- // as that could overwrite any status that the Node
- // brought along from its persisted config data.
- node.setTemporaryOfflineCause(this.offlineCause);
- }
}
/**
@@ -947,19 +915,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 +963,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()) {
@@ -1426,24 +1393,23 @@ public void doRssLatest(StaplerRequest2 req, StaplerResponse2 rsp) throws IOExce
@RequirePOST
public HttpResponse doToggleOffline(@QueryParameter String offlineMessage) throws IOException, ServletException {
- if (!temporarilyOffline) {
- checkPermission(DISCONNECT);
- offlineMessage = Util.fixEmptyAndTrim(offlineMessage);
- setTemporarilyOffline(!temporarilyOffline,
- new OfflineCause.UserCause(User.current(), offlineMessage));
- } else {
+ var node = getNode();
+ if (node == null) {
+ return HttpResponses.notFound();
+ }
+ if (node.isTemporarilyOffline()) {
checkPermission(CONNECT);
- setTemporarilyOffline(!temporarilyOffline, null);
+ setTemporaryOfflineCause(null);
+ return HttpResponses.redirectToDot();
+ } else {
+ return doChangeOfflineCause(offlineMessage);
}
- return HttpResponses.redirectToDot();
}
@RequirePOST
public HttpResponse doChangeOfflineCause(@QueryParameter String offlineMessage) throws IOException, ServletException {
checkPermission(DISCONNECT);
- offlineMessage = Util.fixEmptyAndTrim(offlineMessage);
- setTemporarilyOffline(true,
- new OfflineCause.UserCause(User.current(), offlineMessage));
+ setTemporaryOfflineCause(new OfflineCause.UserCause(User.current(), Util.fixEmptyAndTrim(offlineMessage)));
return HttpResponses.redirectToDot();
}
@@ -1542,7 +1508,7 @@ public void doConfigSubmit(StaplerRequest2 req, StaplerResponse2 rsp) throws IOE
}
// take the user back to the agent top page.
- rsp.sendRedirect2("../" + result.getNodeName() + '/');
+ FormApply.success("../" + result.getNodeName() + '/').generateResponse(req, rsp, null);
}
/**
@@ -1659,15 +1625,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 +1686,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 f25d25a19a50..fcc0aa4e41e7 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/Descriptor.java b/core/src/main/java/hudson/model/Descriptor.java
index 562b96e37b8e..33c981d657f5 100644
--- a/core/src/main/java/hudson/model/Descriptor.java
+++ b/core/src/main/java/hudson/model/Descriptor.java
@@ -24,7 +24,6 @@
package hudson.model;
-import static hudson.util.QuotedStringTokenizer.quote;
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import edu.umd.cs.findbugs.annotations.CheckForNull;
@@ -1306,7 +1305,7 @@ public String getFormField() {
@Override
public void generateResponse(StaplerRequest2 req, StaplerResponse2 rsp, Object node) throws IOException, ServletException {
if (FormApply.isApply(req)) {
- FormApply.applyResponse("notificationBar.show(" + quote(getMessage()) + ",notificationBar.ERROR)")
+ FormApply.showNotification(getMessage(), FormApply.NotificationType.ERROR)
.generateResponse(req, rsp, node);
} else {
// for now, we can't really use the field name that caused the problem.
diff --git a/core/src/main/java/hudson/model/Executor.java b/core/src/main/java/hudson/model/Executor.java
index 94aa1c770d21..f86dbd7c6c0a 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 404d213f7344..89945267edc5 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/Job.java b/core/src/main/java/hudson/model/Job.java
index e4d54d44b375..d22c25e98e3d 100644
--- a/core/src/main/java/hudson/model/Job.java
+++ b/core/src/main/java/hudson/model/Job.java
@@ -34,6 +34,7 @@
import hudson.BulkChange;
import hudson.EnvVars;
import hudson.Extension;
+import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.FeedAdapter;
import hudson.PermalinkList;
@@ -221,29 +222,20 @@ public void onLoad(ItemGroup extends Item> parent, String name)
TextFile f = getNextBuildNumberFile();
if (f.exists()) {
- // starting 1.28, we store nextBuildNumber in a separate file.
- // but old Hudson didn't do it, so if the file doesn't exist,
- // assume that nextBuildNumber was read from config.xml
try {
synchronized (this) {
this.nextBuildNumber = Integer.parseInt(f.readTrim());
}
} catch (NumberFormatException e) {
LOGGER.log(Level.WARNING, "Corruption in {0}: {1}", new Object[] {f, e});
- //noinspection StatementWithEmptyBody
- if (this instanceof LazyBuildMixIn.LazyLoadingJob) {
- // allow LazyBuildMixIn.onLoad to fix it
- } else {
- RunT lB = getLastBuild();
- synchronized (this) {
- this.nextBuildNumber = lB != null ? lB.getNumber() + 1 : 1;
- }
- saveNextBuildNumber();
+ RunT lB = getLastBuild();
+ synchronized (this) {
+ this.nextBuildNumber = lB != null ? lB.getNumber() + 1 : 1;
}
+ saveNextBuildNumber();
}
- } else {
- // From the old Hudson, or doCreateItem. Create this file now.
- saveNextBuildNumber();
+ } else if (nextBuildNumber == 0) {
+ nextBuildNumber = 1;
}
if (properties == null) // didn't exist < 1.72
@@ -346,12 +338,42 @@ public boolean isKeepDependencies() {
}
/**
- * Allocates a new buildCommand number.
+ * Allocates a new build number.
+ * @see BuildNumberAssigner
*/
- public synchronized int assignBuildNumber() throws IOException {
- int r = nextBuildNumber++;
- saveNextBuildNumber();
- return r;
+ public int assignBuildNumber() throws IOException {
+ return ExtensionList.lookupFirst(BuildNumberAssigner.class).assignBuildNumber(this, this::saveNextBuildNumber);
+ }
+
+ /**
+ * Alternate strategy for assigning build numbers.
+ */
+ @Restricted(Beta.class)
+ public interface BuildNumberAssigner extends ExtensionPoint {
+ /**
+ * Implementation of {@link Job#assignBuildNumber}.
+ */
+ int assignBuildNumber(Job, ?> job, SaveNextBuildNumber saveNextBuildNumber) throws IOException;
+ /**
+ * Provides an externally accessible alias for {@link Job#saveNextBuildNumber}, which is {@code protected}.
+ * ({@link #getNextBuildNumber} and {@link #fastUpdateNextBuildNumber} are already accessible.)
+ */
+ interface SaveNextBuildNumber {
+ void call() throws IOException;
+ }
+ }
+
+ @Restricted(DoNotUse.class)
+ @Extension(ordinal = -1000)
+ public static final class DefaultBuildNumberAssigner implements BuildNumberAssigner {
+ @Override
+ public int assignBuildNumber(Job, ?> job, SaveNextBuildNumber saveNextBuildNumber) throws IOException {
+ synchronized (job) {
+ int r = job.nextBuildNumber++;
+ saveNextBuildNumber.call();
+ return r;
+ }
+ }
}
/**
diff --git a/core/src/main/java/hudson/model/Node.java b/core/src/main/java/hudson/model/Node.java
index 55cacd269133..d918b0f1db34 100644
--- a/core/src/main/java/hudson/model/Node.java
+++ b/core/src/main/java/hudson/model/Node.java
@@ -30,7 +30,6 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.BulkChange;
-import hudson.Extension;
import hudson.ExtensionPoint;
import hudson.FilePath;
import hudson.FileSystemProvisioner;
@@ -69,6 +68,7 @@
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import jenkins.model.Nodes;
+import jenkins.util.Listeners;
import jenkins.util.SystemProperties;
import jenkins.util.io.OnMaster;
import net.sf.json.JSONObject;
@@ -265,24 +265,13 @@ public void onLoad(Nodes parent, String name) {
}
/**
- * Let Nodes be aware of the lifecycle of their own {@link Computer}.
+ * @return true if this node has a temporary offline cause set.
*/
- @Extension
- public static class InternalComputerListener extends ComputerListener {
- @Override
- public void onOnline(Computer c, TaskListener listener) {
- Node node = c.getNode();
-
- // At startup, we need to restore any previously in-effect temp offline cause.
- // We wait until the computer is started rather than getting the data to it sooner
- // so that the normal computer start up processing works as expected.
- if (node != null && node.temporaryOfflineCause != null && node.temporaryOfflineCause != c.getOfflineCause()) {
- c.setTemporarilyOffline(true, node.temporaryOfflineCause);
- }
- }
+ boolean isTemporarilyOffline() {
+ return temporaryOfflineCause != null;
}
- private OfflineCause temporaryOfflineCause;
+ private volatile OfflineCause temporaryOfflineCause;
/**
* Enable a {@link Computer} to inform its node when it is taken
@@ -294,6 +283,11 @@ void setTemporaryOfflineCause(OfflineCause cause) {
temporaryOfflineCause = cause;
save();
}
+ if (temporaryOfflineCause != null) {
+ Listeners.notify(ComputerListener.class, false, l -> l.onTemporarilyOffline(toComputer(), temporaryOfflineCause));
+ } else {
+ Listeners.notify(ComputerListener.class, false, l -> l.onTemporarilyOnline(toComputer()));
+ }
} catch (java.io.IOException e) {
LOGGER.warning("Unable to complete save, temporary offline status will not be persisted: " + e.getMessage());
}
diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java
index 08eba90b906c..797cba56f394 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 37b09fd47d25..ec7eaabf32c6 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 8a38ef2d470b..0e3cc894148a 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 5d53a49ef0b7..b17077cde44d 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 46bbc6ab60be..14877d080bf5 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 0690d074617c..9a971d9ca40b 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/node_monitors/AbstractNodeMonitorDescriptor.java b/core/src/main/java/hudson/node_monitors/AbstractNodeMonitorDescriptor.java
index 7cd1c75abc8d..d49924508bee 100644
--- a/core/src/main/java/hudson/node_monitors/AbstractNodeMonitorDescriptor.java
+++ b/core/src/main/java/hudson/node_monitors/AbstractNodeMonitorDescriptor.java
@@ -233,7 +233,7 @@ public boolean isIgnored() {
*/
protected boolean markOnline(Computer c) {
if (isIgnored() || c.isOnline()) return false; // noop
- c.setTemporarilyOffline(false, null);
+ c.setTemporaryOfflineCause(null);
return true;
}
@@ -247,7 +247,7 @@ protected boolean markOnline(Computer c) {
protected boolean markOffline(Computer c, OfflineCause oc) {
if (isIgnored() || c.isTemporarilyOffline()) return false; // noop
- c.setTemporarilyOffline(true, oc);
+ c.setTemporaryOfflineCause(oc);
// notify the admin
MonitorMarkedNodeOffline no = AdministrativeMonitor.all().get(MonitorMarkedNodeOffline.class);
diff --git a/core/src/main/java/hudson/security/AuthorizationStrategy.java b/core/src/main/java/hudson/security/AuthorizationStrategy.java
index db3001fc40f2..5ca218f6a8fd 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/security/GlobalSecurityConfiguration.java b/core/src/main/java/hudson/security/GlobalSecurityConfiguration.java
index a8657df65685..ae90b0428c7b 100644
--- a/core/src/main/java/hudson/security/GlobalSecurityConfiguration.java
+++ b/core/src/main/java/hudson/security/GlobalSecurityConfiguration.java
@@ -37,15 +37,12 @@
import hudson.util.FormApply;
import jakarta.servlet.ServletException;
import java.io.IOException;
-import java.util.Set;
-import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.GlobalConfigurationCategory;
import jenkins.model.Jenkins;
import jenkins.util.ServerTcpPort;
-import net.sf.json.JSONArray;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;
import org.jenkinsci.Symbol;
@@ -92,11 +89,6 @@ public boolean isSlaveAgentPortEnforced() {
return Jenkins.get().isSlaveAgentPortEnforced();
}
- @NonNull
- public Set getAgentProtocols() {
- return Jenkins.get().getAgentProtocols();
- }
-
public boolean isDisableRememberMe() {
return Jenkins.get().isDisableRememberMe();
}
@@ -149,18 +141,6 @@ public boolean configure(StaplerRequest2 req, JSONObject json) throws FormExcept
throw new FormException(e, "slaveAgentPortType");
}
}
- Set agentProtocols = new TreeSet<>();
- if (json.has("agentProtocol")) {
- Object protocols = json.get("agentProtocol");
- if (protocols instanceof JSONArray) {
- for (int i = 0; i < ((JSONArray) protocols).size(); i++) {
- agentProtocols.add(((JSONArray) protocols).getString(i));
- }
- } else {
- agentProtocols.add(protocols.toString());
- }
- }
- j.setAgentProtocols(agentProtocols);
// persist all the additional security configs
boolean result = true;
diff --git a/core/src/main/java/hudson/slaves/OfflineCause.java b/core/src/main/java/hudson/slaves/OfflineCause.java
index 556c0ebb0c53..07c2d3af2872 100644
--- a/core/src/main/java/hudson/slaves/OfflineCause.java
+++ b/core/src/main/java/hudson/slaves/OfflineCause.java
@@ -30,9 +30,11 @@
import hudson.model.User;
import java.io.ObjectStreamException;
import java.util.Collections;
-import java.util.Date;
+import jenkins.agents.IOfflineCause;
import jenkins.model.Jenkins;
import org.jvnet.localizer.Localizable;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
@@ -49,26 +51,31 @@
* @since 1.320
*/
@ExportedBean
-public abstract class OfflineCause {
+public abstract class OfflineCause implements IOfflineCause {
protected final long timestamp = System.currentTimeMillis();
/**
- * Timestamp in which the event happened.
+ * {@inheritDoc}
*
* @since 1.612
*/
@Exported
+ @Override
public long getTimestamp() {
return timestamp;
}
/**
- * Same as {@link #getTimestamp()} but in a different type.
- *
- * @since 1.612
+ * @deprecated Only exists for backward compatibility.
+ * @see Computer#setTemporarilyOffline(boolean)
*/
- public final @NonNull Date getTime() {
- return new Date(timestamp);
+ @Deprecated
+ @Restricted(NoExternalUse.class)
+ public static class LegacyOfflineCause extends OfflineCause {
+ @Exported(name = "description") @Override
+ public String toString() {
+ return "";
+ }
}
/**
@@ -136,15 +143,15 @@ public static class UserCause extends SimpleOfflineCause {
// null when unknown
private /*final*/ @CheckForNull String userId;
+ private final String message;
+
public UserCause(@CheckForNull User user, @CheckForNull String message) {
- this(
- user != null ? user.getId() : null,
- message != null ? " : " + message : ""
- );
+ this(user != null ? user.getId() : null, message);
}
private UserCause(String userId, String message) {
- super(hudson.slaves.Messages._SlaveComputer_DisconnectedBy(userId != null ? userId : Jenkins.ANONYMOUS2.getName(), message));
+ super(hudson.slaves.Messages._SlaveComputer_DisconnectedBy(userId != null ? userId : Jenkins.ANONYMOUS2.getName(), message != null ? " : " + message : ""));
+ this.message = message;
this.userId = userId;
}
@@ -155,6 +162,13 @@ public User getUser() {
;
}
+ /**
+ * @return the message that was provided when the computer was taken offline
+ */
+ public String getMessage() {
+ return message;
+ }
+
// Storing the User in a filed was a mistake, switch to userId
private Object readResolve() throws ObjectStreamException {
if (user != null) {
@@ -170,6 +184,24 @@ private Object readResolve() throws ObjectStreamException {
}
return this;
}
+
+ @Override
+ @NonNull
+ public String getComputerIcon() {
+ return "symbol-computer-disconnected";
+ }
+
+ @Override
+ @NonNull
+ public String getComputerIconAltText() {
+ return "[temporarily offline by user]";
+ }
+
+ @NonNull
+ @Override
+ public String getIcon() {
+ return "symbol-person";
+ }
}
public static class ByCLI extends UserCause {
@@ -190,5 +222,28 @@ public static class IdleOfflineCause extends SimpleOfflineCause {
public IdleOfflineCause() {
super(hudson.slaves.Messages._RetentionStrategy_Demand_OfflineIdle());
}
+
+ @Override
+ @NonNull
+ public String getComputerIcon() {
+ return "symbol-computer-paused";
+ }
+
+ @Override
+ @NonNull
+ public String getComputerIconAltText() {
+ return "[will connect automatically whenever needed]";
+ }
+
+ @Override
+ @NonNull
+ public String getIcon() {
+ return "symbol-pause";
+ }
+
+ @Override
+ public String getStatusClass() {
+ return "info";
+ }
}
}
diff --git a/core/src/main/java/hudson/slaves/RetentionStrategy.java b/core/src/main/java/hudson/slaves/RetentionStrategy.java
index 687e44d5c172..6fe491cdaa87 100644
--- a/core/src/main/java/hudson/slaves/RetentionStrategy.java
+++ b/core/src/main/java/hudson/slaves/RetentionStrategy.java
@@ -272,6 +272,8 @@ public long check(final SlaveComputer c) {
logger.log(Level.INFO, "Launching computer {0} as it has been in demand for {1}",
new Object[]{c.getName(), Util.getTimeSpanString(demandMilliseconds)});
c.connect(false);
+ } else if (c.getOfflineCause() == null) {
+ c.setOfflineCause(new OfflineCause.IdleOfflineCause());
}
} else if (c.isIdle()) {
final long idleMilliseconds = System.currentTimeMillis() - c.getIdleStartMilliseconds();
diff --git a/core/src/main/java/hudson/slaves/SimpleScheduledRetentionStrategy.java b/core/src/main/java/hudson/slaves/SimpleScheduledRetentionStrategy.java
index ea90529b29b4..13a267ace665 100644
--- a/core/src/main/java/hudson/slaves/SimpleScheduledRetentionStrategy.java
+++ b/core/src/main/java/hudson/slaves/SimpleScheduledRetentionStrategy.java
@@ -242,6 +242,8 @@ public void run() {
c.disconnect(OfflineCause.create(Messages._SimpleScheduledRetentionStrategy_FinishedUpTime()));
}
}
+ } else {
+ c.setOfflineCause(new ScheduledOfflineCause());
}
return 0;
}
@@ -252,6 +254,24 @@ private synchronized boolean isOnlineScheduled() {
return (lastStart < now && lastStop > now) || (nextStart < now && nextStop > now);
}
+ public static class ScheduledOfflineCause extends OfflineCause.SimpleOfflineCause {
+ public ScheduledOfflineCause() {
+ super(Messages._SimpleScheduledRetentionStrategy_ScheduledOfflineCause_displayName());
+ }
+
+ @NonNull
+ @Override
+ public String getComputerIcon() {
+ return "symbol-computer-not-accepting";
+ }
+
+ @NonNull
+ @Override
+ public String getIcon() {
+ return "symbol-trigger";
+ }
+ }
+
@Extension @Symbol("schedule")
public static class DescriptorImpl extends Descriptor> {
@NonNull
diff --git a/core/src/main/java/hudson/tasks/LogRotator.java b/core/src/main/java/hudson/tasks/LogRotator.java
index ca95081b0385..0b47f035d809 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/FormApply.java b/core/src/main/java/hudson/util/FormApply.java
index 280a3051286a..3d5f458ec126 100644
--- a/core/src/main/java/hudson/util/FormApply.java
+++ b/core/src/main/java/hudson/util/FormApply.java
@@ -24,8 +24,10 @@
package hudson.util;
+import hudson.Functions;
import jakarta.servlet.ServletException;
import java.io.IOException;
+import jenkins.model.Jenkins;
import org.kohsuke.stapler.HttpResponses.HttpResponseException;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerRequest2;
@@ -51,7 +53,7 @@ public static HttpResponseException success(final String destination) {
public void generateResponse(StaplerRequest2 req, StaplerResponse2 rsp, Object node) throws IOException, ServletException {
if (isApply(req)) {
// if the submission is via 'apply', show a response in the notification bar
- applyResponse("notificationBar.show('" + Messages.HttpResponses_Saved() + "',notificationBar.SUCCESS)")
+ showNotification(Messages.HttpResponses_Saved(), NotificationType.SUCCESS)
.generateResponse(req, rsp, node);
} else {
rsp.sendRedirect(destination);
@@ -69,6 +71,7 @@ public static boolean isApply(StaplerRequest2 req) {
return Boolean.parseBoolean(req.getParameter("core:apply"));
}
+
/**
* @deprecated use {@link #isApply(StaplerRequest2)}
*/
@@ -82,7 +85,10 @@ public static boolean isApply(StaplerRequest req) {
*
* When the response HTML includes a JavaScript function in a pre-determined name, that function gets executed.
* This method generates such a response from JavaScript text.
+ *
+ * @deprecated use {@link #showNotification(String, NotificationType)} instead, which is CSP compatible version
*/
+ @Deprecated
public static HttpResponseException applyResponse(final String script) {
return new HttpResponseException() {
@Override
@@ -98,4 +104,36 @@ public void generateResponse(StaplerRequest2 req, StaplerResponse2 rsp, Object n
}
};
}
+
+ /**
+ * Generates the response for the asynchronous background form submission (AKA the Apply button),
+ * that will show a notification of certain type and with provided message.
+ *
+ * @param message a message to display in the popup. Only plain text is supported.
+ * @param notificationType type of notification. See {@link NotificationType} for supported types. Defines the notification
+ * color and the icon that will be shown.
+ *
+ * @since 2.482
+ */
+ public static HttpResponseException showNotification(final String message, final NotificationType notificationType) {
+ return new HttpResponseException() {
+ @Override
+ public void generateResponse(StaplerRequest2 req, StaplerResponse2 rsp, Object node) throws IOException {
+ rsp.setContentType("text/html;charset=UTF-8");
+ rsp.getWriter().println("");
+ }
+ };
+ }
+
+
+ /**
+ * Corresponds to types declared in index.js
+ */
+ public enum NotificationType {
+ SUCCESS,
+ WARNING,
+ ERROR
+ }
}
diff --git a/core/src/main/java/hudson/util/RobustCollectionConverter.java b/core/src/main/java/hudson/util/RobustCollectionConverter.java
index 64dbbc7d9e9a..f914d909be27 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 f845e38771cc..c802959d0d09 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 d1bc500003e1..686aad13c342 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/AgentProtocol.java b/core/src/main/java/jenkins/AgentProtocol.java
index d43398985810..1494dc60b183 100644
--- a/core/src/main/java/jenkins/AgentProtocol.java
+++ b/core/src/main/java/jenkins/AgentProtocol.java
@@ -7,8 +7,6 @@
import hudson.TcpSlaveAgentListener;
import java.io.IOException;
import java.net.Socket;
-import java.util.Set;
-import jenkins.model.Jenkins;
/**
* Pluggable Jenkins TCP agent protocol handler called from {@link TcpSlaveAgentListener}.
@@ -18,57 +16,31 @@
* Implementations of this extension point is singleton, and its {@link #handle(Socket)} method
* gets invoked concurrently whenever a new connection comes in.
*
- * Extending UI
- *
- * - description.jelly
- * - Optional protocol description
- * - deprecationCause.jelly
- * - Optional. If the protocol is marked as {@link #isDeprecated()},
- * clarifies the deprecation reason and provides extra documentation links
- *
- *
* @author Kohsuke Kawaguchi
* @since 1.467
* @see TcpSlaveAgentListener
*/
public abstract class AgentProtocol implements ExtensionPoint {
/**
- * Allow experimental {@link AgentProtocol} implementations to declare being opt-in.
- * Note that {@link Jenkins#setAgentProtocols(Set)} only records the protocols where the admin has made a
- * conscious decision thus:
- *
- * - if a protocol is opt-in, it records the admin enabling it
- * - if a protocol is opt-out, it records the admin disabling it
- *
- * Implementations should not transition rapidly from {@code opt-in -> opt-out -> opt-in}.
- * Implementations should never flip-flop: {@code opt-in -> opt-out -> opt-in -> opt-out} as that will basically
- * clear any preference that an admin has set. This latter restriction should be ok as we only ever will be
- * adding new protocols and retiring old ones.
- *
- * @return {@code true} if the protocol requires explicit opt-in.
- * @since 2.16
- * @see Jenkins#setAgentProtocols(Set)
+ * @deprecated no longer used
*/
+ @Deprecated
public boolean isOptIn() {
return false;
}
+
/**
- * Allow essential {@link AgentProtocol} implementations (basically {@link TcpSlaveAgentListener.PingAgentProtocol})
- * to be always enabled.
- *
- * @return {@code true} if the protocol can never be disabled.
- * @since 2.16
+ * @deprecated no longer used
*/
-
+ @Deprecated
public boolean isRequired() {
return false;
}
/**
- * Checks if the protocol is deprecated.
- *
- * @since 2.75
+ * @deprecated no longer used
*/
+ @Deprecated
public boolean isDeprecated() {
return false;
}
@@ -79,17 +51,15 @@ public boolean isDeprecated() {
* This is a short string that consists of printable ASCII chars. Sent by the client to select the protocol.
*
* @return
- * null to be disabled. This is useful for avoiding getting used
- * until the protocol is properly configured.
+ * null to be disabled
*/
+ @CheckForNull
public abstract String getName();
/**
- * Returns the human readable protocol display name.
- *
- * @return the human readable protocol display name.
- * @since 2.16
+ * @deprecated no longer used
*/
+ @Deprecated
public String getDisplayName() {
return getName();
}
diff --git a/core/src/main/java/jenkins/agents/IOfflineCause.java b/core/src/main/java/jenkins/agents/IOfflineCause.java
new file mode 100644
index 000000000000..b1c32820e848
--- /dev/null
+++ b/core/src/main/java/jenkins/agents/IOfflineCause.java
@@ -0,0 +1,108 @@
+/*
+ * 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.agents;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.Util;
+import java.util.Date;
+import java.util.Objects;
+import jenkins.model.IComputer;
+
+/**
+ * Represents a cause that puts a {@linkplain IComputer#isOffline() computer offline}.
+ * @since 2.483
+ */
+public interface IOfflineCause {
+ /**
+ * @return The icon to use for the computer that has this offline cause. It will be displayed in the build executor status widget, as well as in nodes list screen.
+ */
+ @NonNull
+ default String getComputerIcon() {
+ return "symbol-computer-offline";
+ }
+
+ /**
+ * @return The alt text for the icon returned by {@link #getComputerIcon()}.
+ */
+ @NonNull
+ default String getComputerIconAltText() {
+ return "[offline]";
+ }
+
+ /**
+ * @return The icon to render this offline cause. It will be displayed in the build executor status widget.
+ */
+ @NonNull
+ default String getIcon() {
+ return "symbol-error";
+ }
+
+ /**
+ * @return The reason why this offline cause exists.
+ *
+ * For implementers: this can use HTML formatting, so make sure to only include trusted content.
+ */
+ @NonNull
+ default String getReason() {
+ // fetch the localized string for "Disconnected By"
+ String gsub_base = hudson.slaves.Messages.SlaveComputer_DisconnectedBy("", "");
+ // regex to remove commented reason base string
+ String gsub1 = "^" + gsub_base + "[\\w\\W]* \\: ";
+ // regex to remove non-commented reason base string
+ String gsub2 = "^" + gsub_base + "[\\w\\W]*";
+ return Objects.requireNonNull(Util.escape(toString().replaceAll(gsub1, "").replaceAll(gsub2, "")));
+ }
+
+ /**
+ * @return A short message (one word) that summarizes the offline cause.
+ *
+ *
+ * For implementers: this can use HTML formatting, so make sure to only include trusted content.
+ */
+ @NonNull
+ default String getMessage() {
+ return Messages.IOfflineCause_offline();
+ }
+
+ /**
+ * @return the CSS class name that should be used to render the status.
+ */
+ @SuppressWarnings("unused") // jelly
+ default String getStatusClass() {
+ return "warning";
+ }
+
+ /**
+ * Timestamp in which the event happened.
+ */
+ long getTimestamp();
+
+ /**
+ * Same as {@link #getTimestamp()} but in a different type.
+ */
+ @NonNull
+ default Date getTime() {
+ return new Date(getTimestamp());
+ }
+}
diff --git a/core/src/main/java/jenkins/cli/SafeRestartCommand.java b/core/src/main/java/jenkins/cli/SafeRestartCommand.java
index 54c624bbba27..4c1a8009e44d 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 1a42c0577a47..ad33643879f5 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 000000000000..f7559ff85799
--- /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 7ddea84c4241..6d2c58b44774 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 000000000000..37fe0af98535
--- /dev/null
+++ b/core/src/main/java/jenkins/model/IComputer.java
@@ -0,0 +1,227 @@
+/*
+ * 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 jenkins.agents.IOfflineCause;
+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;
+ }
+
+ /**
+ * @return the offline cause if the computer is offline.
+ * @since 2.483
+ */
+ IOfflineCause getOfflineCause();
+
+ /**
+ * 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
+ default String getOfflineCauseReason() {
+ if (getOfflineCause() == null) {
+ return "";
+ }
+ return getOfflineCause().getReason();
+ }
+
+ /**
+ * @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()
+ */
+ default String getIcon() {
+ // The computer is not accepting tasks, e.g. because the availability demands it being offline.
+ if (!isAcceptingTasks()) {
+ return "symbol-computer-not-accepting";
+ }
+ var offlineCause = getOfflineCause();
+ if (offlineCause != null) {
+ return offlineCause.getComputerIcon();
+ }
+ // The computer is not connected or it is temporarily offline due to a node monitor
+ if (isOffline()) return "symbol-computer-offline";
+ return "symbol-computer";
+ }
+
+ /**
+ * Returns the alternative text for the computer icon.
+ */
+ @SuppressWarnings("unused") // jelly
+ default String getIconAltText() {
+ if (!isAcceptingTasks()) {
+ return "[suspended]";
+ }
+ var offlineCause = getOfflineCause();
+ if (offlineCause != null) {
+ return offlineCause.getComputerIconAltText();
+ }
+ // There is a "technical" reason the computer will not accept new builds
+ if (isOffline()) return "[offline]";
+ return "[online]";
+ }
+
+ default String getTooltip() {
+ var offlineCause = getOfflineCause();
+ if (offlineCause != null) {
+ return offlineCause.toString();
+ } else {
+ return "";
+ }
+ }
+
+ /**
+ * 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 000000000000..5f959a158faa
--- /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 000000000000..1aec72db5be9
--- /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 705044cd8fa0..bedbea0af7c4 100644
--- a/core/src/main/java/jenkins/model/Jenkins.java
+++ b/core/src/main/java/jenkins/model/Jenkins.java
@@ -656,47 +656,6 @@ private static int getSlaveAgentPortInitialValue(int def) {
*/
private static final boolean SLAVE_AGENT_PORT_ENFORCE = SystemProperties.getBoolean(Jenkins.class.getName() + ".slaveAgentPortEnforce", false);
- /**
- * The TCP agent protocols that are explicitly disabled (we store the disabled ones so that newer protocols
- * are enabled by default). Will be {@code null} instead of empty to simplify XML format.
- *
- * @since 2.16
- */
- @CheckForNull
- @GuardedBy("this")
- private List disabledAgentProtocols;
- /**
- * @deprecated Just a temporary buffer for XSTream migration code from JENKINS-39465, do not use
- */
- @Deprecated
- private transient String[] _disabledAgentProtocols;
-
- /**
- * The TCP agent protocols that are {@link AgentProtocol#isOptIn()} and explicitly enabled.
- * Will be {@code null} instead of empty to simplify XML format.
- *
- * @since 2.16
- */
- @CheckForNull
- @GuardedBy("this")
- private List enabledAgentProtocols;
- /**
- * @deprecated Just a temporary buffer for XSTream migration code from JENKINS-39465, do not use
- */
- @Deprecated
- private transient String[] _enabledAgentProtocols;
-
- /**
- * The TCP agent protocols that are enabled. Built from {@link #disabledAgentProtocols} and
- * {@link #enabledAgentProtocols}.
- *
- * @since 2.16
- * @see #setAgentProtocols(Set)
- * @see #getAgentProtocols()
- */
- @GuardedBy("this")
- private transient Set agentProtocols;
-
/**
* Whitespace-separated labels assigned to the built-in node as a {@link Node}.
*/
@@ -1012,6 +971,7 @@ protected Jenkins(File root, ServletContext context, PluginManager pluginManager
adjuncts = new AdjunctManager(getServletContext(), pluginManager.uberClassLoader, "adjuncts/" + SESSION_HASH, TimeUnit.DAYS.toMillis(365));
ClassFilterImpl.register();
+ LOGGER.info("Starting version " + getVersion());
// initialization consists of ...
executeReactor(is,
@@ -1095,18 +1055,6 @@ protected Object readResolve() {
if (SLAVE_AGENT_PORT_ENFORCE) {
slaveAgentPort = getSlaveAgentPortInitialValue(slaveAgentPort);
}
- synchronized (this) {
- if (disabledAgentProtocols == null && _disabledAgentProtocols != null) {
- disabledAgentProtocols = Arrays.asList(_disabledAgentProtocols);
- _disabledAgentProtocols = null;
- }
- if (enabledAgentProtocols == null && _enabledAgentProtocols != null) {
- enabledAgentProtocols = Arrays.asList(_enabledAgentProtocols);
- _enabledAgentProtocols = null;
- }
- // Invalidate the protocols cache after the reload
- agentProtocols = null;
- }
// no longer persisted
installStateName = null;
@@ -1281,81 +1229,15 @@ private void forceSetSlaveAgentPort(int port) throws IOException {
*/
@NonNull
public synchronized Set getAgentProtocols() {
- if (agentProtocols == null) {
- Set result = new TreeSet<>();
- Set disabled = new TreeSet<>();
- for (String p : Util.fixNull(disabledAgentProtocols)) {
- disabled.add(p.trim());
- }
- Set enabled = new TreeSet<>();
- for (String p : Util.fixNull(enabledAgentProtocols)) {
- enabled.add(p.trim());
- }
- for (AgentProtocol p : AgentProtocol.all()) {
- String name = p.getName();
- if (name != null && (p.isRequired()
- || (!disabled.contains(name) && (!p.isOptIn() || enabled.contains(name))))) {
- result.add(name);
- }
- }
- /*
- * An empty result is almost never valid, but it can happen due to JENKINS-70206. Since we know the result
- * is likely incorrect, at least decline to cache it so that a correct result can be computed later on
- * rather than continuing to deliver the incorrect result indefinitely.
- */
- if (!result.isEmpty()) {
- agentProtocols = result;
- }
- return result;
- }
- return agentProtocols;
+ return AgentProtocol.all().stream().map(AgentProtocol::getName).filter(Objects::nonNull).collect(Collectors.toCollection(TreeSet::new));
}
/**
- * Sets the enabled agent protocols.
- *
- * @param protocols the enabled agent protocols.
- * @since 2.16
+ * @deprecated No longer does anything.
*/
+ @Deprecated
public synchronized void setAgentProtocols(@NonNull Set protocols) {
- Set disabled = new TreeSet<>();
- Set enabled = new TreeSet<>();
- for (AgentProtocol p : AgentProtocol.all()) {
- String name = p.getName();
- if (name != null && !p.isRequired()) {
- // we want to record the protocols where the admin has made a conscious decision
- // thus, if a protocol is opt-in, we record the admin enabling it
- // if a protocol is opt-out, we record the admin disabling it
- // We should not transition rapidly from opt-in -> opt-out -> opt-in
- // the scenario we want to have work is:
- // 1. We introduce a new protocol, it starts off as opt-in. Some admins decide to test and opt-in
- // 2. We decide that the protocol is ready for general use. It gets marked as opt-out. Any admins
- // that took part in early testing now have their config switched to not mention the new protocol
- // at all when they save their config as the protocol is now opt-out. Any admins that want to
- // disable it can do so and will have their preference recorded.
- // 3. We decide that the protocol needs to be retired. It gets switched back to opt-in. At this point
- // the initial opt-in admins, assuming they visited an upgrade to a controller with step 2, will
- // have the protocol disabled for them. This is what we want. If they didn't upgrade to a controller
- // with step 2, well there is not much we can do to differentiate them from somebody who is upgrading
- // from a previous step 3 controller and had needed to keep the protocol turned on.
- //
- // What we should never do is flip-flop: opt-in -> opt-out -> opt-in -> opt-out as that will basically
- // clear any preference that an admin has set, but this should be ok as we only ever will be
- // adding new protocols and retiring old ones.
- if (p.isOptIn()) {
- if (protocols.contains(name)) {
- enabled.add(name);
- }
- } else {
- if (!protocols.contains(name)) {
- disabled.add(name);
- }
- }
- }
- }
- disabledAgentProtocols = disabled.isEmpty() ? null : new ArrayList<>(disabled);
- enabledAgentProtocols = enabled.isEmpty() ? null : new ArrayList<>(enabled);
- agentProtocols = null;
+ LOGGER.log(Level.WARNING, null, new IllegalStateException("Jenkins.agentProtocols no longer configurable"));
}
private void launchTcpSlaveAgentListener() throws IOException {
@@ -2071,7 +1953,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 +1962,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 +2129,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 +2369,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 +3704,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);
@@ -4151,7 +4041,7 @@ public synchronized void doConfigExecutorsSubmit(StaplerRequest2 req, StaplerRes
updateComputerList();
- rsp.sendRedirect(req.getContextPath() + '/' + toComputer().getUrl()); // back to the computer page
+ FormApply.success(req.getContextPath() + '/' + toComputer().getUrl()).generateResponse(req, rsp, null);
}
/**
@@ -4659,13 +4549,13 @@ public void generateNotFoundResponse(StaplerRequest2 req, StaplerResponse2 rsp)
*/
@Deprecated(since = "2.414")
public HttpResponse doSafeRestart(StaplerRequest req) throws IOException, ServletException, RestartNotSupportedException {
- return doSafeRestart(StaplerRequest.toStaplerRequest2(req), null);
+ return doSafeRestart(req != null ? StaplerRequest.toStaplerRequest2(req) : null, null);
}
/**
* 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 +4574,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(req != null ? StaplerRequest.toStaplerRequest2(req) : null, 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 +5380,7 @@ protected MasterComputer() {
* Returns "" to match with {@link Jenkins#getNodeName()}.
*/
@Override
+ @NonNull
public String getName() {
return "";
}
@@ -5497,6 +5402,7 @@ public String getCaption() {
}
@Override
+ @NonNull
public String getUrl() {
return "computer/(built-in)/";
}
@@ -5975,8 +5881,6 @@ public boolean shouldShowStackTrace() {
// for backward compatibility with <1.75, recognize the tag name "view" as well.
XSTREAM.alias("view", ListView.class);
XSTREAM.alias("listView", ListView.class);
- XSTREAM.addImplicitArray(Jenkins.class, "_disabledAgentProtocols", "disabledAgentProtocol");
- XSTREAM.addImplicitArray(Jenkins.class, "_enabledAgentProtocols", "enabledAgentProtocol");
XSTREAM2.addCriticalField(Jenkins.class, "securityRealm");
XSTREAM2.addCriticalField(Jenkins.class, "authorizationStrategy");
// this seems to be necessary to force registration of converter early enough
diff --git a/core/src/main/java/jenkins/model/ModelObjectWithContextMenu.java b/core/src/main/java/jenkins/model/ModelObjectWithContextMenu.java
index f46280fef0c0..11eea481106a 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 8f4c0e5eafc6..a01c3fc342b5 100644
--- a/core/src/main/java/jenkins/model/Nodes.java
+++ b/core/src/main/java/jenkins/model/Nodes.java
@@ -43,10 +43,9 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.HashSet;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentMap;
@@ -114,26 +113,31 @@ public List getNodes() {
* @throws IOException if the new list of nodes could not be persisted.
*/
public void setNodes(final @NonNull Collection extends Node> nodes) throws IOException {
- Set toRemove = new HashSet<>();
- Queue.withLock(new Runnable() {
- @Override
- public void run() {
- toRemove.addAll(Nodes.this.nodes.keySet());
- for (Node n : nodes) {
- final String name = n.getNodeName();
+ Map toRemove = new HashMap<>();
+ Queue.withLock(() -> {
+ toRemove.putAll(Nodes.this.nodes);
+ for (var node : nodes) {
+ final var name = node.getNodeName();
+ var oldNode = toRemove.get(name);
+ if (oldNode != null) {
+ NodeListener.fireOnUpdated(oldNode, node);
toRemove.remove(name);
- Nodes.this.nodes.put(name, n);
- n.onLoad(Nodes.this, name);
+ } else {
+ NodeListener.fireOnCreated(node);
}
- Nodes.this.nodes.keySet().removeAll(toRemove);
- jenkins.updateComputerList();
- jenkins.trimLabels();
+ Nodes.this.nodes.put(name, node);
+ node.onLoad(Nodes.this, name);
}
+ Nodes.this.nodes.keySet().removeAll(toRemove.keySet());
+ jenkins.updateComputerList();
+ jenkins.trimLabels();
});
save();
- for (String name : toRemove) {
- LOGGER.fine(() -> "deleting " + new File(getRootDir(), name));
- Util.deleteRecursive(new File(getRootDir(), name));
+ for (var deletedNode : toRemove.values()) {
+ NodeListener.fireOnDeleted(deletedNode);
+ var nodeName = deletedNode.getNodeName();
+ LOGGER.fine(() -> "deleting " + new File(getRootDir(), nodeName));
+ Util.deleteRecursive(new File(getRootDir(), nodeName));
}
}
@@ -295,6 +299,7 @@ public void run() {
jenkins.trimLabels(node);
}
NodeListener.fireOnDeleted(node);
+ SaveableListener.fireOnDeleted(node, getConfigFile(node));
}
}
diff --git a/core/src/main/java/jenkins/model/lazy/LazyBuildMixIn.java b/core/src/main/java/jenkins/model/lazy/LazyBuildMixIn.java
index c73c5cbf80f1..d6329ed13ed5 100644
--- a/core/src/main/java/jenkins/model/lazy/LazyBuildMixIn.java
+++ b/core/src/main/java/jenkins/model/lazy/LazyBuildMixIn.java
@@ -109,8 +109,8 @@ public void onLoad(ItemGroup extends Item> parent, String name) throws IOExcep
int max = _builds.maxNumberOnDisk();
int next = asJob().getNextBuildNumber();
if (next <= max) {
- LOGGER.log(Level.WARNING, "JENKINS-27530: improper nextBuildNumber {0} detected in {1} with highest build number {2}; adjusting", new Object[] {next, asJob(), max});
- asJob().updateNextBuildNumber(max + 1);
+ LOGGER.log(Level.FINE, "nextBuildNumber {0} detected in {1} with highest build number {2}; adjusting", new Object[] {next, asJob(), max});
+ asJob().fastUpdateNextBuildNumber(max + 1);
}
RunMap currentBuilds = this.builds;
if (parent != null) {
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 000000000000..37b0e62150de
--- /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 a2d954fbc459..b278f4d24c93 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/monitor/OperatingSystemEndOfLifeAdminMonitor.java b/core/src/main/java/jenkins/monitor/OperatingSystemEndOfLifeAdminMonitor.java
index 0ff40d018225..50cf3c9da6f0 100644
--- a/core/src/main/java/jenkins/monitor/OperatingSystemEndOfLifeAdminMonitor.java
+++ b/core/src/main/java/jenkins/monitor/OperatingSystemEndOfLifeAdminMonitor.java
@@ -145,7 +145,7 @@ void readOperatingSystemList(String initialOperatingSystemJson) throws IOExcepti
}
LOGGER.log(Level.FINE, "Matched operating system {0}", name);
- if (startDate.isBefore(LocalDate.now())) {
+ if (!startDate.isAfter(LocalDate.now())) {
this.operatingSystemName = name;
this.documentationUrl = buildDocumentationUrl(this.operatingSystemName);
this.endOfLifeDate = endOfLife.toString();
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 000000000000..7baec1962f48
--- /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 000000000000..91a79f354d71
--- /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/java/jenkins/security/stapler/TypedFilter.java b/core/src/main/java/jenkins/security/stapler/TypedFilter.java
index eabce3b2dfc9..5851c5e47269 100644
--- a/core/src/main/java/jenkins/security/stapler/TypedFilter.java
+++ b/core/src/main/java/jenkins/security/stapler/TypedFilter.java
@@ -5,8 +5,6 @@
import hudson.ExtensionList;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
-import java.util.HashMap;
-import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.util.SystemProperties;
@@ -25,8 +23,6 @@
public class TypedFilter implements FieldRef.Filter, FunctionList.Filter {
private static final Logger LOGGER = Logger.getLogger(TypedFilter.class.getName());
- private static final Map, Boolean> staplerCache = new HashMap<>();
-
private boolean isClassAcceptable(Class> clazz) {
if (clazz.isArray()) {
// special case to allow klass.isArray() dispatcher
@@ -46,31 +42,23 @@ private boolean isClassAcceptable(Class> clazz) {
return false;
}
}
- return SKIP_TYPE_CHECK || isStaplerRelevantCached(clazz);
+ return SKIP_TYPE_CHECK || isStaplerRelevant.get(clazz);
}
- private static boolean isStaplerRelevantCached(@NonNull Class> clazz) {
- if (staplerCache.containsKey(clazz)) {
- return staplerCache.get(clazz);
+ private static final ClassValue isStaplerRelevant = new ClassValue<>() {
+ @Override
+ protected Boolean computeValue(Class> clazz) {
+ return isSpecificClassStaplerRelevant(clazz) || isSuperTypesStaplerRelevant(clazz);
}
- boolean ret = isStaplerRelevant(clazz);
-
- staplerCache.put(clazz, ret);
- return ret;
- }
-
- @Restricted(NoExternalUse.class)
- public static boolean isStaplerRelevant(@NonNull Class> clazz) {
- return isSpecificClassStaplerRelevant(clazz) || isSuperTypesStaplerRelevant(clazz);
- }
+ };
private static boolean isSuperTypesStaplerRelevant(@NonNull Class> clazz) {
Class> superclass = clazz.getSuperclass();
- if (superclass != null && isStaplerRelevantCached(superclass)) {
+ if (superclass != null && isStaplerRelevant.get(superclass)) {
return true;
}
for (Class> interfaceClass : clazz.getInterfaces()) {
- if (isStaplerRelevantCached(interfaceClass)) {
+ if (isStaplerRelevant.get(interfaceClass)) {
return true;
}
}
diff --git a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol4.java b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol4.java
index 54c1402927a9..87eb26daeb4c 100644
--- a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol4.java
+++ b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol4.java
@@ -137,16 +137,6 @@ private char[] constructPassword() {
return "password".toCharArray();
}
- @Override
- public boolean isOptIn() {
- return false;
- }
-
- @Override
- public String getDisplayName() {
- return Messages.JnlpSlaveAgentProtocol4_displayName();
- }
-
@Override
public String getName() {
return "JNLP4-connect"; // matches JnlpProtocol4Handler.getName
diff --git a/core/src/main/java/jenkins/widgets/ExecutorsWidget.java b/core/src/main/java/jenkins/widgets/ExecutorsWidget.java
index c660d4e03911..3577d21af48b 100644
--- a/core/src/main/java/jenkins/widgets/ExecutorsWidget.java
+++ b/core/src/main/java/jenkins/widgets/ExecutorsWidget.java
@@ -10,6 +10,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import jenkins.model.IComputer;
import jenkins.model.Jenkins;
import org.jenkinsci.Symbol;
@@ -23,9 +24,9 @@
*/
public class ExecutorsWidget extends Widget {
private final String ownerUrl;
- private final List computers;
+ private final List computers;
- public ExecutorsWidget(@NonNull String ownerUrl, @NonNull List computers) {
+ public ExecutorsWidget(@NonNull String ownerUrl, @NonNull List extends IComputer> computers) {
this.ownerUrl = ownerUrl;
this.computers = new ArrayList<>(computers);
}
@@ -35,7 +36,7 @@ protected String getOwnerUrl() {
return ownerUrl;
}
- public List getComputers() {
+ public List extends IComputer> getComputers() {
return Collections.unmodifiableList(computers);
}
@@ -92,7 +93,7 @@ public Class widgetType() {
@NonNull
@Override
public Collection createFor(@NonNull ComputerSet target) {
- return List.of(new ExecutorsWidget("computer/", List.of(target.get_all())));
+ return List.of(new ExecutorsWidget("computer/", new ArrayList<>(target.getComputers())));
}
}
}
diff --git a/core/src/main/resources/hudson/Messages.properties b/core/src/main/resources/hudson/Messages.properties
index 99c77ebbed46..a15f31703cd0 100644
--- a/core/src/main/resources/hudson/Messages.properties
+++ b/core/src/main/resources/hudson/Messages.properties
@@ -127,7 +127,6 @@ PluginWrapper.Plugin.Has.Dependent=The plugin ''{0}'' has, at least, one depende
PluginWrapper.Plugin.Disabled=Plugin ''{0}'' disabled
PluginWrapper.NoSuchPlugin=No such plugin found with the name ''{0}''
PluginWrapper.Error.Disabling=There was an error disabling the ''{0}'' plugin. Error: ''{1}''
-TcpSlaveAgentListener.PingAgentProtocol.displayName=Ping protocol
ProxyConfigurationManager.DisplayName=Proxy Configuration
ProxyConfigurationManager.Description=Configure the http proxy used by Jenkins
diff --git a/core/src/main/resources/hudson/Messages_bg.properties b/core/src/main/resources/hudson/Messages_bg.properties
index 98b816053e44..e741c7400a06 100644
--- a/core/src/main/resources/hudson/Messages_bg.properties
+++ b/core/src/main/resources/hudson/Messages_bg.properties
@@ -105,9 +105,6 @@ PluginWrapper.disabledAndObsolete=\
# {0} is disabled. To fix, enable it.
PluginWrapper.disabled=\
„{0}“ е изключена. Включете я.
-# Ping protocol
-TcpSlaveAgentListener.PingAgentProtocol.displayName=\
- Протокол „ping“
# {0} v{1} failed to load. Fix this plugin first.
PluginWrapper.failed_to_load_dependency=\
„{0}“, версия {1} не се зареди. Оправете приставката.
diff --git a/core/src/main/resources/hudson/Messages_de.properties b/core/src/main/resources/hudson/Messages_de.properties
index e4068a20b58a..d524a518cd44 100644
--- a/core/src/main/resources/hudson/Messages_de.properties
+++ b/core/src/main/resources/hudson/Messages_de.properties
@@ -75,5 +75,3 @@ AboutJenkins.DisplayName=Über Jenkins
AboutJenkins.Description=Versions- und Lizenzinformationen anzeigen.
Functions.NoExceptionDetails=Keine Details zum Ausnahmefehler
-
-TcpSlaveAgentListener.PingAgentProtocol.displayName=Ping-Protokoll
diff --git a/core/src/main/resources/hudson/Messages_es.properties b/core/src/main/resources/hudson/Messages_es.properties
index 298ef23821d8..47d3b400c747 100644
--- a/core/src/main/resources/hudson/Messages_es.properties
+++ b/core/src/main/resources/hudson/Messages_es.properties
@@ -117,4 +117,3 @@ PluginWrapper.Plugin.Has.Dependant=El plugin {0} tiene, al menos, un plugin depe
PluginWrapper.Plugin.Disabled=Plugin {0} deshabilitado
PluginWrapper.NoSuchPlugin=No se encuentra un plugin con el nombre {0}
PluginWrapper.Error.Disabling=Hubo un error al desactivar el plugin ''{0}''. Error: ''{1}''
-TcpSlaveAgentListener.PingAgentProtocol.displayName=Protocolo ping
diff --git a/core/src/main/resources/hudson/Messages_fr.properties b/core/src/main/resources/hudson/Messages_fr.properties
index 1e0286deddc5..06e739d3b479 100644
--- a/core/src/main/resources/hudson/Messages_fr.properties
+++ b/core/src/main/resources/hudson/Messages_fr.properties
@@ -126,4 +126,3 @@ PluginWrapper.Plugin.Has.Dependent=Le plugin "{0}" a au moins un plugin dépenda
PluginWrapper.Plugin.Disabled=Plugin "{0}" désactivé
PluginWrapper.NoSuchPlugin=Aucun plugin trouvé avec le nom "{0}"
PluginWrapper.Error.Disabling=Une erreur a été relevée lors de la désactivation du plugin "{0}". Erreur : "{1}"
-TcpSlaveAgentListener.PingAgentProtocol.displayName=Protocole de ping
diff --git a/core/src/main/resources/hudson/Messages_it.properties b/core/src/main/resources/hudson/Messages_it.properties
index 2747e2d39366..d63200aabd1e 100644
--- a/core/src/main/resources/hudson/Messages_it.properties
+++ b/core/src/main/resources/hudson/Messages_it.properties
@@ -109,7 +109,6 @@ ProxyConfiguration.MalformedTestUrl=URL di prova {0} malformato.
ProxyConfiguration.NonTLSWarning=Jenkins supporta solo l''utilizzo di una connessione http al proxy. Le credenziali potrebbero essere esposte a qualcuno sulla stessa rete.
ProxyConfiguration.Success=Connessione riuscita (codice {0})
ProxyConfiguration.TestUrlRequired=È richiesto un URL di prova.
-TcpSlaveAgentListener.PingAgentProtocol.displayName=Protocollo ping
Util.day={0} g
Util.hour={0} h
Util.millisecond={0} ms
diff --git a/core/src/main/resources/hudson/Messages_pt_BR.properties b/core/src/main/resources/hudson/Messages_pt_BR.properties
index 0a809578524e..d67bdcba599c 100644
--- a/core/src/main/resources/hudson/Messages_pt_BR.properties
+++ b/core/src/main/resources/hudson/Messages_pt_BR.properties
@@ -57,7 +57,6 @@ PluginWrapper.missing=Não foi possível encontrar {0} v{1}. Para corrigir, inst
Functions.NoExceptionDetails=Sem detalhes da exception
FilePath.validateAntFileMask.whitespaceSeparator=Espaços em branco não podem mais serem utilizados como separador. Por \
favor use ", " como separadores.
-TcpSlaveAgentListener.PingAgentProtocol.displayName=Protocolo de ping
PluginWrapper.PluginWrapperAdministrativeMonitor.DisplayName=Falha ao carregar a extensão
ProxyConfiguration.MalformedTestUrl=URL de teste {0} inválida.
FilePath.TildaDoesntWork="~" é suportado apenas em um shell Unix e em nenhum outro lugar.
diff --git a/core/src/main/resources/hudson/Messages_sr.properties b/core/src/main/resources/hudson/Messages_sr.properties
index 3c266c5d67a3..414a84b3a41b 100644
--- a/core/src/main/resources/hudson/Messages_sr.properties
+++ b/core/src/main/resources/hudson/Messages_sr.properties
@@ -40,4 +40,3 @@ PluginWrapper.disabledAndObsolete={0}, верзија {1} је онемогућ
PluginWrapper.disabled={0}, верзија {1} је онемогућено. Молимо вас, омогућите ову модулу.
PluginWrapper.obsolete={0}, верзија {1} је старије него што је подржано. Инсталирајте верзију {2} или новије.
PluginWrapper.obsoleteCore=Морате ажурирати Jenkins са верзије {0} на {1} или новије да би могли користити ову модулу.
-TcpSlaveAgentListener.PingAgentProtocol.displayName=Протокол 'ping'
diff --git a/core/src/main/resources/hudson/Messages_sv_SE.properties b/core/src/main/resources/hudson/Messages_sv_SE.properties
index fdfa6ee3a524..f4a977e99098 100644
--- a/core/src/main/resources/hudson/Messages_sv_SE.properties
+++ b/core/src/main/resources/hudson/Messages_sv_SE.properties
@@ -125,7 +125,6 @@ PluginWrapper.Plugin.Has.Dependent=Insticksprogrammet ''{0}'' har minst ett bero
PluginWrapper.Plugin.Disabled=Insticksprogrammet ''{0}'' inaktiverades
PluginWrapper.NoSuchPlugin=Inget insticksprogram med namnet ''{0}'' hittades
PluginWrapper.Error.Disabling=Ett fel uppstod när insticksprogrammet ''{0}'' inaktiverades. Fel: ''{1}''
-TcpSlaveAgentListener.PingAgentProtocol.displayName=Ping-protokoll
ProxyConfigurationManager.DisplayName=Proxykonfiguration
ProxyConfigurationManager.Description=Konfigurera http-proxyn som Jenkins använder
diff --git a/core/src/main/resources/hudson/Messages_zh_TW.properties b/core/src/main/resources/hudson/Messages_zh_TW.properties
index 708613e7e603..cc5fb76cb059 100644
--- a/core/src/main/resources/hudson/Messages_zh_TW.properties
+++ b/core/src/main/resources/hudson/Messages_zh_TW.properties
@@ -96,7 +96,6 @@ PluginWrapper.Plugin.Has.Dependent=外掛「{0}」有至少一個相依性外掛
PluginWrapper.Plugin.Disabled=已停用外掛「{0}」
PluginWrapper.NoSuchPlugin=找不到名為「{0}」的外掛
PluginWrapper.Error.Disabling=停用外掛「{0}」時發生錯誤,錯誤\: 「{1}」
-TcpSlaveAgentListener.PingAgentProtocol.displayName=Ping 協定
PluginManager.emptyUpdateSiteUrl=更新站台網址不得為空,請輸入網址
PluginManager.connectionFailed=無法連線至這個URL
diff --git a/core/src/main/resources/hudson/diagnosis/ReverseProxySetupMonitor/message_tr.properties b/core/src/main/resources/hudson/diagnosis/ReverseProxySetupMonitor/message_tr.properties
index 2ac4acc9ebbe..2de2187639d5 100644
--- a/core/src/main/resources/hudson/diagnosis/ReverseProxySetupMonitor/message_tr.properties
+++ b/core/src/main/resources/hudson/diagnosis/ReverseProxySetupMonitor/message_tr.properties
@@ -1,4 +1,4 @@
# This file is under the MIT License by authors
-More\ Info=Daha fazla Bilgi
+More\ Info=Daha Fazla Bilgi
blurb=Ters vekil sunucu yapılandırmanız bozuk görünüyor.
diff --git a/core/src/main/resources/hudson/logging/LogRecorder/configure.jelly b/core/src/main/resources/hudson/logging/LogRecorder/configure.jelly
index b49ccc278858..ef7c6f3781b2 100644
--- a/core/src/main/resources/hudson/logging/LogRecorder/configure.jelly
+++ b/core/src/main/resources/hudson/logging/LogRecorder/configure.jelly
@@ -66,12 +66,8 @@ THE SOFTWARE.
-
-
-
-
+
-
diff --git a/core/src/main/resources/hudson/model/AllView/noJob_tr.properties b/core/src/main/resources/hudson/model/AllView/noJob_tr.properties
index f11e7258733e..31dd63cf187c 100644
--- a/core/src/main/resources/hudson/model/AllView/noJob_tr.properties
+++ b/core/src/main/resources/hudson/model/AllView/noJob_tr.properties
@@ -29,6 +29,7 @@ setUpDistributedBuilds=Dağıtılmış bir yapılandırma kurun
setUpAgent=Bir ajan kur
setUpCloud=Bir bulut ayarla
learnMoreDistributedBuilds=Dağıtılmış yapılandırmalar hakkında daha fazla bilgi edinin
+thisFolderIsEmpty=Bu klasör boş
startBuilding=Yazılım projenizi yapılandırmaya başlayın
diff --git a/core/src/main/resources/hudson/model/Computer/configure.jelly b/core/src/main/resources/hudson/model/Computer/configure.jelly
index bd33078013cd..ce76f5fb2d3b 100644
--- a/core/src/main/resources/hudson/model/Computer/configure.jelly
+++ b/core/src/main/resources/hudson/model/Computer/configure.jelly
@@ -46,15 +46,8 @@ THE SOFTWARE.
-
-
-
-
-
+
-
-
-
diff --git a/core/src/main/resources/hudson/model/Computer/setOfflineCause.jelly b/core/src/main/resources/hudson/model/Computer/setOfflineCause.jelly
index 587057a464e1..8bac3c7e27c7 100644
--- a/core/src/main/resources/hudson/model/Computer/setOfflineCause.jelly
+++ b/core/src/main/resources/hudson/model/Computer/setOfflineCause.jelly
@@ -34,7 +34,7 @@ THE SOFTWARE.
${%blurb}