diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index d2b62e99a2f2..46794c280264 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; @@ -157,6 +156,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import jenkins.console.ConsoleUrlProvider; +import jenkins.console.WithConsoleUrl; import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; import jenkins.model.Jenkins; @@ -1981,20 +1981,14 @@ public static String joinPath(String... components) { } /** - * Computes the link to the console for the run for the specified executable, taking {@link ConsoleUrlProvider} into account. - * @param executable the executable (normally a {@link Run}) - * @return the absolute URL for accessing the build console for the executable, or null if there is no build associated with the executable + * Computes the link to the console for the run for the specified object, taking {@link ConsoleUrlProvider} into account. + * @param withConsoleUrl the object to compute a console url for (can be {@link Run}, a {@code PlaceholderExecutable}...) + * @return the absolute URL for accessing the build console for the given object, or null if there is no console URL defined for the object. * @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); - } else { - // Handles cases such as PlaceholderExecutable for Pipeline node steps. - return getConsoleUrl(executable.getParentExecutable()); - } + public static @CheckForNull String getConsoleUrl(WithConsoleUrl withConsoleUrl) { + String consoleUrl = withConsoleUrl.getConsoleUrl(); + return consoleUrl != null ? Stapler.getCurrentRequest().getContextPath() + '/' + consoleUrl : null; } /** diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index a8d4ca08ea0d..08eba90b906c 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.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; +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 c6eedc42785f..f4084bcbf254 100644 --- a/core/src/main/java/hudson/model/Run.java +++ b/core/src/main/java/hudson/model/Run.java @@ -109,6 +109,8 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.GZIPInputStream; +import jenkins.console.ConsoleUrlProvider; +import jenkins.console.WithConsoleUrl; import jenkins.model.ArtifactManager; import jenkins.model.ArtifactManagerConfiguration; import jenkins.model.ArtifactManagerFactory; @@ -157,7 +159,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. @@ -1059,6 +1061,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 3310f13fd332..dcd3883ba07c 100644 --- a/core/src/main/java/jenkins/console/ConsoleUrlProvider.java +++ b/core/src/main/java/jenkins/console/ConsoleUrlProvider.java @@ -62,8 +62,8 @@ 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} - * if this implementation does not want to server a special console view for this build. + * @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 serve 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.getCurrentRequest2().getContextPath() + url; - } else { - return Stapler.getCurrentRequest2().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(); +} diff --git a/test/src/test/java/jenkins/console/ConsoleUrlProviderTest.java b/test/src/test/java/jenkins/console/ConsoleUrlProviderTest.java index b9881fbeddac..9a97ff0748df 100644 --- a/test/src/test/java/jenkins/console/ConsoleUrlProviderTest.java +++ b/test/src/test/java/jenkins/console/ConsoleUrlProviderTest.java @@ -58,9 +58,9 @@ public void getRedirectUrl() throws Exception { // Custom URL without leading slash b.setDescription("custom my/build/console"); assertCustomConsoleUrl(r.contextPath + "/my/build/console", b); - // Custom URL with leading slash + // Custom URL with leading slash -> not supported, falls back to default b.setDescription("custom /my/build/console"); - assertCustomConsoleUrl(r.contextPath + "/my/build/console", b); + assertCustomConsoleUrl(r.contextPath + '/' + b.getUrl() + "console", b); // Default URL is used when extensions throw exceptions. b.setDescription("NullPointerException"); assertCustomConsoleUrl(r.contextPath + '/' + b.getUrl() + "console", b);