diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java index 081f73dd1625..1047fe97000c 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 f25d25a19a50..f8e4905b0904 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 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 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 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 94aa1c770d21..f86dbd7c6c0a 100644 --- a/core/src/main/java/hudson/model/Executor.java +++ b/core/src/main/java/hudson/model/Executor.java @@ -61,6 +61,7 @@ import java.util.stream.Collectors; import jenkins.model.CauseOfInterruption; import jenkins.model.CauseOfInterruption.UserInterruption; +import jenkins.model.IExecutor; import jenkins.model.InterruptedBuildAction; import jenkins.model.Jenkins; import jenkins.model.queue.AsynchronousExecution; @@ -88,7 +89,7 @@ * @author Kohsuke Kawaguchi */ @ExportedBean -public class Executor extends Thread implements ModelObject { +public class Executor extends Thread implements ModelObject, IExecutor { protected final @NonNull Computer owner; private final Queue queue; private final ReadWriteLock lock = new ReentrantReadWriteLock(); @@ -526,6 +527,7 @@ public void completedAsynchronous(@CheckForNull Throwable error) { * @return * null if the executor is idle. */ + @Override public @CheckForNull Queue.Executable getCurrentExecutable() { lock.readLock().lock(); try { @@ -555,14 +557,8 @@ public Queue.Executable getCurrentExecutableForApi() { return Collections.unmodifiableCollection(causes); } - /** - * Returns the current {@link WorkUnit} (of {@link #getCurrentExecutable() the current executable}) - * that this executor is running. - * - * @return - * null if the executor is idle. - */ @CheckForNull + @Override public WorkUnit getCurrentWorkUnit() { lock.readLock().lock(); try { @@ -601,22 +597,14 @@ public String getDisplayName() { return "Executor #" + getNumber(); } - /** - * Gets the executor number that uniquely identifies it among - * other {@link Executor}s for the same computer. - * - * @return - * a sequential number starting from 0. - */ @Exported + @Override public int getNumber() { return number; } - /** - * Returns true if this {@link Executor} is ready for action. - */ @Exported + @Override public boolean isIdle() { lock.readLock().lock(); try { @@ -705,13 +693,8 @@ public boolean isParking() { return null; } - /** - * Returns the progress of the current build in the number between 0-100. - * - * @return -1 - * if it's impossible to estimate the progress. - */ @Exported + @Override public int getProgress() { long d = executableEstimatedDuration; if (d <= 0) { @@ -725,14 +708,8 @@ public int getProgress() { return num; } - /** - * Returns true if the current build is likely stuck. - * - *

- * This is a heuristics based approach, but if the build is suspiciously taking for a long time, - * this method returns true. - */ @Exported + @Override public boolean isLikelyStuck() { lock.readLock().lock(); try { @@ -754,6 +731,7 @@ public boolean isLikelyStuck() { } } + @Override public long getElapsedTime() { lock.readLock().lock(); try { @@ -777,20 +755,7 @@ public long getTimeSpentInQueue() { } } - /** - * Gets the string that says how long since this build has started. - * - * @return - * string like "3 minutes" "1 day" etc. - */ - public String getTimestampString() { - return Util.getTimeSpanString(getElapsedTime()); - } - - /** - * Computes a human-readable text that shows the expected remaining time - * until the build completes. - */ + @Override public String getEstimatedRemainingTime() { long d = executableEstimatedDuration; if (d < 0) { @@ -911,9 +876,7 @@ public HttpResponse doYank() { return HttpResponses.redirectViaContextPath("/"); } - /** - * Checks if the current user has a permission to stop this build. - */ + @Override public boolean hasStopPermission() { lock.readLock().lock(); try { diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index 08eba90b906c..797cba56f394 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -1956,24 +1956,6 @@ default void checkAbortPermission() { } } - /** - * Works just like {@link #checkAbortPermission()} except it indicates the status by a return value, - * instead of exception. - * Also used by default for {@link hudson.model.Queue.Item#hasCancelPermission}. - *

- * NOTE: If you have implemented {@link AccessControlled} this returns by default - * {@code return hasPermission(hudson.model.Item.CANCEL);} - * - * @return false - * if the user doesn't have the permission. - */ - default boolean hasAbortPermission() { - if (this instanceof AccessControlled) { - return ((AccessControlled) this).hasPermission(CANCEL); - } - return true; - } - /** * Returns the URL of this task relative to the context root of the application. * @@ -1984,6 +1966,7 @@ default boolean hasAbortPermission() { * @return * URL that ends with '/'. */ + @Override String getUrl(); /** diff --git a/core/src/main/java/hudson/model/queue/SubTask.java b/core/src/main/java/hudson/model/queue/SubTask.java index 0690d074617c..9a971d9ca40b 100644 --- a/core/src/main/java/hudson/model/queue/SubTask.java +++ b/core/src/main/java/hudson/model/queue/SubTask.java @@ -33,20 +33,16 @@ import hudson.model.Queue; import hudson.model.ResourceActivity; import java.io.IOException; +import jenkins.model.queue.ITask; /** * A component of {@link Queue.Task} that represents a computation carried out by a single {@link Executor}. * * A {@link Queue.Task} consists of a number of {@link SubTask}. * - *

- * Plugins are encouraged to extend from {@link AbstractSubTask} - * instead of implementing this interface directly, to maintain - * compatibility with future changes to this interface. - * * @since 1.377 */ -public interface SubTask extends ResourceActivity { +public interface SubTask extends ResourceActivity, ITask { /** * If this task needs to be run on a node with a particular label, * return that {@link Label}. Otherwise null, indicating @@ -115,4 +111,13 @@ default long getEstimatedDuration() { default Object getSameNodeConstraint() { return null; } + + /** + * A subtask may not be reachable by its own URL. In that case, this method should return null. + * @return the URL where to reach specifically this subtask, relative to Jenkins URL. If non-null, must end with '/'. + */ + @Override + default String getUrl() { + return null; + } } diff --git a/core/src/main/java/hudson/security/AuthorizationStrategy.java b/core/src/main/java/hudson/security/AuthorizationStrategy.java index db3001fc40f2..08fa10fe9897 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 000000000000..f7559ff85799 --- /dev/null +++ b/core/src/main/java/jenkins/model/DisplayExecutor.java @@ -0,0 +1,97 @@ +/* + * The MIT License + * + * Copyright 2024 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.model; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.model.Executor; +import hudson.model.ModelObject; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * A value class providing a consistent snapshot view of the state of an executor to avoid race conditions + * during rendering of the executors list. + */ +@Restricted(NoExternalUse.class) +public class DisplayExecutor implements ModelObject, IDisplayExecutor { + + @NonNull + private final String displayName; + @NonNull + private final String url; + @NonNull + private final Executor executor; + + public DisplayExecutor(@NonNull String displayName, @NonNull String url, @NonNull Executor executor) { + this.displayName = displayName; + this.url = url; + this.executor = executor; + } + + @Override + @NonNull + public String getDisplayName() { + return displayName; + } + + @Override + @NonNull + public String getUrl() { + return url; + } + + @Override + @NonNull + public Executor getExecutor() { + return executor; + } + + @Override + public String toString() { + return "DisplayExecutor{" + "displayName='" + displayName + '\'' + + ", url='" + url + '\'' + + ", executor=" + executor + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DisplayExecutor that = (DisplayExecutor) o; + + return executor.equals(that.executor); + } + + @Override + public int hashCode() { + return executor.hashCode(); + } +} diff --git a/core/src/main/java/jenkins/model/IComputer.java b/core/src/main/java/jenkins/model/IComputer.java new file mode 100644 index 000000000000..f975f200f669 --- /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 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 000000000000..af10341349d4 --- /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 000000000000..35894f33e4ab --- /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 705044cd8fa0..6ea969668669 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 f46280fef0c0..b76586be0158 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 000000000000..d381f24868ec --- /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 f261e1f3f8cd..1b3c9aa1ea02 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. - +

- +
diff --git a/test/src/test/java/hudson/model/ComputerSetTest.java b/test/src/test/java/hudson/model/ComputerSetTest.java index f40cf3e00c96..e137cbfec823 100644 --- a/test/src/test/java/hudson/model/ComputerSetTest.java +++ b/test/src/test/java/hudson/model/ComputerSetTest.java @@ -26,7 +26,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; @@ -91,10 +90,10 @@ public void nodeOfflineCli() throws Exception { @Test public void getComputerNames() throws Exception { assertThat(ComputerSet.getComputerNames(), is(empty())); - j.createSlave("aNode", "", null); - assertThat(ComputerSet.getComputerNames(), contains("aNode")); j.createSlave("anAnotherNode", "", null); - assertThat(ComputerSet.getComputerNames(), containsInAnyOrder("aNode", "anAnotherNode")); + assertThat(ComputerSet.getComputerNames(), contains("anAnotherNode")); + j.createSlave("aNode", "", null); + assertThat(ComputerSet.getComputerNames(), contains("aNode", "anAnotherNode")); } @Test