diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index cd25f9bc9871..669d13ca85f3 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -54,7 +54,6 @@ import hudson.model.ParameterDefinition; import hudson.model.ParameterDefinition.ParameterDescriptor; import hudson.model.PasswordParameterDefinition; -import hudson.model.Queue; import hudson.model.Run; import hudson.model.Slave; import hudson.model.TimeZoneProperty; @@ -153,6 +152,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import jenkins.console.ConsoleUrlProvider; +import jenkins.console.WithConsoleUrl; import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; import jenkins.model.Jenkins; @@ -1912,14 +1912,12 @@ public static String joinPath(String... components) { * @return the absolute URL for accessing the build console for the executable, or null if there is no build associated with the executable * @since 2.433 */ - public static @CheckForNull String getConsoleUrl(Queue.Executable executable) { - if (executable == null) { - return null; - } else if (executable instanceof Run) { - return ConsoleUrlProvider.getRedirectUrl((Run) executable); + public static @CheckForNull String getConsoleUrl(Object executable) { + if (executable instanceof WithConsoleUrl) { + String consoleUrl = ((WithConsoleUrl) executable).getConsoleUrl(); + return consoleUrl != null ? Stapler.getCurrentRequest().getContextPath() + '/' + consoleUrl : null; } else { - // Handles cases such as PlaceholderExecutable for Pipeline node steps. - return getConsoleUrl(executable.getParentExecutable()); + return null; } } diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index 0d299fb9426d..541762a309b8 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -109,6 +109,7 @@ import java.util.stream.Collectors; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; +import jenkins.console.WithConsoleUrl; import jenkins.model.Jenkins; import jenkins.model.queue.AsynchronousExecution; import jenkins.model.queue.CompositeCauseOfBlockage; @@ -2088,7 +2089,7 @@ default Collection getSubTasks() { * used to render the HTML that indicates this executable is executing. */ @StaplerAccessibleType - public interface Executable extends Runnable { + public interface Executable extends Runnable, WithConsoleUrl { /** * Task from which this executable was created. * @@ -2131,6 +2132,16 @@ default long getEstimatedDuration() { return Executables.getParentOf(this).getEstimatedDuration(); } + /** + * Handles cases such as {@code PlaceholderExecutable} for Pipeline node steps. + * @return by default, that of {@link #getParentExecutable} if defined + */ + @Override + default String getConsoleUrl() { + Executable parent = getParentExecutable(); + return parent != null ? parent.getConsoleUrl() : null; + } + /** * Used to render the HTML. Should be a human readable text of what this executable is. */ diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java index 33658252b922..f50136df5aa2 100644 --- a/core/src/main/java/hudson/model/Run.java +++ b/core/src/main/java/hudson/model/Run.java @@ -108,6 +108,8 @@ import java.util.zip.GZIPInputStream; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; +import jenkins.console.ConsoleUrlProvider; +import jenkins.console.WithConsoleUrl; import jenkins.model.ArtifactManager; import jenkins.model.ArtifactManagerConfiguration; import jenkins.model.ArtifactManagerFactory; @@ -153,7 +155,7 @@ */ @ExportedBean public abstract class Run, RunT extends Run> - extends Actionable implements ExtensionPoint, Comparable, AccessControlled, PersistenceRoot, DescriptorByNameOwner, OnMaster, StaplerProxy { + extends Actionable implements ExtensionPoint, Comparable, AccessControlled, PersistenceRoot, DescriptorByNameOwner, OnMaster, StaplerProxy, WithConsoleUrl { /** * The original {@link Queue.Item#getId()} has not yet been mapped onto the {@link Run} instance. @@ -1055,6 +1057,14 @@ protected void dropLinks() { return project.getUrl() + getNumber() + '/'; } + /** + * @see ConsoleUrlProvider#consoleUrlOf + */ + @Override + public String getConsoleUrl() { + return ConsoleUrlProvider.consoleUrlOf(this); + } + /** * Obtains the absolute URL to this build. * diff --git a/core/src/main/java/jenkins/console/ConsoleUrlProvider.java b/core/src/main/java/jenkins/console/ConsoleUrlProvider.java index 0b9e70c3db53..53f0e54ff03e 100644 --- a/core/src/main/java/jenkins/console/ConsoleUrlProvider.java +++ b/core/src/main/java/jenkins/console/ConsoleUrlProvider.java @@ -62,7 +62,7 @@ public interface ConsoleUrlProvider extends Describable { * Get a URL relative to the context path of Jenkins which should be used to link to the console for the specified build. *

Should only be used in the context of serving an HTTP request. * @param run the build - * @return the URL for the console for the specified build, relative to the context of Jenkins, or {@code null} + * @return the URL for the console for the specified build, relative to the context of Jenkins (should not start with {@code /}), or {@code null} * if this implementation does not want to server a special console view for this build. */ @CheckForNull String getConsoleUrl(Run run); @@ -80,6 +80,14 @@ default Descriptor getDescriptor() { * @return the URL for the console for the specified build, relative to the web server root */ static @NonNull String getRedirectUrl(Run run) { + return Stapler.getCurrentRequest().getContextPath() + '/' + run.getConsoleUrl(); + } + + /** + * Looks up the {@link #getConsoleUrl} value from the first provider to offer one. + * @since TODO + */ + static @NonNull String consoleUrlOf(Run run) { final List providers = new ArrayList<>(); User currentUser = User.current(); if (currentUser != null) { @@ -104,6 +112,8 @@ default Descriptor getDescriptor() { if (tempUrl != null) { if (new URI(tempUrl).isAbsolute()) { LOGGER.warning(() -> "Ignoring absolute console URL " + tempUrl + " for " + run + " from " + provider.getClass()); + } else if (tempUrl.startsWith("/")) { + LOGGER.warning(() -> "Ignoring URL " + tempUrl + " starting with / for " + run + " from " + provider.getClass()); } else { // Found a valid non-null URL. url = tempUrl; @@ -118,11 +128,7 @@ default Descriptor getDescriptor() { // Reachable if DefaultConsoleUrlProvider is not one of the configured providers, including if no providers are configured at all. url = run.getUrl() + "console"; } - if (url.startsWith("/")) { - return Stapler.getCurrentRequest().getContextPath() + url; - } else { - return Stapler.getCurrentRequest().getContextPath() + '/' + url; - } + return url; } /** diff --git a/core/src/main/java/jenkins/console/WithConsoleUrl.java b/core/src/main/java/jenkins/console/WithConsoleUrl.java new file mode 100644 index 000000000000..d667b40fae66 --- /dev/null +++ b/core/src/main/java/jenkins/console/WithConsoleUrl.java @@ -0,0 +1,43 @@ +/* + * 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.console; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +/** + * A model object that may have a console URL. + */ +@Restricted(Beta.class) +public interface WithConsoleUrl { + + /** + * @return a URL relative to the context root without leading slash, such as {@code job/xxx/123/console}; + * or null if unknown or not applicable + */ + @CheckForNull + String getConsoleUrl(); +}