* Any exception indicates the deletion has failed, but {@link AbortException} would prevent the caller
- * from showing the stack trace. This
+ * from showing the stack trace.
+ * @see ItemDeletion
*/
@Override
public void delete() throws IOException, InterruptedException {
checkPermission(DELETE);
+ ItemListener.checkBeforeDelete(this);
boolean responsibleForAbortingBuilds = !ItemDeletion.contains(this);
boolean ownsRegistration = ItemDeletion.register(this);
if (!ownsRegistration && ItemDeletion.isRegistered(this)) {
@@ -719,87 +713,7 @@ public void delete() throws IOException, InterruptedException {
try {
// if a build is in progress. Cancel it.
if (responsibleForAbortingBuilds || ownsRegistration) {
- Queue queue = Queue.getInstance();
- if (this instanceof Queue.Task) {
- // clear any items in the queue so they do not get picked up
- queue.cancel((Queue.Task) this);
- }
- // now cancel any child items - this happens after ItemDeletion registration, so we can use a snapshot
- for (Queue.Item i : queue.getItems()) {
- Item item = Tasks.getItemOf(i.task);
- while (item != null) {
- if (item == this) {
- if (!queue.cancel(i)) {
- LOGGER.warning(() -> "failed to cancel " + i);
- }
- break;
- }
- if (item.getParent() instanceof Item) {
- item = (Item) item.getParent();
- } else {
- break;
- }
- }
- }
- // interrupt any builds in progress (and this should be a recursive test so that folders do not pay
- // the 15 second delay for every child item). This happens after queue cancellation, so will be
- // a complete set of builds in flight
- Map buildsInProgress = new LinkedHashMap<>();
- for (Computer c : Jenkins.get().getComputers()) {
- for (Executor e : c.getAllExecutors()) {
- final WorkUnit workUnit = e.getCurrentWorkUnit();
- final Queue.Executable executable = workUnit != null ? workUnit.getExecutable() : null;
- final SubTask subtask = executable != null ? getParentOf(executable) : null;
-
- if (subtask != null) {
- Item item = Tasks.getItemOf(subtask);
- while (item != null) {
- if (item == this) {
- buildsInProgress.put(e, e.getCurrentExecutable());
- e.interrupt(Result.ABORTED);
- break;
- }
- if (item.getParent() instanceof Item) {
- item = (Item) item.getParent();
- } else {
- break;
- }
- }
- }
- }
- }
- if (!buildsInProgress.isEmpty()) {
- // give them 15 seconds or so to respond to the interrupt
- long expiration = System.nanoTime() + TimeUnit.SECONDS.toNanos(15);
- // comparison with executor.getCurrentExecutable() == computation currently should always be true
- // as we no longer recycle Executors, but safer to future-proof in case we ever revisit recycling
- while (!buildsInProgress.isEmpty() && expiration - System.nanoTime() > 0L) {
- // we know that ItemDeletion will prevent any new builds in the queue
- // ItemDeletion happens-before Queue.cancel so we know that the Queue will stay clear
- // Queue.cancel happens-before collecting the buildsInProgress list
- // thus buildsInProgress contains the complete set we need to interrupt and wait for
- for (Iterator> iterator =
- buildsInProgress.entrySet().iterator();
- iterator.hasNext(); ) {
- Map.Entry entry = iterator.next();
- // 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()
- || 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);
- }
- if (!buildsInProgress.isEmpty()) {
- throw new Failure(Messages.AbstractItem_FailureToStopBuilds(
- buildsInProgress.size(), getFullDisplayName()
- ));
- }
- }
+ ItemDeletion.cancelBuildsInProgress(this);
}
if (this instanceof ItemGroup) {
// delete individual items first
diff --git a/core/src/main/java/hudson/model/AbstractProject.java b/core/src/main/java/hudson/model/AbstractProject.java
index c323f50bca0b..c568f8df8b31 100644
--- a/core/src/main/java/hudson/model/AbstractProject.java
+++ b/core/src/main/java/hudson/model/AbstractProject.java
@@ -1011,6 +1011,7 @@ public List getActions() {
* null if no information is available (for example,
* if no build was done yet.)
*/
+ @SuppressWarnings("deprecation")
@Override
public Node getLastBuiltOn() {
// where was it built on?
diff --git a/core/src/main/java/hudson/model/AdministrativeMonitor.java b/core/src/main/java/hudson/model/AdministrativeMonitor.java
index e2f69d654ca8..bdbfb48027d3 100644
--- a/core/src/main/java/hudson/model/AdministrativeMonitor.java
+++ b/core/src/main/java/hudson/model/AdministrativeMonitor.java
@@ -200,7 +200,10 @@ public void doDisable(StaplerRequest req, StaplerResponse rsp) throws IOExceptio
* Form UI elements that change system state, e.g. toggling a feature on or off, need to be hidden from users
* lacking Administer permission.
*
+ * @since 2.233
+ * @deprecated Callers should use {@link #checkRequiredPermission()} or {@link #hasRequiredPermission()}.
*/
+ @Deprecated
public Permission getRequiredPermission() {
return Jenkins.ADMINISTER;
}
@@ -213,6 +216,7 @@ public Permission getRequiredPermission() {
*
* @see #getRequiredPermission()
* @see #hasRequiredPermission()
+ * @since 2.468
*/
public void checkRequiredPermission() {
Jenkins.get().checkPermission(getRequiredPermission());
@@ -226,6 +230,7 @@ public void checkRequiredPermission() {
*
* @see #getRequiredPermission()
* @see #checkRequiredPermission()
+ * @since 2.468
*/
public boolean hasRequiredPermission() {
return Jenkins.get().hasPermission(getRequiredPermission());
@@ -236,7 +241,7 @@ public boolean hasRequiredPermission() {
*
* @return true if the current user has the minimum required permission to view any administrative monitor.
*
- * @since TODO
+ * @since 2.468
*/
public static boolean hasPermissionToDisplay() {
return Jenkins.get().hasAnyPermission(Jenkins.SYSTEM_READ, Jenkins.MANAGE);
diff --git a/core/src/main/java/hudson/model/BuildAuthorizationToken.java b/core/src/main/java/hudson/model/BuildAuthorizationToken.java
index f101eb3d6e74..a09ed113e1cf 100644
--- a/core/src/main/java/hudson/model/BuildAuthorizationToken.java
+++ b/core/src/main/java/hudson/model/BuildAuthorizationToken.java
@@ -29,7 +29,6 @@
import hudson.security.ACL;
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
-import jenkins.security.ApiTokenProperty;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
@@ -82,10 +81,6 @@ public static void checkPermission(Job, ?> project, BuildAuthorizationToken to
return;
}
- if (req.getAttribute(ApiTokenProperty.class.getName()) instanceof User) {
- return;
- }
-
rsp.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
rsp.addHeader("Allow", "POST");
throw HttpResponses.forwardToView(project, "requirePOST.jelly");
diff --git a/core/src/main/java/hudson/model/Descriptor.java b/core/src/main/java/hudson/model/Descriptor.java
index 07789531e94d..b5eb07784d27 100644
--- a/core/src/main/java/hudson/model/Descriptor.java
+++ b/core/src/main/java/hudson/model/Descriptor.java
@@ -594,6 +594,9 @@ public T newInstance(@Nullable StaplerRequest req, @NonNull JSONObject formData)
return verifyNewInstance(bindJSON(req, clazz, formData, true));
}
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException | RuntimeException e) {
+ if (e instanceof RuntimeException && e instanceof HttpResponse) {
+ throw (RuntimeException) e;
+ }
throw new LinkageError("Failed to instantiate " + clazz + " from " + RedactSecretJsonInErrorMessageSanitizer.INSTANCE.sanitize(formData), e);
}
}
@@ -674,7 +677,7 @@ public Object instantiate(Class actualType, JSONObject json) {
+ actualType.getName() + " " + json);
}
} catch (Exception x) {
- LOGGER.log(Level.WARNING, "falling back to default instantiation " + actualType.getName() + " " + json, x);
+ LOGGER.log(x instanceof HttpResponse ? Level.FINE : Level.WARNING, "falling back to default instantiation " + actualType.getName() + " " + json, x);
// If nested objects are not using newInstance, bindJSON will wind up throwing the same exception anyway,
// so logging above will result in a duplicated stack trace.
// However if they *are* then this is the only way to find errors in that newInstance.
diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java
index d489f042100d..0d299fb9426d 100644
--- a/core/src/main/java/hudson/model/Queue.java
+++ b/core/src/main/java/hudson/model/Queue.java
@@ -130,6 +130,7 @@
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter;
+import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.interceptor.RequirePOST;
@@ -2414,6 +2415,10 @@ public Api getApi() throws AccessDeniedException {
}
}
+ public HttpResponse doIndex(StaplerRequest req) {
+ return HttpResponses.text("Queue item exists. For details check, for example, " + req.getRequestURI() + "api/json?tree=cancelled,executable[url]");
+ }
+
protected Object readResolve() {
this.future = new FutureImpl(task);
return this;
diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java
index 03ab31314372..4df95b7b3f45 100644
--- a/core/src/main/java/hudson/model/UpdateCenter.java
+++ b/core/src/main/java/hudson/model/UpdateCenter.java
@@ -58,14 +58,18 @@
import hudson.util.PersistedList;
import hudson.util.VersionNumber;
import hudson.util.XStream2;
+import java.io.BufferedInputStream;
import java.io.File;
+import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.io.UncheckedIOException;
import java.lang.reflect.Constructor;
import java.net.HttpRetryException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
+import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
@@ -86,6 +90,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
@@ -1322,6 +1327,10 @@ public File download(DownloadJob job, URL src) throws IOException {
sha512 != null ? new DigestOutputStream(_out, sha512) : _out, sha256) : _out, sha1) : _out;
InputStream in = con.getInputStream();
CountingInputStream cin = new CountingInputStream(in)) {
+ if (LOGGER.isLoggable(Level.FINE)) {
+ var sourceUrlString = getSourceUrl(src, con);
+ LOGGER.fine(() -> "Downloading " + job.getName() + " from " + sourceUrlString);
+ }
while ((len = cin.read(buf)) >= 0) {
out.write(buf, 0, len);
final int count = cin.getCount();
@@ -1358,15 +1367,22 @@ public File download(DownloadJob job, URL src) throws IOException {
return tmp;
} catch (IOException e) {
// assist troubleshooting in case of e.g. "too many redirects" by printing actual URL
- String extraMessage = "";
- if (con != null && con.getURL() != null && !src.toString().equals(con.getURL().toString())) {
- // Two URLs are considered equal if different hosts resolve to same IP. Prefer to log in case of string inequality,
- // because who knows how the server responds to different host name in the request header?
- // Also, since it involved name resolution, it'd be an expensive operation.
- extraMessage = " (redirected to: " + con.getURL() + ")";
+ throw new IOException("Failed to download from " + getSourceUrl(src, con), e);
+ }
+ }
+
+ private static String getSourceUrl(@NonNull URL src, @CheckForNull URLConnection connection) {
+ var sourceUrlString = src.toExternalForm();
+ if (connection != null) {
+ var connectionURL = connection.getURL();
+ if (connectionURL != null) {
+ var finalUrlString = connectionURL.toExternalForm();
+ if (!sourceUrlString.equals(finalUrlString)) {
+ return sourceUrlString + " → " + finalUrlString;
+ }
}
- throw new IOException("Failed to download from " + src + extraMessage, e);
}
+ return sourceUrlString;
}
/**
@@ -1798,6 +1814,83 @@ public void run() {
String getComputedSHA512();
}
+ @SuppressFBWarnings(value = "WEAK_MESSAGE_DIGEST_SHA1", justification = "SHA-1 is only used as a fallback if SHA-256/SHA-512 are not available")
+ private static class FileWithComputedChecksums implements WithComputedChecksums {
+
+ private final File file;
+
+ private String computedSHA1;
+ private String computedSHA256;
+ private String computedSHA512;
+
+ FileWithComputedChecksums(File file) {
+ this.file = Objects.requireNonNull(file);
+ }
+
+ @Override
+ public synchronized String getComputedSHA1() {
+ if (computedSHA1 != null) {
+ return computedSHA1;
+ }
+
+ MessageDigest messageDigest;
+ try {
+ messageDigest = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ throw new UnsupportedOperationException(e);
+ }
+ computedSHA1 = computeDigest(messageDigest);
+ return computedSHA1;
+ }
+
+ @Override
+ public synchronized String getComputedSHA256() {
+ if (computedSHA256 != null) {
+ return computedSHA256;
+ }
+
+ MessageDigest messageDigest;
+ try {
+ messageDigest = MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException e) {
+ throw new UnsupportedOperationException(e);
+ }
+ computedSHA256 = computeDigest(messageDigest);
+ return computedSHA256;
+ }
+
+ @Override
+ public synchronized String getComputedSHA512() {
+ if (computedSHA512 != null) {
+ return computedSHA512;
+ }
+
+ MessageDigest messageDigest;
+ try {
+ messageDigest = MessageDigest.getInstance("SHA-512");
+ } catch (NoSuchAlgorithmException e) {
+ throw new UnsupportedOperationException(e);
+ }
+ computedSHA512 = computeDigest(messageDigest);
+ return computedSHA512;
+ }
+
+ private String computeDigest(MessageDigest digest) {
+ try (InputStream is = new FileInputStream(file);
+ BufferedInputStream bis = new BufferedInputStream(is)) {
+ byte[] buffer = new byte[1024];
+ int read = bis.read(buffer, 0, buffer.length);
+ while (read > -1) {
+ digest.update(buffer, 0, read);
+ read = bis.read(buffer, 0, buffer.length);
+ }
+ return Base64.getEncoder().encodeToString(digest.digest());
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+ }
+
/**
* Base class for a job that downloads a file from the Jenkins project.
*/
@@ -2234,7 +2327,24 @@ public void _run() throws IOException, InstallationStatus {
return;
}
try {
- super._run();
+ File cached = getCached(this);
+ if (cached != null) {
+ File dst = getDestination();
+
+ // A bit naive, but following the corresponding logic in UpdateCenterConfiguration#download...
+ File tmp = new File(dst.getPath() + ".tmp");
+ Files.copy(cached.toPath(), tmp.toPath(), StandardCopyOption.REPLACE_EXISTING);
+
+ config.postValidate(this, tmp);
+
+ /*
+ * Will unfortunately validate the checksum a second time, but this should still be faster than
+ * network I/O and at least allows us to reuse code...
+ */
+ config.install(this, tmp, dst);
+ } else {
+ super._run();
+ }
// if this is a bundled plugin, make sure it won't get overwritten
PluginWrapper pw = plugin.getInstalled();
@@ -2267,6 +2377,62 @@ public void _run() throws IOException, InstallationStatus {
}
}
+ /**
+ * If we happen to have the file already in the {@coode WEB-INF/detached-plugins} directory and it happens to
+ * match the checksum we were expecting, then save ourselves a trip to the download site. This method is
+ * best-effort, and if anything goes wrong we simply fall back to the standard download path.
+ *
+ * @return The cached file, or null for a cache miss
+ */
+ @CheckForNull
+ private File getCached(DownloadJob job) {
+ URL src;
+ try {
+ /*
+ * Could make PluginManager#getDetachedLocation public and consume it here, but this method is
+ * best-effort anyway.
+ */
+ src = Jenkins.get().servletContext.getResource(String.format("/WEB-INF/detached-plugins/%s.hpi", plugin.name));
+ } catch (MalformedURLException e) {
+ return null;
+ }
+
+ if (src == null || !"file".equals(src.getProtocol())) {
+ return null;
+ }
+
+ try {
+ config.preValidate(this, src);
+ } catch (IOException e) {
+ return null;
+ }
+
+ File cached;
+ try {
+ cached = new File(src.toURI());
+ } catch (URISyntaxException e) {
+ return null;
+ }
+
+ if (!cached.isFile()) {
+ return null;
+ }
+
+ WithComputedChecksums withComputedChecksums = new FileWithComputedChecksums(cached);
+ try {
+ verifyChecksums(withComputedChecksums, plugin, cached);
+ } catch (IOException | UncheckedIOException | UnsupportedOperationException e) {
+ return null;
+ }
+
+ // Allow us to reuse UpdateCenter.InstallationJob#replace.
+ job.computedSHA1 = withComputedChecksums.getComputedSHA1();
+ job.computedSHA256 = withComputedChecksums.getComputedSHA256();
+ job.computedSHA512 = withComputedChecksums.getComputedSHA512();
+
+ return cached;
+ }
+
/**
* Indicates there is another installation job for this plugin
* @since 2.1
diff --git a/core/src/main/java/hudson/model/User.java b/core/src/main/java/hudson/model/User.java
index e588a79b347a..792622eb3c54 100644
--- a/core/src/main/java/hudson/model/User.java
+++ b/core/src/main/java/hudson/model/User.java
@@ -344,7 +344,7 @@ public synchronized void addProperty(@NonNull UserProperty p) throws IOException
* The properties not included in the list will be let untouched.
* It will call the {@link UserProperty#setUser(User)} method and at the end, {@link #save()} once.
*
- * @since TODO
+ * @since 2.468
*/
public synchronized void addProperties(@NonNull List multipleProperties) throws IOException {
List newProperties = new ArrayList<>(this.properties);
diff --git a/core/src/main/java/hudson/model/UserProperty.java b/core/src/main/java/hudson/model/UserProperty.java
index 6538a5fdf661..a6ebeb738b23 100644
--- a/core/src/main/java/hudson/model/UserProperty.java
+++ b/core/src/main/java/hudson/model/UserProperty.java
@@ -86,7 +86,7 @@ public static DescriptorExtensionList all(
/**
* Returns all the registered {@link UserPropertyCategory} descriptors for a given category.
*
- * @since TODO
+ * @since 2.468
*/
public static List allByCategoryClass(@NonNull Class extends UserPropertyCategory> categoryClass) {
DescriptorExtensionList all = all();
diff --git a/core/src/main/java/hudson/model/UserPropertyDescriptor.java b/core/src/main/java/hudson/model/UserPropertyDescriptor.java
index ff3171c1fbf7..66762bf3c716 100644
--- a/core/src/main/java/hudson/model/UserPropertyDescriptor.java
+++ b/core/src/main/java/hudson/model/UserPropertyDescriptor.java
@@ -85,7 +85,7 @@ public boolean isEnabled() {
*
* @return never null, always the same value for a given instance of {@link Descriptor}.
*
- * @since TODO
+ * @since 2.468
*/
public @NonNull UserPropertyCategory getUserPropertyCategory() {
// As this method is expected to be overloaded by subclasses
@@ -120,7 +120,7 @@ public boolean isEnabled() {
*
* @return String name corresponding to the symbol of {@link #getUserPropertyCategory()}
*
- * @since TODO
+ * @since 2.468
*/
@Deprecated
protected @CheckForNull String getUserPropertyCategoryAsString() {
diff --git a/core/src/main/java/hudson/model/listeners/ItemListener.java b/core/src/main/java/hudson/model/listeners/ItemListener.java
index aef431c3207b..fe09a9c56373 100644
--- a/core/src/main/java/hudson/model/listeners/ItemListener.java
+++ b/core/src/main/java/hudson/model/listeners/ItemListener.java
@@ -35,6 +35,8 @@
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.util.Listeners;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* Receives notifications about CRUD operations of {@link Item}.
@@ -94,6 +96,16 @@ public void onCopied(Item src, Item item) {
public void onLoaded() {
}
+ /**
+ * Called before an item is deleted, providing the ability to veto the deletion operation before it starts.
+ * @param item the item being deleted
+ * @throws Failure to veto the operation.
+ * @throws InterruptedException If a blocking condition was interrupted, also vetoing the operation.
+ * @since TODO
+ */
+ public void onCheckDelete(Item item) throws Failure, InterruptedException {
+ }
+
/**
* Called right before a job is going to be deleted.
*
@@ -205,6 +217,19 @@ public static void fireOnUpdated(final Item item) {
Listeners.notify(ItemListener.class, false, l -> l.onUpdated(item));
}
+ @Restricted(NoExternalUse.class)
+ public static void checkBeforeDelete(Item item) throws Failure, InterruptedException {
+ for (ItemListener l : all()) {
+ try {
+ l.onCheckDelete(item);
+ } catch (Failure e) {
+ throw e;
+ } catch (RuntimeException x) {
+ LOGGER.log(Level.WARNING, "failed to send event to listener of " + l.getClass(), x);
+ }
+ }
+ }
+
/** @since 1.548 */
public static void fireOnDeleted(final Item item) {
Listeners.notify(ItemListener.class, false, l -> l.onDeleted(item));
diff --git a/core/src/main/java/hudson/model/queue/MappingWorksheet.java b/core/src/main/java/hudson/model/queue/MappingWorksheet.java
index 9dbeaa3816f4..8150dd1bc132 100644
--- a/core/src/main/java/hudson/model/queue/MappingWorksheet.java
+++ b/core/src/main/java/hudson/model/queue/MappingWorksheet.java
@@ -190,7 +190,9 @@ public class WorkChunk extends ReadOnlyList {
* If the previous execution of this task run on a certain node
* and this task prefers to run on the same node, return that.
* Otherwise null.
+ * @deprecated Unused.
*/
+ @Deprecated
public final ExecutorChunk lastBuiltOn;
@@ -200,6 +202,7 @@ private WorkChunk(List base, int index) {
this.index = index;
this.assignedLabel = getAssignedLabel(base.get(0));
+ @SuppressWarnings("deprecation")
Node lbo = base.get(0).getLastBuiltOn();
for (ExecutorChunk ec : executors) {
if (ec.node == lbo) {
diff --git a/core/src/main/java/hudson/model/queue/QueueTaskFilter.java b/core/src/main/java/hudson/model/queue/QueueTaskFilter.java
index e2a113dfaa59..a36c5ca7c753 100644
--- a/core/src/main/java/hudson/model/queue/QueueTaskFilter.java
+++ b/core/src/main/java/hudson/model/queue/QueueTaskFilter.java
@@ -52,6 +52,7 @@ public Label getAssignedLabel() {
return base.getAssignedLabel();
}
+ @Deprecated
@Override
public Node getLastBuiltOn() {
return base.getLastBuiltOn();
diff --git a/core/src/main/java/hudson/model/queue/SubTask.java b/core/src/main/java/hudson/model/queue/SubTask.java
index f8b7dd435088..0690d074617c 100644
--- a/core/src/main/java/hudson/model/queue/SubTask.java
+++ b/core/src/main/java/hudson/model/queue/SubTask.java
@@ -62,7 +62,9 @@ default Label getAssignedLabel() {
* and this task prefers to run on the same node, return that.
* Otherwise null.
* @return by default, null
+ * @deprecated Unused.
*/
+ @Deprecated
default Node getLastBuiltOn() {
return null;
}
diff --git a/core/src/main/java/hudson/model/userproperty/UserPropertyCategory.java b/core/src/main/java/hudson/model/userproperty/UserPropertyCategory.java
index 803b7e2d3527..5d5467b6eed4 100644
--- a/core/src/main/java/hudson/model/userproperty/UserPropertyCategory.java
+++ b/core/src/main/java/hudson/model/userproperty/UserPropertyCategory.java
@@ -43,7 +43,7 @@
* as the catch-all "unclassified".) Categories themselves are extensible — plugins may introduce
* its own category as well, although that should only happen if you are creating a big enough subsystem.
*
- * @since TODO
+ * @since 2.468
* @see UserProperty
*/
public abstract class UserPropertyCategory implements ExtensionPoint, ModelObject {
diff --git a/core/src/main/java/hudson/util/BootFailure.java b/core/src/main/java/hudson/util/BootFailure.java
index a460ffea148e..f23cf7fa66b7 100644
--- a/core/src/main/java/hudson/util/BootFailure.java
+++ b/core/src/main/java/hudson/util/BootFailure.java
@@ -15,6 +15,7 @@
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletContext;
+import jenkins.model.Jenkins;
import jenkins.util.groovy.GroovyHookScript;
import org.kohsuke.stapler.WebApp;
@@ -51,6 +52,7 @@ public void publish(ServletContext context, @CheckForNull File home) {
.bind("servletContext", context)
.bind("attempts", loadAttempts(home))
.run();
+ Jenkins.get().getLifecycle().onBootFailure(this);
}
/**
diff --git a/core/src/main/java/jenkins/agents/WebSocketAgents.java b/core/src/main/java/jenkins/agents/WebSocketAgents.java
index 005587c6365a..d9560f156ebd 100644
--- a/core/src/main/java/jenkins/agents/WebSocketAgents.java
+++ b/core/src/main/java/jenkins/agents/WebSocketAgents.java
@@ -36,6 +36,7 @@
import hudson.remoting.ChannelBuilder;
import hudson.remoting.ChunkHeader;
import hudson.remoting.Engine;
+import hudson.slaves.SlaveComputer;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
@@ -107,7 +108,9 @@ public HttpResponse doIndex(StaplerRequest req, StaplerResponse rsp) throws IOEx
Capability remoteCapability = Capability.fromASCII(remoteCapabilityStr);
LOGGER.fine(() -> "received " + remoteCapability);
rsp.setHeader(Capability.KEY, new Capability().toASCII());
- rsp.setHeader(Engine.REMOTING_MINIMUM_VERSION_HEADER, RemotingVersionInfo.getMinimumSupportedVersion().toString());
+ if (!SlaveComputer.ALLOW_UNSUPPORTED_REMOTING_VERSIONS) {
+ rsp.setHeader(Engine.REMOTING_MINIMUM_VERSION_HEADER, RemotingVersionInfo.getMinimumSupportedVersion().toString());
+ }
rsp.setHeader(Engine.WEBSOCKET_COOKIE_HEADER, cookie);
return WebSockets.upgrade(new Session(state, agent, remoteCapability));
}
diff --git a/core/src/main/java/jenkins/model/GlobalComputerRetentionCheckIntervalConfiguration.java b/core/src/main/java/jenkins/model/GlobalComputerRetentionCheckIntervalConfiguration.java
index c73b862ef1e5..46d760fe8ee2 100644
--- a/core/src/main/java/jenkins/model/GlobalComputerRetentionCheckIntervalConfiguration.java
+++ b/core/src/main/java/jenkins/model/GlobalComputerRetentionCheckIntervalConfiguration.java
@@ -23,7 +23,7 @@ public class GlobalComputerRetentionCheckIntervalConfiguration extends GlobalCon
/**
* Gets the check interval for computer retention.
*
- * @since TODO
+ * @since 2.463
*/
public int getComputerRetentionCheckInterval() {
if (computerRetentionCheckInterval <= 0) {
@@ -42,7 +42,7 @@ public int getComputerRetentionCheckInterval() {
*
* @param interval new check interval in seconds
* @throws IllegalArgumentException interval must be greater than zero
- * @since TODO
+ * @since 2.463
*/
private void setComputerRetentionCheckInterval(int interval) throws IllegalArgumentException {
if (interval <= 0) {
diff --git a/core/src/main/java/jenkins/model/ParameterizedJobMixIn.java b/core/src/main/java/jenkins/model/ParameterizedJobMixIn.java
index 734534220901..662ffa9359e2 100644
--- a/core/src/main/java/jenkins/model/ParameterizedJobMixIn.java
+++ b/core/src/main/java/jenkins/model/ParameterizedJobMixIn.java
@@ -218,6 +218,7 @@ public final void doBuild(StaplerRequest req, StaplerResponse rsp, @QueryParamet
Queue.Item item = Jenkins.get().getQueue().schedule2(asJob(), delay.getTimeInSeconds(), getBuildCause(asJob(), req)).getItem();
if (item != null) {
+ // TODO JENKINS-66105 use SC_SEE_OTHER if !ScheduleResult.created
rsp.sendRedirect(SC_CREATED, req.getContextPath() + '/' + item.getUrl());
} else {
rsp.sendRedirect(".");
diff --git a/core/src/main/java/jenkins/model/experimentalflags/RemoveYuiUserExperimentalFlag.java b/core/src/main/java/jenkins/model/experimentalflags/RemoveYuiUserExperimentalFlag.java
new file mode 100644
index 000000000000..e8f8dcc31775
--- /dev/null
+++ b/core/src/main/java/jenkins/model/experimentalflags/RemoveYuiUserExperimentalFlag.java
@@ -0,0 +1,49 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2024, Markus Winter
+ *
+ * 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.experimentalflags;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import hudson.Extension;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+@Extension
+@Restricted(NoExternalUse.class)
+public class RemoveYuiUserExperimentalFlag extends BooleanUserExperimentalFlag {
+ public RemoveYuiUserExperimentalFlag() {
+ super("remove-yui.flag");
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Remove YUI";
+ }
+
+ @Nullable
+ @Override
+ public String getShortDescription() {
+ return "Remove YUI from all Jenkins UI pages. This will break anything that depends on YUI";
+ }
+}
diff --git a/core/src/main/java/jenkins/model/queue/ItemDeletion.java b/core/src/main/java/jenkins/model/queue/ItemDeletion.java
index e5c9fc2ca87e..fd18edde04a5 100644
--- a/core/src/main/java/jenkins/model/queue/ItemDeletion.java
+++ b/core/src/main/java/jenkins/model/queue/ItemDeletion.java
@@ -28,25 +28,42 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.ExtensionList;
+import hudson.model.AbstractItem;
import hudson.model.Action;
+import hudson.model.Computer;
+import hudson.model.Executor;
+import hudson.model.Failure;
import hudson.model.Item;
+import hudson.model.Messages;
import hudson.model.Queue;
+import hudson.model.Result;
+import hudson.model.queue.Executables;
+import hudson.model.queue.SubTask;
import hudson.model.queue.Tasks;
+import hudson.model.queue.WorkUnit;
import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.logging.Logger;
+import jenkins.model.Jenkins;
import net.jcip.annotations.GuardedBy;
/**
* A {@link Queue.QueueDecisionHandler} that blocks items being deleted from entering the queue.
- *
+ * @see AbstractItem#delete()
* @since 2.55
*/
@Extension
public class ItemDeletion extends Queue.QueueDecisionHandler {
+ private static final Logger LOGGER = Logger.getLogger(ItemDeletion.class.getName());
+
/**
* Lock to guard the {@link #registrations} set.
*/
@@ -176,4 +193,94 @@ public boolean shouldSchedule(Queue.Task p, List actions) {
}
return true;
}
+
+ /**
+ * Cancels any builds in progress of this item (if a job) or descendants (if a folder).
+ * Also cancels any associated queue items.
+ * @param initiatingItem an item being deleted
+ * @since TODO
+ */
+ public static void cancelBuildsInProgress(@NonNull Item initiatingItem) throws Failure, InterruptedException {
+ Queue queue = Queue.getInstance();
+ if (initiatingItem instanceof Queue.Task) {
+ // clear any items in the queue so they do not get picked up
+ queue.cancel((Queue.Task) initiatingItem);
+ }
+ // now cancel any child items - this happens after ItemDeletion registration, so we can use a snapshot
+ for (Queue.Item i : queue.getItems()) {
+ Item item = Tasks.getItemOf(i.task);
+ while (item != null) {
+ if (item == initiatingItem) {
+ if (!queue.cancel(i)) {
+ LOGGER.warning(() -> "failed to cancel " + i);
+ }
+ break;
+ }
+ if (item.getParent() instanceof Item) {
+ item = (Item) item.getParent();
+ } else {
+ break;
+ }
+ }
+ }
+ // interrupt any builds in progress (and this should be a recursive test so that folders do not pay
+ // the 15 second delay for every child item). This happens after queue cancellation, so will be
+ // a complete set of builds in flight
+ Map buildsInProgress = new LinkedHashMap<>();
+ for (Computer c : Jenkins.get().getComputers()) {
+ for (Executor e : c.getAllExecutors()) {
+ final WorkUnit workUnit = e.getCurrentWorkUnit();
+ final Queue.Executable executable = workUnit != null ? workUnit.getExecutable() : null;
+ final SubTask subtask = executable != null ? Executables.getParentOf(executable) : null;
+ if (subtask != null) {
+ Item item = Tasks.getItemOf(subtask);
+ while (item != null) {
+ if (item == initiatingItem) {
+ buildsInProgress.put(e, e.getCurrentExecutable());
+ e.interrupt(Result.ABORTED);
+ break;
+ }
+ if (item.getParent() instanceof Item) {
+ item = (Item) item.getParent();
+ } else {
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (!buildsInProgress.isEmpty()) {
+ // give them 15 seconds or so to respond to the interrupt
+ long expiration = System.nanoTime() + TimeUnit.SECONDS.toNanos(15);
+ // comparison with executor.getCurrentExecutable() == computation currently should always be true
+ // as we no longer recycle Executors, but safer to future-proof in case we ever revisit recycling
+ while (!buildsInProgress.isEmpty() && expiration - System.nanoTime() > 0L) {
+ // we know that ItemDeletion will prevent any new builds in the queue
+ // ItemDeletion happens-before Queue.cancel so we know that the Queue will stay clear
+ // Queue.cancel happens-before collecting the buildsInProgress list
+ // thus buildsInProgress contains the complete set we need to interrupt and wait for
+ for (Iterator> iterator =
+ buildsInProgress.entrySet().iterator();
+ iterator.hasNext(); ) {
+ Map.Entry entry = iterator.next();
+ // 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()
+ || 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);
+ }
+ if (!buildsInProgress.isEmpty()) {
+ throw new Failure(Messages.AbstractItem_FailureToStopBuilds(
+ buildsInProgress.size(), initiatingItem.getFullDisplayName()
+ ));
+ }
+ }
+ }
+
}
diff --git a/core/src/main/java/jenkins/widgets/BuildTimeTrend.java b/core/src/main/java/jenkins/widgets/BuildTimeTrend.java
index 59ab879f05da..dd0f0434ecb3 100644
--- a/core/src/main/java/jenkins/widgets/BuildTimeTrend.java
+++ b/core/src/main/java/jenkins/widgets/BuildTimeTrend.java
@@ -25,7 +25,9 @@
package jenkins.widgets;
import hudson.model.AbstractBuild;
+import hudson.model.AbstractProject;
import hudson.model.BallColor;
+import hudson.model.Job;
import hudson.model.Node;
import hudson.model.Run;
import jenkins.console.ConsoleUrlProvider;
@@ -37,6 +39,10 @@
@Restricted(DoNotUse.class) // only for buildTimeTrend.jelly
public class BuildTimeTrend extends RunListProgressiveRendering {
+ public boolean isAbstractProject(Job, ?> job) {
+ return job instanceof AbstractProject;
+ }
+
@Override protected void calculate(Run, ?> build, JSONObject element) {
BallColor iconColor = build.getIconColor();
element.put("iconName", iconColor.getIconName());
@@ -46,6 +52,8 @@ public class BuildTimeTrend extends RunListProgressiveRendering {
element.put("displayName", build.getDisplayName());
element.put("duration", build.getDuration());
element.put("durationString", build.getDurationString());
+ element.put("timestampString", build.getTimestampString());
+ element.put("timestampString2", build.getTimestampString2());
element.put("consoleUrl", ConsoleUrlProvider.getRedirectUrl(build));
if (build instanceof AbstractBuild) {
AbstractBuild, ?> b = (AbstractBuild) build;
diff --git a/core/src/main/resources/hudson/TcpSlaveAgentListener/index.jelly b/core/src/main/resources/hudson/TcpSlaveAgentListener/index.jelly
index 1b04d3c2c9ae..9e4b36af14a2 100644
--- a/core/src/main/resources/hudson/TcpSlaveAgentListener/index.jelly
+++ b/core/src/main/resources/hudson/TcpSlaveAgentListener/index.jelly
@@ -40,8 +40,11 @@ THE SOFTWARE.
-
-
+
+
+
+
+
Jenkins
diff --git a/core/src/main/resources/hudson/model/Job/buildTimeTrend.jelly b/core/src/main/resources/hudson/model/Job/buildTimeTrend.jelly
index 18f874a6751f..7efb53f362a4 100644
--- a/core/src/main/resources/hudson/model/Job/buildTimeTrend.jelly
+++ b/core/src/main/resources/hudson/model/Job/buildTimeTrend.jelly
@@ -25,7 +25,7 @@ THE SOFTWARE.
-
+
@@ -45,29 +45,36 @@ THE SOFTWARE.
+