---
.../java/hudson/markup/MarkupFormatter.java | 11 ++++++---
.../hudson/tasks/Shell/config.groovy | 2 +-
.../resources/lib/form/textarea/textarea.js | 23 ++++++++++++++++++-
3 files changed, 31 insertions(+), 5 deletions(-)
diff --git a/core/src/main/java/hudson/markup/MarkupFormatter.java b/core/src/main/java/hudson/markup/MarkupFormatter.java
index a3bf53a739a19..95af637af9749 100644
--- a/core/src/main/java/hudson/markup/MarkupFormatter.java
+++ b/core/src/main/java/hudson/markup/MarkupFormatter.java
@@ -62,9 +62,14 @@
* This is an extension point in Hudson, allowing plugins to implement different markup formatters.
*
*
- * Implement the following methods to enable and control CodeMirror syntax highlighting
- * public String getCodeMirrorMode() // return null to disable CodeMirror dynamically
- * public String getCodeMirrorConfig()
+ * Implement the following methods to enable and control CodeMirror syntax highlighting:
+ *
+ * public String getCodeMirrorMode()
(return null
to disable CodeMirror dynamically)
+ * -
+ *
public String getCodeMirrorConfig()
(JSON snippet without surrounding curly braces, e.g., "mode": "text/css"
.
+ * Historically this allowed invalid JSON, but since TODO it needs to be properly quoted etc.
+ *
+ *
*
* Views
*
diff --git a/core/src/main/resources/hudson/tasks/Shell/config.groovy b/core/src/main/resources/hudson/tasks/Shell/config.groovy
index f895ffe88f3ef..bfd1631969177 100644
--- a/core/src/main/resources/hudson/tasks/Shell/config.groovy
+++ b/core/src/main/resources/hudson/tasks/Shell/config.groovy
@@ -25,7 +25,7 @@ package hudson.tasks.Shell
f=namespace(lib.FormTagLib)
f.entry(title:_("Command"),description:_("description",rootURL)) {
- f.textarea(name: "command", value: instance?.command, class: "fixed-width", 'codemirror-mode': 'shell', 'codemirror-config': "mode: 'text/x-sh'")
+ f.textarea(name: "command", value: instance?.command, class: "fixed-width", 'codemirror-mode': 'shell', 'codemirror-config': '"mode": "text/x-sh"')
}
f.advanced() {
diff --git a/core/src/main/resources/lib/form/textarea/textarea.js b/core/src/main/resources/lib/form/textarea/textarea.js
index 1677e11fdd168..e1253cdfdab73 100644
--- a/core/src/main/resources/lib/form/textarea/textarea.js
+++ b/core/src/main/resources/lib/form/textarea/textarea.js
@@ -3,7 +3,28 @@ Behaviour.specify("TEXTAREA.codemirror", "textarea", 0, function (e) {
if (!config) {
config = "";
}
- config = eval("({" + config + "})");
+ try {
+ config = JSON.parse("{" + config + "}");
+ } catch (ex) {
+ /*
+ * Attempt to parse fairly common legacy format whose exact content is:
+ * mode:''
+ */
+ let match = config.match("^mode: ?'([^']+)'$");
+ if (match) {
+ console.log(
+ "Parsing simple legacy codemirror-config value using fallback: " +
+ config
+ );
+ config = { mode: match[1] };
+ } else {
+ console.log(
+ "Failed to parse codemirror-config '{" + config + "}' as JSON",
+ ex
+ );
+ config = {};
+ }
+ }
if (!config.onBlur) {
config.onBlur = function (editor) {
editor.save();
From 9036086e29e276374e7bbe0612ad600106b67758 Mon Sep 17 00:00:00 2001
From: Yen Cheng Lin <92412722+ridemountainpig@users.noreply.github.com>
Date: Wed, 2 Oct 2024 22:44:36 +0800
Subject: [PATCH 13/83] [JENKINS-73437] Fix build history no automatic line
wrapping (#9693)
* Fix build history no automatic line wrapping
* Update _job.scss
---
war/src/main/scss/pages/_job.scss | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/war/src/main/scss/pages/_job.scss b/war/src/main/scss/pages/_job.scss
index 3b982fcffcf40..8692d19dc3f99 100644
--- a/war/src/main/scss/pages/_job.scss
+++ b/war/src/main/scss/pages/_job.scss
@@ -129,9 +129,12 @@
font-weight: 450;
flex-grow: 1;
padding: 0.45rem 0 0;
+ word-break: normal;
+ overflow-wrap: anywhere;
.app-builds-container__item__time {
color: var(--text-color-secondary);
+ white-space: nowrap;
}
}
@@ -168,6 +171,8 @@
padding-left: 2.25rem;
margin-top: -2px;
grid-column: 1 / span 2;
+ word-break: normal;
+ overflow-wrap: anywhere;
&::before {
content: "";
From 68ea077ed1ac67ee5da2962e1c1b60331e45e762 Mon Sep 17 00:00:00 2001
From: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date: Wed, 2 Oct 2024 15:45:04 +0100
Subject: [PATCH 14/83] Refine 'Administrative monitors' interface (#9735)
* Squashed commit of the following:
* Rename section
* Push
* Update _plugin-manager.scss
* Update _checkbox.scss
* Reset files
* Update _badges.scss
* Update _badges.scss
* Update _theme.scss
* Remove label
* Fix lint
---------
Co-authored-by: Tim Jacomb
---
.../config.groovy | 23 ++++----
.../config_it.properties | 2 -
.../config_ru.properties | 2 -
.../config_sv_SE.properties | 2 -
.../config_tr.properties | 2 -
war/src/main/scss/abstracts/_theme.scss | 6 ---
war/src/main/scss/components/_badges.scss | 54 +++++--------------
war/src/main/scss/form/_checkbox.scss | 2 -
8 files changed, 25 insertions(+), 68 deletions(-)
diff --git a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config.groovy b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config.groovy
index f470918608d3a..78b82d5a50212 100644
--- a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config.groovy
+++ b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config.groovy
@@ -29,22 +29,21 @@ import hudson.model.AdministrativeMonitor
f = namespace(lib.FormTagLib)
st = namespace("jelly:stapler")
-f.section(title: _("Administrative monitors configuration")) {
+f.section(title: _("Administrative monitors"), description: _("blurb")) {
f.advanced(title: _("Administrative monitors")) {
- f.entry(title: _("Enabled administrative monitors")) {
- p(class: "jenkins-form-description", _("blurb"))
+ f.entry() {
for (AdministrativeMonitor am : new ArrayList<>(AdministrativeMonitor.all())
.sort({ o1, o2 -> o1.getDisplayName() <=> o2.getDisplayName() })) {
- div(class: "jenkins-checkbox-help-wrapper") {
- f.checkbox(name: "administrativeMonitor",
- title: am.displayName,
- checked: am.enabled,
- json: am.id)
- if (am.isSecurity()) {
- span(style: 'margin-left: 0.5rem', class: 'am-badge', _("Security"))
+ div(style: "margin-bottom: 0.625rem") {
+ div(class: "jenkins-checkbox-help-wrapper") {
+ f.checkbox(name: "administrativeMonitor",
+ title: am.displayName,
+ checked: am.enabled,
+ json: am.id)
+ if (am.isSecurity()) {
+ span(style: 'margin-left: 0.5rem', class: 'jenkins-badge', _("Security"))
+ }
}
- }
- div(class: "tr") {
div(class: "jenkins-checkbox__description") {
st.include(it: am, page: "description", optional: true)
}
diff --git a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_it.properties b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_it.properties
index 99774b93a33c7..e6a4cda88b8d8 100644
--- a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_it.properties
+++ b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_it.properties
@@ -22,10 +22,8 @@
# THE SOFTWARE.
Administrative\ monitors=Monitor amministrativi
-Administrative\ monitors\ configuration=Configurazione monitor amministrativi
blurb=I monitor amministrativi sono avvisi visualizzati agli amministratori \
di Jenkins riguardanti lo stato dell''istanza di Jenkins. In generale è \
caldamente consigliato mantenere tutti i monitor amministrativi abilitati, \
ma se non si è interessati a ricevere specifici avvisi, li si deselezioni \
qui per nasconderli permanentemente.
-Enabled\ administrative\ monitors=Monitor amministrativi abilitati
diff --git a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_ru.properties b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_ru.properties
index fabeb6e453efa..fc794e82e30f5 100644
--- a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_ru.properties
+++ b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_ru.properties
@@ -21,8 +21,6 @@
# THE SOFTWARE.
Administrative\ monitors=Мониторы администрирования
-Administrative\ monitors\ configuration=Настройка мониторов администрирования
blurb=Мониторы администрирования - это предупреждения о состоянии экземпляра Jenkins, которые показываются \
администраторам Jenkins. Обычно настоятельно рекомендуется оставить все мониторы администрирования включёнными, но \
если некоторые предупреждения вас не интересуют, отключите их здесь, чтобы навсегда скрыть их.
-Enabled\ administrative\ monitors=Включённые мониторы администрирования
diff --git a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_sv_SE.properties b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_sv_SE.properties
index 2d46f007949b2..4d20f2a9ff9bc 100644
--- a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_sv_SE.properties
+++ b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_sv_SE.properties
@@ -4,5 +4,3 @@ blurb=Administrativa övervakningar är varningar som visas för Jenkins-adminis
inte är intresserad av specifika varningar kan du avmarkera dem här för att \
dölja dem permanent.
Administrative\ monitors=Administrativa övervakningar
-Administrative\ monitors\ configuration=Konfiguration av administrativa övervakningar
-Enabled\ administrative\ monitors=Aktiverade administrativa övervakningar
\ No newline at end of file
diff --git a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_tr.properties b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_tr.properties
index 188640215ba65..63b7ff3612b01 100644
--- a/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_tr.properties
+++ b/core/src/main/resources/jenkins/management/AdministrativeMonitorsConfiguration/config_tr.properties
@@ -1,4 +1,2 @@
-Administrative\ monitors\ configuration=İdari monitör ayarları
Administrative\ monitors=İdari monitörler
-Enabled\ administrative\ monitors=Aktif idari monitörler
blurb=İdari monitörler, Jenkins yöneticilerine Jenkins örneğinin durumu hakkında gösterilen uyarılardır. Genel olarak tüm yönetim monitörlerini etkin tutmanız şiddetle tavsiye edilir, ancak belirli uyarılarla ilgilenmiyorsanız, kalıcı olarak gizlemek için buradaki işaretlerini kaldırın.
diff --git a/war/src/main/scss/abstracts/_theme.scss b/war/src/main/scss/abstracts/_theme.scss
index c108634f7c851..0558369c9ac36 100644
--- a/war/src/main/scss/abstracts/_theme.scss
+++ b/war/src/main/scss/abstracts/_theme.scss
@@ -331,12 +331,6 @@ $semantics: (
// Plugin manager
--plugin-manager-bg-color-already-upgraded: var(--light-grey);
- --plugin-manager-category-link-bg-color: var(--very-light-grey);
- --plugin-manager-category-link-bg-color--hover: #f2f2f2;
- --plugin-manager-category-link-border-color: var(--medium-grey);
- --plugin-manager-category-link-border-color--hover: var(--black);
- --plugin-manager-category-text-color: var(--text-color);
- --plugin-manager-category-link-color--hover: var(--text-color);
// Auto complete
--auto-complete-bg-color--prehighlight: #b3d4ff;
diff --git a/war/src/main/scss/components/_badges.scss b/war/src/main/scss/components/_badges.scss
index 7fc69376af34a..45fc8b9dc8039 100644
--- a/war/src/main/scss/components/_badges.scss
+++ b/war/src/main/scss/components/_badges.scss
@@ -1,39 +1,5 @@
-.am-badge,
-.plugin-manager__category-label:link,
-.plugin-manager__category-label:visited {
- display: inline-block;
- border: 1px solid var(--plugin-manager-category-link-border-color);
- background-color: var(--plugin-manager-category-link-bg-color);
- color: var(--plugin-manager-category-text-color);
- border-radius: 4px;
- font-size: 0.75rem;
- font-weight: 500;
- padding: 0 0.5rem;
- margin: 0 0.25rem 0 0;
- text-decoration: none;
- text-align: center;
- white-space: nowrap;
- vertical-align: baseline;
- transition: all 0.15s ease-in-out;
-}
-
-.plugin-manager__category-label:link,
-.plugin-manager__category-label:visited {
- &:hover,
- &:focus,
- &:active {
- background-color: var(--plugin-manager-category-link-bg-color--hover);
- border-color: var(--plugin-manager-category-link-border-color--hover);
- color: var(--plugin-manager-category-link-color--hover);
- }
-}
-
-.am-badge {
- display: inline;
-}
-
.jenkins-badge {
- display: flex;
+ display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 100px;
@@ -42,9 +8,17 @@
min-height: 20px;
min-width: 20px;
padding: 0 0.4rem;
- background: color-mix(in sRGB, var(--color) 85%, transparent);
- box-shadow: inset 0 -1px 2px var(--color);
- color: var(--white) !important;
- text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
- backdrop-filter: blur(2.5px);
+ background: color-mix(
+ in sRGB,
+ var(--text-color-secondary) 12.5%,
+ transparent
+ );
+
+ &[class*="color"] {
+ background: color-mix(in sRGB, var(--color) 85%, transparent);
+ color: var(--white) !important;
+ box-shadow: inset 0 -1px 2px var(--color, var(--text-color-secondary));
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ backdrop-filter: blur(2.5px);
+ }
}
diff --git a/war/src/main/scss/form/_checkbox.scss b/war/src/main/scss/form/_checkbox.scss
index d9cd5f18c9518..6e5633dd991f4 100644
--- a/war/src/main/scss/form/_checkbox.scss
+++ b/war/src/main/scss/form/_checkbox.scss
@@ -187,9 +187,7 @@
}
.jenkins-checkbox__description {
- margin-top: 0.3rem;
margin-left: 34px;
- margin-bottom: 1rem;
color: var(--text-color-secondary);
line-height: 1.66;
}
From a6f723cef725ea66c992f286516e872857dc6d9d Mon Sep 17 00:00:00 2001
From: Vincent Latombe
Date: Wed, 2 Oct 2024 16:45:44 +0200
Subject: [PATCH 15/83] Introduce `SaveableListener#onDeleted` (#9743)
* Introduce SaveableListener#onDeleted
Usually `Saveable` objects are written, but it can happen on occasion that they get deleted, and it wasn't generating an event for every case.
This provides a more fine-grained event that can be handled by implemented listeners.
In my case, I have a use case in CloudBees CI where I need to clear a cache entry when a Saveable gets deleted from disk.
* Spotless
* Explicitly test that the user that has performed the change can be obtained from the Saveable listener.
---
.../main/java/hudson/logging/LogRecorder.java | 2 +-
.../main/java/hudson/model/AbstractItem.java | 1 +
core/src/main/java/hudson/model/Run.java | 2 +
.../model/listeners/SaveableListener.java | 30 +++++---
core/src/main/java/jenkins/model/Nodes.java | 1 +
.../logging/LogRecorderManagerTest.java | 2 +-
.../java/hudson/model/AbstractItemTest.java | 68 ++++++++++++++++---
test/src/test/java/hudson/model/RunTest.java | 14 ++++
.../test/java/jenkins/model/NodesTest.java | 17 +++++
9 files changed, 115 insertions(+), 22 deletions(-)
diff --git a/core/src/main/java/hudson/logging/LogRecorder.java b/core/src/main/java/hudson/logging/LogRecorder.java
index ad4c40028547b..c11c1bbfa57ef 100644
--- a/core/src/main/java/hudson/logging/LogRecorder.java
+++ b/core/src/main/java/hudson/logging/LogRecorder.java
@@ -556,7 +556,7 @@ public void delete() throws IOException {
loggers.forEach(Target::disable);
getParent().getRecorders().forEach(logRecorder -> logRecorder.getLoggers().forEach(Target::enable));
- SaveableListener.fireOnChange(this, getConfigFile());
+ SaveableListener.fireOnDeleted(this, getConfigFile());
}
/**
diff --git a/core/src/main/java/hudson/model/AbstractItem.java b/core/src/main/java/hudson/model/AbstractItem.java
index e1a448d1f06e9..f31316c316569 100644
--- a/core/src/main/java/hudson/model/AbstractItem.java
+++ b/core/src/main/java/hudson/model/AbstractItem.java
@@ -814,6 +814,7 @@ public void delete() throws IOException, InterruptedException {
ItemDeletion.deregister(this);
}
}
+ SaveableListener.fireOnDeleted(this, getConfigFile());
getParent().onDeleted(AbstractItem.this);
Jenkins.get().rebuildDependencyGraphAsync();
}
diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java
index 37b09fd47d25f..450be07b7f1a3 100644
--- a/core/src/main/java/hudson/model/Run.java
+++ b/core/src/main/java/hudson/model/Run.java
@@ -1570,6 +1570,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 +1579,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();
diff --git a/core/src/main/java/hudson/model/listeners/SaveableListener.java b/core/src/main/java/hudson/model/listeners/SaveableListener.java
index 46bbc6ab60be5..02747877e76f8 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 TODO
+ */
+ 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 TODO
+ */
+ 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/jenkins/model/Nodes.java b/core/src/main/java/jenkins/model/Nodes.java
index 8f4c0e5eafc62..e15d391c4975e 100644
--- a/core/src/main/java/jenkins/model/Nodes.java
+++ b/core/src/main/java/jenkins/model/Nodes.java
@@ -295,6 +295,7 @@ public void run() {
jenkins.trimLabels(node);
}
NodeListener.fireOnDeleted(node);
+ SaveableListener.fireOnDeleted(node, getConfigFile(node));
}
}
diff --git a/test/src/test/java/hudson/logging/LogRecorderManagerTest.java b/test/src/test/java/hudson/logging/LogRecorderManagerTest.java
index 608e7eb0b464f..101729038a332 100644
--- a/test/src/test/java/hudson/logging/LogRecorderManagerTest.java
+++ b/test/src/test/java/hudson/logging/LogRecorderManagerTest.java
@@ -229,7 +229,7 @@ public static class DeletingLogRecorderListener extends SaveableListener {
private static boolean recordDeletion;
@Override
- public void onChange(Saveable o, XmlFile file) {
+ public void onDeleted(Saveable o, XmlFile file) {
if (o instanceof LogRecorder && "dummy".equals(((LogRecorder) o).getName())) {
if (!file.exists()) {
recordDeletion = true;
diff --git a/test/src/test/java/hudson/model/AbstractItemTest.java b/test/src/test/java/hudson/model/AbstractItemTest.java
index 165c39a96e6e9..8e2a5036b14a2 100644
--- a/test/src/test/java/hudson/model/AbstractItemTest.java
+++ b/test/src/test/java/hudson/model/AbstractItemTest.java
@@ -6,6 +6,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
import hudson.ExtensionList;
import hudson.XmlFile;
@@ -33,6 +34,7 @@
import org.jvnet.hudson.test.MockAuthorizationStrategy;
import org.jvnet.hudson.test.SleepBuilder;
import org.jvnet.hudson.test.TestExtension;
+import org.springframework.security.core.Authentication;
public class AbstractItemTest {
@@ -45,12 +47,21 @@ public class AbstractItemTest {
@Test
public void reload() throws Exception {
Jenkins jenkins = j.jenkins;
- FreeStyleProject p = jenkins.createProject(FreeStyleProject.class, "foo");
- p.setDescription("Hello World");
+ jenkins.setSecurityRealm(j.createDummySecurityRealm());
+ MockAuthorizationStrategy mas = new MockAuthorizationStrategy();
+ mas.grant(Item.CONFIGURE).everywhere().to("alice", "bob");
+ mas.grant(Item.READ).everywhere().to("alice");
- FreeStyleBuild b = j.buildAndAssertSuccess(p);
- b.setDescription("This is my build");
+ FreeStyleProject p;
+ FreeStyleBuild b;
+ var alice = User.getById("alice", true);
+ try (ACLContext ignored = ACL.as(alice)) {
+ p = jenkins.createProject(FreeStyleProject.class, "foo");
+ p.setDescription("Hello World");
+ b = j.buildAndAssertSuccess(p);
+ b.setDescription("This is my build");
+ }
// update on disk representation
Path path = p.getConfigFile().getFile().toPath();
Files.writeString(path, Files.readString(path, StandardCharsets.UTF_8).replaceAll("Hello World", "Good Evening"), StandardCharsets.UTF_8);
@@ -61,15 +72,25 @@ public void reload() throws Exception {
// reload away
p.doReload();
- assertFalse(SaveableListener.class.getSimpleName() + " should not have been called", testSaveableListener.wasCalled());
-
-
+ assertFalse(SaveableListener.class.getSimpleName() + " should not have been called", testSaveableListener.isChangeCalled());
assertEquals("Good Evening", p.getDescription());
FreeStyleBuild b2 = p.getBuildByNumber(1);
assertNotEquals(b, b2); // should be different object
assertEquals(b.getDescription(), b2.getDescription()); // but should have the same properties
+
+ try (var ignored = ACL.as(alice)) {
+ p.setDescription("This is Alice's project");
+ }
+ assertTrue(SaveableListener.class.getSimpleName() + " should have been called", testSaveableListener.isChangeCalled());
+ assertThat(testSaveableListener.getChangeUser(), equalTo(alice.impersonate2()));
+
+ try (var ignored = ACL.as(alice)) {
+ p.delete();
+ }
+ assertTrue(SaveableListener.class.getSimpleName() + " should have been called", testSaveableListener.isDeleteCalled());
+ assertThat(testSaveableListener.getDeleteUser(), equalTo(alice.impersonate2()));
}
@Test
@@ -158,20 +179,45 @@ private String getPath(URL u) {
public static class TestSaveableListener extends SaveableListener {
private Saveable saveable;
- private boolean called;
+ private boolean changeCalled;
+ private Authentication changeUser;
+
+ private boolean deleteCalled;
+ private Authentication deleteUser;
private void setSaveable(Saveable saveable) {
this.saveable = saveable;
}
- public boolean wasCalled() {
- return called;
+ public boolean isChangeCalled() {
+ return changeCalled;
+ }
+
+ public Authentication getChangeUser() {
+ return changeUser;
+ }
+
+ public boolean isDeleteCalled() {
+ return deleteCalled;
+ }
+
+ public Authentication getDeleteUser() {
+ return deleteUser;
}
@Override
public void onChange(Saveable o, XmlFile file) {
if (o == saveable) {
- this.called = true;
+ changeCalled = true;
+ changeUser = Jenkins.getAuthentication2();
+ }
+ }
+
+ @Override
+ public void onDeleted(Saveable o, XmlFile file) {
+ if (o == saveable) {
+ deleteCalled = true;
+ deleteUser = Jenkins.getAuthentication2();
}
}
}
diff --git a/test/src/test/java/hudson/model/RunTest.java b/test/src/test/java/hudson/model/RunTest.java
index b6a145857020b..34be1f14c2ef5 100644
--- a/test/src/test/java/hudson/model/RunTest.java
+++ b/test/src/test/java/hudson/model/RunTest.java
@@ -32,8 +32,11 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import hudson.ExtensionList;
import hudson.FilePath;
import hudson.Launcher;
+import hudson.XmlFile;
+import hudson.model.listeners.SaveableListener;
import hudson.tasks.ArtifactArchiver;
import hudson.tasks.BuildTrigger;
import hudson.tasks.Builder;
@@ -112,6 +115,17 @@ public class RunTest {
FreeStyleBuild b = j.buildAndAssertSuccess(p);
b.delete();
assertTrue(Mgr.deleted.get());
+ assertTrue(ExtensionList.lookupSingleton(SaveableListenerImpl.class).deleted);
+ }
+
+ @TestExtension("deleteArtifactsCustom")
+ public static class SaveableListenerImpl extends SaveableListener {
+ boolean deleted;
+
+ @Override
+ public void onDeleted(Saveable o, XmlFile file) {
+ deleted = true;
+ }
}
@Issue("SECURITY-1902")
diff --git a/test/src/test/java/jenkins/model/NodesTest.java b/test/src/test/java/jenkins/model/NodesTest.java
index 275dd9a3e5fbc..c7207b9977c56 100644
--- a/test/src/test/java/jenkins/model/NodesTest.java
+++ b/test/src/test/java/jenkins/model/NodesTest.java
@@ -37,10 +37,13 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.ExtensionList;
+import hudson.XmlFile;
import hudson.model.Descriptor;
import hudson.model.Failure;
import hudson.model.Node;
+import hudson.model.Saveable;
import hudson.model.Slave;
+import hudson.model.listeners.SaveableListener;
import hudson.slaves.ComputerLauncher;
import hudson.slaves.DumbSlave;
import java.io.IOException;
@@ -99,6 +102,10 @@ public void addNodeShouldReplaceExistingNode() throws Exception {
assertEquals(0, l.deleted);
assertEquals(1, l.updated);
assertEquals(1, l.created);
+ var saveableListener = ExtensionList.lookupSingleton(SaveableListenerImpl.class);
+ assertEquals(0, saveableListener.deleted);
+ r.jenkins.removeNode(newNode);
+ assertEquals(1, saveableListener.deleted);
}
@TestExtension("addNodeShouldReplaceExistingNode")
@@ -122,6 +129,16 @@ protected void onCreated(Node node) {
}
}
+ @TestExtension("addNodeShouldReplaceExistingNode")
+ public static final class SaveableListenerImpl extends SaveableListener {
+ int deleted;
+
+ @Override
+ public void onDeleted(Saveable o, XmlFile file) {
+ deleted++;
+ }
+ }
+
@Test
@Issue("JENKINS-56403")
public void replaceNodeShouldRemoveOldNode() throws Exception {
From 39e6622524293ff496ecfccec858a94fd66380c1 Mon Sep 17 00:00:00 2001
From: Vincent Latombe
Date: Wed, 2 Oct 2024 16:46:21 +0200
Subject: [PATCH 16/83] Extract interfaces for objects to be used through the
executors widget (#9749)
* Extract interfaces for objects to be used through the executors widget
This is required for CloudBees CI HA support: we provide alternate implementations of these interfaces to represent computers and related objects that exist in different physical replicas of the same logical instance. This allows to be build an aggregated view of computers and executors, some local, some remote.
* Fix Deprecated annotation
* Missing @Override annotations
* Missing @since TODO
* No longer true according to AbstractSubTask javadoc
* Restore old signature for compatibility
* Fix reviews
* Javadoc
* Typo
* Fix inconsistency between hasOfflineCause and getOfflineCauseReason
* Remove default implementation of ITask#getUrl
* Provide default impl in SubTask for compatibility
* Mark as CheckForNull
* Spotbugs
* ComputerSet#getComputers returns a collection that is sorted by name
* Missing Override
* Move permission declaration to the right place, closer to usage
---
core/src/main/java/hudson/model/Computer.java | 174 ++++-------------
.../main/java/hudson/model/ComputerSet.java | 52 ++++-
core/src/main/java/hudson/model/Executor.java | 59 ++----
core/src/main/java/hudson/model/Queue.java | 19 +-
.../main/java/hudson/model/queue/SubTask.java | 17 +-
.../security/AuthorizationStrategy.java | 17 ++
.../java/jenkins/model/DisplayExecutor.java | 97 ++++++++++
.../main/java/jenkins/model/IComputer.java | 182 ++++++++++++++++++
.../java/jenkins/model/IDisplayExecutor.java | 55 ++++++
.../main/java/jenkins/model/IExecutor.java | 144 ++++++++++++++
core/src/main/java/jenkins/model/Jenkins.java | 18 +-
.../model/ModelObjectWithContextMenu.java | 12 ++
.../main/java/jenkins/model/queue/ITask.java | 76 ++++++++
.../hudson/model/ComputerSet/index.jelly | 4 +-
.../main/resources/lib/hudson/executors.jelly | 46 ++---
.../java/hudson/model/ComputerSetTest.java | 7 +-
16 files changed, 726 insertions(+), 253 deletions(-)
create mode 100644 core/src/main/java/jenkins/model/DisplayExecutor.java
create mode 100644 core/src/main/java/jenkins/model/IComputer.java
create mode 100644 core/src/main/java/jenkins/model/IDisplayExecutor.java
create mode 100644 core/src/main/java/jenkins/model/IExecutor.java
create mode 100644 core/src/main/java/jenkins/model/queue/ITask.java
diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java
index 081f73dd16256..1047fe97000c1 100644
--- a/core/src/main/java/hudson/model/Computer.java
+++ b/core/src/main/java/hudson/model/Computer.java
@@ -41,7 +41,6 @@
import hudson.console.AnnotatedLargeText;
import hudson.init.Initializer;
import hudson.model.Descriptor.FormException;
-import hudson.model.Queue.FlyweightTask;
import hudson.model.labels.LabelAtom;
import hudson.model.queue.WorkUnit;
import hudson.node_monitors.AbstractDiskSpaceMonitor;
@@ -106,6 +105,9 @@
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import jenkins.model.DisplayExecutor;
+import jenkins.model.IComputer;
+import jenkins.model.IDisplayExecutor;
import jenkins.model.Jenkins;
import jenkins.security.ImpersonatingExecutorService;
import jenkins.security.MasterToSlaveCallable;
@@ -116,8 +118,6 @@
import jenkins.util.SystemProperties;
import jenkins.widgets.HasWidgets;
import net.jcip.annotations.GuardedBy;
-import org.jenkins.ui.icon.Icon;
-import org.jenkins.ui.icon.IconSet;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@@ -150,7 +150,7 @@
* if a {@link Node} is configured (probably temporarily) with 0 executors,
* you won't have a {@link Computer} object for it (except for the built-in node,
* which always gets its {@link Computer} in case we have no static executors and
- * we need to run a {@link FlyweightTask} - see JENKINS-7291 for more discussion.)
+ * we need to run a {@link Queue.FlyweightTask} - see JENKINS-7291 for more discussion.)
*
* Also, even if you remove a {@link Node}, it takes time for the corresponding
* {@link Computer} to be removed, if some builds are already in progress on that
@@ -164,7 +164,7 @@
* @author Kohsuke Kawaguchi
*/
@ExportedBean
-public /*transient*/ abstract class Computer extends Actionable implements AccessControlled, ExecutorListener, DescriptorByNameOwner, StaplerProxy, HasWidgets {
+public /*transient*/ abstract class Computer extends Actionable implements AccessControlled, IComputer, ExecutorListener, DescriptorByNameOwner, StaplerProxy, HasWidgets {
private final CopyOnWriteArrayList executors = new CopyOnWriteArrayList<>();
// TODO:
@@ -351,12 +351,6 @@ public AnnotatedLargeText getLogText() {
return new AnnotatedLargeText<>(getLogFile(), Charset.defaultCharset(), false, this);
}
- @NonNull
- @Override
- public ACL getACL() {
- return Jenkins.get().getAuthorizationStrategy().getACL(this);
- }
-
/**
* If the computer was offline (either temporarily or not),
* this method will return the cause.
@@ -369,14 +363,13 @@ public OfflineCause getOfflineCause() {
return offlineCause;
}
- /**
- * If the computer was offline (either temporarily or not),
- * this method will return the cause as a string (without user info).
- *
- * @return
- * empty string if the system was put offline without given a cause.
- */
+ @Override
+ public boolean hasOfflineCause() {
+ return offlineCause != null;
+ }
+
@Exported
+ @Override
public String getOfflineCauseReason() {
if (offlineCause == null) {
return "";
@@ -581,9 +574,6 @@ public int getNumExecutors() {
return numExecutors;
}
- /**
- * Returns {@link Node#getNodeName() the name of the node}.
- */
public @NonNull String getName() {
return nodeName != null ? nodeName : "";
}
@@ -628,6 +618,7 @@ public BuildTimelineWidget getTimeline() {
}
@Exported
+ @Override
public boolean isOffline() {
return temporarilyOffline || getChannel() == null;
}
@@ -645,12 +636,6 @@ public boolean isManualLaunchAllowed() {
return getRetentionStrategy().isManualLaunchAllowed(this);
}
-
- /**
- * Is a {@link #connect(boolean)} operation in progress?
- */
- public abstract boolean isConnecting();
-
/**
* Returns true if this computer is supposed to be launched via inbound protocol.
* @deprecated since 2008-05-18.
@@ -662,14 +647,8 @@ public boolean isJnlpAgent() {
return false;
}
- /**
- * Returns true if this computer can be launched by Hudson proactively and automatically.
- *
- *
- * For example, inbound agents return {@code false} from this, because the launch process
- * needs to be initiated from the agent side.
- */
@Exported
+ @Override
public boolean isLaunchSupported() {
return true;
}
@@ -727,14 +706,8 @@ public void setTemporarilyOffline(boolean temporarilyOffline, OfflineCause cause
}
}
- /**
- * Returns the icon for this computer.
- *
- * It is both the recommended and default implementation to serve different icons based on {@link #isOffline}
- *
- * @see #getIconClassName()
- */
@Exported
+ @Override
public String getIcon() {
// The machine was taken offline by someone
if (isTemporarilyOffline() && getOfflineCause() instanceof OfflineCause.UserCause) return "symbol-computer-disconnected";
@@ -748,19 +721,15 @@ public String getIcon() {
}
/**
- * Returns the class name that will be used to lookup the icon.
+ * {@inheritDoc}
*
- * This class name will be added as a class tag to the html img tags where the icon should
- * show up followed by a size specifier given by {@link Icon#toNormalizedIconSizeClass(String)}
- * The conversion of class tag to src tag is registered through {@link IconSet#addIcon(Icon)}
- *
- * It is both the recommended and default implementation to serve different icons based on {@link #isOffline}
- *
- * @see #getIcon()
+ *
+ * It is both the recommended and default implementation to serve different icons based on {@link #isOffline}.
*/
@Exported
+ @Override
public String getIconClassName() {
- return getIcon();
+ return IComputer.super.getIconClassName();
}
public String getIconAltText() {
@@ -780,6 +749,8 @@ public String getCaption() {
return Messages.Computer_Caption(nodeName);
}
+ @Override
+ @NonNull
public String getUrl() {
return "computer/" + Util.fullEncode(getName()) + "/";
}
@@ -947,19 +918,18 @@ public int countIdle() {
return n;
}
- /**
- * Returns the number of {@link Executor}s that are doing some work right now.
- */
+ @Override
public final int countBusy() {
return countExecutors() - countIdle();
}
/**
- * Returns the current size of the executor pool for this computer.
+ * {@inheritDoc}
* This number may temporarily differ from {@link #getNumExecutors()} if there
* are busy tasks when the configured size is decreased. OneOffExecutors are
* not included in this count.
*/
+ @Override
public final int countExecutors() {
return executors.size();
}
@@ -996,14 +966,14 @@ public List getAllExecutors() {
}
/**
- * Used to render the list of executors.
- * @return a snapshot of the executor display information
+ * {@inheritDoc}
* @since 1.607
*/
- @Restricted(NoExternalUse.class)
- public List getDisplayExecutors() {
+ @Override
+ @NonNull
+ public List getDisplayExecutors() {
// The size may change while we are populating, but let's start with a reasonable guess to minimize resizing
- List result = new ArrayList<>(executors.size() + oneOffExecutors.size());
+ List result = new ArrayList<>(executors.size() + oneOffExecutors.size());
int index = 0;
for (Executor e : executors) {
if (e.isDisplayCell()) {
@@ -1659,15 +1629,8 @@ public Object getTarget() {
return e != null ? e.getOwner() : null;
}
- /**
- * Returns {@code true} if the computer is accepting tasks. Needed to allow agents programmatic suspension of task
- * scheduling that does not overlap with being offline.
- *
- * @return {@code true} if the computer is accepting tasks
- * @see hudson.slaves.RetentionStrategy#isAcceptingTasks(Computer)
- * @see hudson.model.Node#isAcceptingTasks()
- */
@OverrideMustInvoke
+ @Override
public boolean isAcceptingTasks() {
final Node node = getNode();
return getRetentionStrategy().isAcceptingTasks(this) && (node == null || node.isAcceptingTasks());
@@ -1727,79 +1690,12 @@ public static void relocateOldLogs() {
}
}
- /**
- * A value class to provide a consistent snapshot view of the state of an executor to avoid race conditions
- * during rendering of the executors list.
- *
- * @since 1.607
- */
- @Restricted(NoExternalUse.class)
- public static class DisplayExecutor implements ModelObject {
-
- @NonNull
- private final String displayName;
- @NonNull
- private final String url;
- @NonNull
- private final Executor executor;
-
- public DisplayExecutor(@NonNull String displayName, @NonNull String url, @NonNull Executor executor) {
- this.displayName = displayName;
- this.url = url;
- this.executor = executor;
- }
-
- @Override
- @NonNull
- public String getDisplayName() {
- return displayName;
- }
-
- @NonNull
- public String getUrl() {
- return url;
- }
-
- @NonNull
- public Executor getExecutor() {
- return executor;
- }
-
- @Override
- public String toString() {
- String sb = "DisplayExecutor{" + "displayName='" + displayName + '\'' +
- ", url='" + url + '\'' +
- ", executor=" + executor +
- '}';
- return sb;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
-
- DisplayExecutor that = (DisplayExecutor) o;
-
- return executor.equals(that.executor);
- }
-
- @Extension(ordinal = Double.MAX_VALUE)
- @Restricted(DoNotUse.class)
- public static class InternalComputerListener extends ComputerListener {
- @Override
- public void onOnline(Computer c, TaskListener listener) throws IOException, InterruptedException {
- c.cachedEnvironment = null;
- }
- }
-
+ @Extension(ordinal = Double.MAX_VALUE)
+ @Restricted(DoNotUse.class)
+ public static class InternalComputerListener extends ComputerListener {
@Override
- public int hashCode() {
- return executor.hashCode();
+ public void onOnline(Computer c, TaskListener listener) throws IOException, InterruptedException {
+ c.cachedEnvironment = null;
}
}
diff --git a/core/src/main/java/hudson/model/ComputerSet.java b/core/src/main/java/hudson/model/ComputerSet.java
index f25d25a19a50c..f8e4905b09047 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 = "TODO")
public Computer[] get_all() {
- return Jenkins.get().getComputers();
+ return getComputers().stream().filter(Computer.class::isInstance).toArray(Computer[]::new);
+ }
+
+ /**
+ * @return All {@link IComputer} instances managed by this set, sorted by name.
+ */
+ @Exported(name = "computer", inline = true)
+ public Collection extends IComputer> getComputers() {
+ return ExtensionList.lookupFirst(ComputerSource.class).get().stream().sorted(Comparator.comparing(IComputer::getName)).toList();
+ }
+
+ /**
+ * Allows plugins to override the displayed list of computers.
+ *
+ */
+ @Restricted(Beta.class)
+ public interface ComputerSource extends ExtensionPoint {
+ Collection extends IComputer> get();
}
@Override
public ContextMenu doChildrenContextMenu(StaplerRequest2 request, StaplerResponse2 response) throws Exception {
ContextMenu m = new ContextMenu();
- for (Computer c : get_all()) {
+ for (IComputer c : getComputers()) {
m.add(c);
}
return m;
@@ -170,7 +199,7 @@ public int size() {
@Exported
public int getTotalExecutors() {
int r = 0;
- for (Computer c : get_all()) {
+ for (IComputer c : getComputers()) {
if (c.isOnline())
r += c.countExecutors();
}
@@ -183,7 +212,7 @@ public int getTotalExecutors() {
@Exported
public int getBusyExecutors() {
int r = 0;
- for (Computer c : get_all()) {
+ for (IComputer c : getComputers()) {
if (c.isOnline())
r += c.countBusy();
}
@@ -195,7 +224,7 @@ public int getBusyExecutors() {
*/
public int getIdleExecutors() {
int r = 0;
- for (Computer c : get_all())
+ for (IComputer c : getComputers())
if ((c.isOnline() || c.isConnecting()) && c.isAcceptingTasks())
r += c.countIdle();
return r;
@@ -214,7 +243,7 @@ public Computer getDynamic(String token, StaplerRequest2 req, StaplerResponse2 r
public void do_launchAll(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException {
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
- for (Computer c : get_all()) {
+ for (IComputer c : getComputers()) {
if (c.isLaunchSupported())
c.connect(true);
}
@@ -502,4 +531,13 @@ private static NodeMonitor createDefaultInstance(Descriptor d, bool
}
return null;
}
+
+ @Extension(ordinal = -1)
+ @Restricted(DoNotUse.class)
+ public static class ComputerSourceImpl implements ComputerSource {
+ @Override
+ public Collection extends IComputer> get() {
+ return Jenkins.get().getComputersCollection();
+ }
+ }
}
diff --git a/core/src/main/java/hudson/model/Executor.java b/core/src/main/java/hudson/model/Executor.java
index 94aa1c770d218..f86dbd7c6c0a0 100644
--- a/core/src/main/java/hudson/model/Executor.java
+++ b/core/src/main/java/hudson/model/Executor.java
@@ -61,6 +61,7 @@
import java.util.stream.Collectors;
import jenkins.model.CauseOfInterruption;
import jenkins.model.CauseOfInterruption.UserInterruption;
+import jenkins.model.IExecutor;
import jenkins.model.InterruptedBuildAction;
import jenkins.model.Jenkins;
import jenkins.model.queue.AsynchronousExecution;
@@ -88,7 +89,7 @@
* @author Kohsuke Kawaguchi
*/
@ExportedBean
-public class Executor extends Thread implements ModelObject {
+public class Executor extends Thread implements ModelObject, IExecutor {
protected final @NonNull Computer owner;
private final Queue queue;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
@@ -526,6 +527,7 @@ public void completedAsynchronous(@CheckForNull Throwable error) {
* @return
* null if the executor is idle.
*/
+ @Override
public @CheckForNull Queue.Executable getCurrentExecutable() {
lock.readLock().lock();
try {
@@ -555,14 +557,8 @@ public Queue.Executable getCurrentExecutableForApi() {
return Collections.unmodifiableCollection(causes);
}
- /**
- * Returns the current {@link WorkUnit} (of {@link #getCurrentExecutable() the current executable})
- * that this executor is running.
- *
- * @return
- * null if the executor is idle.
- */
@CheckForNull
+ @Override
public WorkUnit getCurrentWorkUnit() {
lock.readLock().lock();
try {
@@ -601,22 +597,14 @@ public String getDisplayName() {
return "Executor #" + getNumber();
}
- /**
- * Gets the executor number that uniquely identifies it among
- * other {@link Executor}s for the same computer.
- *
- * @return
- * a sequential number starting from 0.
- */
@Exported
+ @Override
public int getNumber() {
return number;
}
- /**
- * Returns true if this {@link Executor} is ready for action.
- */
@Exported
+ @Override
public boolean isIdle() {
lock.readLock().lock();
try {
@@ -705,13 +693,8 @@ public boolean isParking() {
return null;
}
- /**
- * Returns the progress of the current build in the number between 0-100.
- *
- * @return -1
- * if it's impossible to estimate the progress.
- */
@Exported
+ @Override
public int getProgress() {
long d = executableEstimatedDuration;
if (d <= 0) {
@@ -725,14 +708,8 @@ public int getProgress() {
return num;
}
- /**
- * Returns true if the current build is likely stuck.
- *
- *
- * This is a heuristics based approach, but if the build is suspiciously taking for a long time,
- * this method returns true.
- */
@Exported
+ @Override
public boolean isLikelyStuck() {
lock.readLock().lock();
try {
@@ -754,6 +731,7 @@ public boolean isLikelyStuck() {
}
}
+ @Override
public long getElapsedTime() {
lock.readLock().lock();
try {
@@ -777,20 +755,7 @@ public long getTimeSpentInQueue() {
}
}
- /**
- * Gets the string that says how long since this build has started.
- *
- * @return
- * string like "3 minutes" "1 day" etc.
- */
- public String getTimestampString() {
- return Util.getTimeSpanString(getElapsedTime());
- }
-
- /**
- * Computes a human-readable text that shows the expected remaining time
- * until the build completes.
- */
+ @Override
public String getEstimatedRemainingTime() {
long d = executableEstimatedDuration;
if (d < 0) {
@@ -911,9 +876,7 @@ public HttpResponse doYank() {
return HttpResponses.redirectViaContextPath("/");
}
- /**
- * Checks if the current user has a permission to stop this build.
- */
+ @Override
public boolean hasStopPermission() {
lock.readLock().lock();
try {
diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java
index 08eba90b906ca..797cba56f3942 100644
--- a/core/src/main/java/hudson/model/Queue.java
+++ b/core/src/main/java/hudson/model/Queue.java
@@ -1956,24 +1956,6 @@ default void checkAbortPermission() {
}
}
- /**
- * Works just like {@link #checkAbortPermission()} except it indicates the status by a return value,
- * instead of exception.
- * Also used by default for {@link hudson.model.Queue.Item#hasCancelPermission}.
- *
- * NOTE: If you have implemented {@link AccessControlled} this returns by default
- * {@code return hasPermission(hudson.model.Item.CANCEL);}
- *
- * @return false
- * if the user doesn't have the permission.
- */
- default boolean hasAbortPermission() {
- if (this instanceof AccessControlled) {
- return ((AccessControlled) this).hasPermission(CANCEL);
- }
- return true;
- }
-
/**
* Returns the URL of this task relative to the context root of the application.
*
@@ -1984,6 +1966,7 @@ default boolean hasAbortPermission() {
* @return
* URL that ends with '/'.
*/
+ @Override
String getUrl();
/**
diff --git a/core/src/main/java/hudson/model/queue/SubTask.java b/core/src/main/java/hudson/model/queue/SubTask.java
index 0690d074617c4..9a971d9ca40b7 100644
--- a/core/src/main/java/hudson/model/queue/SubTask.java
+++ b/core/src/main/java/hudson/model/queue/SubTask.java
@@ -33,20 +33,16 @@
import hudson.model.Queue;
import hudson.model.ResourceActivity;
import java.io.IOException;
+import jenkins.model.queue.ITask;
/**
* A component of {@link Queue.Task} that represents a computation carried out by a single {@link Executor}.
*
* A {@link Queue.Task} consists of a number of {@link SubTask}.
*
- *
- * Plugins are encouraged to extend from {@link AbstractSubTask}
- * instead of implementing this interface directly, to maintain
- * compatibility with future changes to this interface.
- *
* @since 1.377
*/
-public interface SubTask extends ResourceActivity {
+public interface SubTask extends ResourceActivity, ITask {
/**
* If this task needs to be run on a node with a particular label,
* return that {@link Label}. Otherwise null, indicating
@@ -115,4 +111,13 @@ default long getEstimatedDuration() {
default Object getSameNodeConstraint() {
return null;
}
+
+ /**
+ * A subtask may not be reachable by its own URL. In that case, this method should return null.
+ * @return the URL where to reach specifically this subtask, relative to Jenkins URL. If non-null, must end with '/'.
+ */
+ @Override
+ default String getUrl() {
+ return null;
+ }
}
diff --git a/core/src/main/java/hudson/security/AuthorizationStrategy.java b/core/src/main/java/hudson/security/AuthorizationStrategy.java
index db3001fc40f2c..08fa10fe9897d 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 TODO
+ **/
+ 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/jenkins/model/DisplayExecutor.java b/core/src/main/java/jenkins/model/DisplayExecutor.java
new file mode 100644
index 0000000000000..f7559ff85799c
--- /dev/null
+++ b/core/src/main/java/jenkins/model/DisplayExecutor.java
@@ -0,0 +1,97 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2024 CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package jenkins.model;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.model.Executor;
+import hudson.model.ModelObject;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+/**
+ * A value class providing a consistent snapshot view of the state of an executor to avoid race conditions
+ * during rendering of the executors list.
+ */
+@Restricted(NoExternalUse.class)
+public class DisplayExecutor implements ModelObject, IDisplayExecutor {
+
+ @NonNull
+ private final String displayName;
+ @NonNull
+ private final String url;
+ @NonNull
+ private final Executor executor;
+
+ public DisplayExecutor(@NonNull String displayName, @NonNull String url, @NonNull Executor executor) {
+ this.displayName = displayName;
+ this.url = url;
+ this.executor = executor;
+ }
+
+ @Override
+ @NonNull
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ @Override
+ @NonNull
+ public String getUrl() {
+ return url;
+ }
+
+ @Override
+ @NonNull
+ public Executor getExecutor() {
+ return executor;
+ }
+
+ @Override
+ public String toString() {
+ return "DisplayExecutor{" + "displayName='" + displayName + '\'' +
+ ", url='" + url + '\'' +
+ ", executor=" + executor +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ DisplayExecutor that = (DisplayExecutor) o;
+
+ return executor.equals(that.executor);
+ }
+
+ @Override
+ public int hashCode() {
+ return executor.hashCode();
+ }
+}
diff --git a/core/src/main/java/jenkins/model/IComputer.java b/core/src/main/java/jenkins/model/IComputer.java
new file mode 100644
index 0000000000000..f975f200f669e
--- /dev/null
+++ b/core/src/main/java/jenkins/model/IComputer.java
@@ -0,0 +1,182 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2024 CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package jenkins.model;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.Util;
+import hudson.model.Computer;
+import hudson.model.Node;
+import hudson.security.ACL;
+import hudson.security.AccessControlled;
+import java.util.List;
+import java.util.concurrent.Future;
+import org.jenkins.ui.icon.Icon;
+import org.jenkins.ui.icon.IconSet;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.Beta;
+
+/**
+ * Interface for computer-like objects meant to be passed to {@code t:executors} tag.
+ *
+ * @since TODO
+ */
+@Restricted(Beta.class)
+public interface IComputer extends AccessControlled {
+ /**
+ * Returns {@link Node#getNodeName() the name of the node}.
+ */
+ @NonNull
+ String getName();
+
+ /**
+ * Used to render the list of executors.
+ * @return a snapshot of the executor display information
+ */
+ @NonNull
+ List extends IDisplayExecutor> getDisplayExecutors();
+
+ /**
+ * @return {@code true} if the node is offline. {@code false} if it is online.
+ */
+ boolean isOffline();
+
+ /**
+ * @return the node name for UI purposes.
+ */
+ @NonNull
+ String getDisplayName();
+
+ /**
+ * Returns {@code true} if the computer is accepting tasks. Needed to allow agents programmatic suspension of task
+ * scheduling that does not overlap with being offline.
+ *
+ * @return {@code true} if the computer is accepting tasks
+ * @see hudson.slaves.RetentionStrategy#isAcceptingTasks(Computer)
+ * @see hudson.model.Node#isAcceptingTasks()
+ */
+ boolean isAcceptingTasks();
+
+ /**
+ * @return the URL where to reach specifically this computer, relative to Jenkins URL.
+ */
+ @NonNull
+ String getUrl();
+
+ /**
+ * @return {@code true} if this computer has a defined offline cause, @{code false} otherwise.
+ */
+ default boolean hasOfflineCause() {
+ return Util.fixEmpty(getOfflineCauseReason()) != null;
+ }
+
+ /**
+ * If the computer was offline (either temporarily or not),
+ * this method will return the cause as a string (without user info).
+ *
+ * {@code hasOfflineCause() == true} implies this must be nonempty.
+ *
+ * @return
+ * empty string if the system was put offline without given a cause.
+ */
+ @NonNull
+ String getOfflineCauseReason();
+
+ /**
+ * @return true if the node is currently connecting to the Jenkins controller.
+ */
+ boolean isConnecting();
+
+ /**
+ * Returns the icon for this computer.
+ *
+ * It is both the recommended and default implementation to serve different icons based on {@link #isOffline}
+ *
+ * @see #getIconClassName()
+ */
+ String getIcon();
+
+ /**
+ * Returns the alternative text for the computer icon.
+ */
+ String getIconAltText();
+
+ /**
+ * Returns the class name that will be used to look up the icon.
+ *
+ * This class name will be added as a class tag to the html img tags where the icon should
+ * show up followed by a size specifier given by {@link Icon#toNormalizedIconSizeClass(String)}
+ * The conversion of class tag to src tag is registered through {@link IconSet#addIcon(Icon)}
+ *
+ * @see #getIcon()
+ */
+ default String getIconClassName() {
+ return getIcon();
+ }
+
+ /**
+ * Returns the number of {@link IExecutor}s that are doing some work right now.
+ */
+ int countBusy();
+ /**
+ * Returns the current size of the executor pool for this computer.
+ */
+ int countExecutors();
+
+ /**
+ * @return true if the computer is online.
+ */
+ boolean isOnline();
+ /**
+ * @return the number of {@link IExecutor}s that are idle right now.
+ */
+ int countIdle();
+
+ /**
+ * @return true if this computer can be launched by Jenkins proactively and automatically.
+ *
+ *
+ * For example, inbound agents return {@code false} from this, because the launch process
+ * needs to be initiated from the agent side.
+ */
+ boolean isLaunchSupported();
+
+ /**
+ * Attempts to connect this computer.
+ *
+ * @param forceReconnect If true and a connect activity is already in progress, it will be cancelled and
+ * the new one will be started. If false, and a connect activity is already in progress, this method
+ * will do nothing and just return the pending connection operation.
+ * @return A {@link Future} representing pending completion of the task. The 'completion' includes
+ * both a successful completion and a non-successful completion (such distinction typically doesn't
+ * make much sense because as soon as {@link IComputer} is connected it can be disconnected by some other threads.)
+ */
+ Future> connect(boolean forceReconnect);
+
+ @NonNull
+ @Override
+ default ACL getACL() {
+ return Jenkins.get().getAuthorizationStrategy().getACL(this);
+ }
+}
diff --git a/core/src/main/java/jenkins/model/IDisplayExecutor.java b/core/src/main/java/jenkins/model/IDisplayExecutor.java
new file mode 100644
index 0000000000000..af10341349d46
--- /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 TODO
+ */
+@Restricted(Beta.class)
+public interface IDisplayExecutor {
+ /**
+ * @return The UI label for this executor.
+ */
+ @NonNull
+ String getDisplayName();
+
+ /**
+ * @return the URL where to reach specifically this executor, relative to Jenkins URL.
+ */
+ @NonNull
+ String getUrl();
+
+ /**
+ * @return the executor this display information is for.
+ */
+ @NonNull
+ IExecutor getExecutor();
+}
diff --git a/core/src/main/java/jenkins/model/IExecutor.java b/core/src/main/java/jenkins/model/IExecutor.java
new file mode 100644
index 0000000000000..35894f33e4abd
--- /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 TODO
+ */
+@Restricted(Beta.class)
+public interface IExecutor {
+ /**
+ * Returns true if this {@link IExecutor} is ready for action.
+ */
+ boolean isIdle();
+
+ /**
+ * @return the {@link IComputer} that this executor belongs to.
+ */
+ IComputer getOwner();
+
+ /**
+ * @return the current executable, if any.
+ */
+ @CheckForNull Queue.Executable getCurrentExecutable();
+
+ /**
+ * Returns the current {@link WorkUnit} (of {@link #getCurrentExecutable() the current executable})
+ * that this executor is running.
+ *
+ * @return
+ * null if the executor is idle.
+ */
+ @CheckForNull WorkUnit getCurrentWorkUnit();
+
+ /**
+ * @return the current display name of the executor. Usually the name of the executable.
+ */
+ String getDisplayName();
+
+ /**
+ * @return a reference to the parent task of the current executable, if any.
+ */
+ @CheckForNull
+ default ITask getParentTask() {
+ var currentExecutable = getCurrentExecutable();
+ if (currentExecutable == null) {
+ var workUnit = getCurrentWorkUnit();
+ if (workUnit != null) {
+ return workUnit.work;
+ } else {
+ // Idle
+ return null;
+ }
+ } else {
+ return currentExecutable.getParent();
+ }
+ }
+
+ /**
+ * Checks if the current user has a permission to stop this build.
+ */
+ boolean hasStopPermission();
+
+ /**
+ * Gets the executor number that uniquely identifies it among
+ * other {@link IExecutor}s for the same computer.
+ *
+ * @return
+ * a sequential number starting from 0.
+ */
+ int getNumber();
+
+ /**
+ * Gets the elapsed time since the build has started.
+ *
+ * @return
+ * the number of milliseconds since the build has started.
+ */
+ long getElapsedTime();
+
+ /**
+ * Gets the string that says how long since this build has started.
+ *
+ * @return
+ * string like "3 minutes" "1 day" etc.
+ */
+ default String getTimestampString() {
+ return Util.getTimeSpanString(getElapsedTime());
+ }
+
+ /**
+ * Computes a human-readable text that shows the expected remaining time
+ * until the build completes.
+ */
+ String getEstimatedRemainingTime();
+
+ /**
+ * Returns true if the current build is likely stuck.
+ *
+ *
+ * This is a heuristics based approach, but if the build is suspiciously taking for a long time,
+ * this method returns true.
+ */
+ boolean isLikelyStuck();
+
+ /**
+ * Returns the progress of the current build in the number between 0-100.
+ *
+ * @return -1
+ * if it's impossible to estimate the progress.
+ */
+ int getProgress();
+}
diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java
index 705044cd8fa00..6ea969668669c 100644
--- a/core/src/main/java/jenkins/model/Jenkins.java
+++ b/core/src/main/java/jenkins/model/Jenkins.java
@@ -2071,7 +2071,7 @@ public boolean isUpgradedFromBefore(VersionNumber v) {
* Gets the read-only list of all {@link Computer}s.
*/
public Computer[] getComputers() {
- return computers.values().stream().sorted(Comparator.comparing(Computer::getName)).toArray(Computer[]::new);
+ return getComputersCollection().stream().sorted(Comparator.comparing(Computer::getName)).toArray(Computer[]::new);
}
@CLIResolver
@@ -2080,7 +2080,7 @@ public Computer[] getComputers() {
|| name.equals("(master)")) // backwards compatibility for URLs
name = "";
- for (Computer c : computers.values()) {
+ for (Computer c : getComputersCollection()) {
if (c.getName().equals(name))
return c;
}
@@ -2247,6 +2247,14 @@ protected ConcurrentMap getComputerMap() {
return computers;
}
+ /**
+ * @return the collection of all {@link Computer}s in this instance.
+ */
+ @Restricted(NoExternalUse.class)
+ public Collection getComputersCollection() {
+ return computers.values();
+ }
+
/**
* Returns all {@link Node}s in the system, excluding {@link Jenkins} instance itself which
* represents the built-in node (in other words, this only returns agents).
@@ -2479,7 +2487,7 @@ protected Iterable allAsIterable() {
protected Computer get(String key) { return getComputer(key); }
@Override
- protected Collection all() { return computers.values(); }
+ protected Collection all() { return getComputersCollection(); }
})
.add(new CollectionSearchIndex() { // for users
@Override
@@ -3814,7 +3822,7 @@ private Set> _cleanUpDisconnectComputers(final List errors)
final Set> pending = new HashSet<>();
// JENKINS-28840 we know we will be interrupting all the Computers so get the Queue lock once for all
Queue.withLock(() -> {
- for (Computer c : computers.values()) {
+ for (Computer c : getComputersCollection()) {
try {
c.interrupt();
killComputer(c);
@@ -5476,6 +5484,7 @@ protected MasterComputer() {
* Returns "" to match with {@link Jenkins#getNodeName()}.
*/
@Override
+ @NonNull
public String getName() {
return "";
}
@@ -5497,6 +5506,7 @@ public String getCaption() {
}
@Override
+ @NonNull
public String getUrl() {
return "computer/(built-in)/";
}
diff --git a/core/src/main/java/jenkins/model/ModelObjectWithContextMenu.java b/core/src/main/java/jenkins/model/ModelObjectWithContextMenu.java
index f46280fef0c01..b76586be0158b 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 = "TODO")
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 TODO
+ */
+ public ContextMenu add(IComputer c) {
return add(new MenuItem()
.withDisplayName(c.getDisplayName())
.withIconClass(c.getIconClassName())
diff --git a/core/src/main/java/jenkins/model/queue/ITask.java b/core/src/main/java/jenkins/model/queue/ITask.java
new file mode 100644
index 0000000000000..d381f24868ecd
--- /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 TODO
+ */
+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/resources/hudson/model/ComputerSet/index.jelly b/core/src/main/resources/hudson/model/ComputerSet/index.jelly
index f261e1f3f8cd2..1b3c9aa1ea02a 100644
--- a/core/src/main/resources/hudson/model/ComputerSet/index.jelly
+++ b/core/src/main/resources/hudson/model/ComputerSet/index.jelly
@@ -72,7 +72,7 @@ THE SOFTWARE.
-
+
@@ -93,7 +93,7 @@ THE SOFTWARE.
|
-
+
|