diff --git a/core/src/main/java/hudson/model/AbstractBuild.java b/core/src/main/java/hudson/model/AbstractBuild.java index 2268f3dd9967..0315886dd328 100644 --- a/core/src/main/java/hudson/model/AbstractBuild.java +++ b/core/src/main/java/hudson/model/AbstractBuild.java @@ -1,7 +1,8 @@ /* * The MIT License * - * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo! Inc., CloudBees, Inc. + * Copyright (c) 2004-2018, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo! Inc., + * CloudBees Inc., Oleg Nenashev and other contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -530,6 +531,10 @@ protected Launcher createLauncher(@Nonnull BuildListener listener) throws IOExce final Node currentNode = getCurrentNode(); Launcher l = currentNode.createLauncher(listener); + // Produce correct logger + // TODO: Consider merging with create Launcher + l = getLoggingMethod().decorateLauncher(l, getBuild(), currentNode); + if (project instanceof BuildableItemWithBuildWrappers) { BuildableItemWithBuildWrappers biwbw = (BuildableItemWithBuildWrappers) project; for (BuildWrapper bw : biwbw.getBuildWrappersList()) diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java index 76122707fc9a..f041d436483a 100644 --- a/core/src/main/java/hudson/model/Run.java +++ b/core/src/main/java/hudson/model/Run.java @@ -1,11 +1,10 @@ /* * The MIT License * - * Copyright (c) 2004-2012, Sun Microsystems, Inc., Kohsuke Kawaguchi, + * Copyright (c) 2004-2018, Sun Microsystems, Inc., Kohsuke Kawaguchi, * Daniel Dyer, Red Hat, Inc., Tom Huybrechts, Romain Seguy, Yahoo! Inc., - * Darek Ostolski, CloudBees, Inc. - * - * Copyright (c) 2012, Martin Schroeder, Intel Mobile Communications GmbH + * Martin Schroeder, Intel Mobile Communications GmbH, + * Darek Ostolski, CloudBees, Inc., Oleg Nenashev and other contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -27,7 +26,6 @@ */ package hudson.model; -import com.jcraft.jzlib.GZIPInputStream; import com.thoughtworks.xstream.XStream; import hudson.AbortException; import hudson.BulkChange; @@ -37,13 +35,16 @@ import hudson.FeedAdapter; import hudson.Functions; import hudson.console.AnnotatedLargeText; -import hudson.console.ConsoleLogFilter; -import hudson.console.ConsoleNote; import hudson.console.ModelHyperlinkNote; import hudson.console.PlainTextConsoleOutputStream; -import java.nio.file.Files; -import java.nio.file.InvalidPathException; -import java.nio.file.StandardOpenOption; + +import java.io.Closeable; + +import jenkins.model.logging.LogBrowser; +import jenkins.model.logging.LogHandler; +import jenkins.model.logging.Loggable; +import jenkins.model.logging.impl.FileLogBrowser; +import jenkins.model.logging.impl.FileLogStorage; import jenkins.util.SystemProperties; import hudson.Util; import hudson.XmlFile; @@ -65,14 +66,11 @@ import hudson.util.ProcessTree; import hudson.util.XStream2; -import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; -import java.io.RandomAccessFile; import java.io.Reader; import java.io.Serializable; import java.nio.charset.Charset; @@ -108,11 +106,13 @@ import jenkins.model.BuildDiscarder; import jenkins.model.Jenkins; import jenkins.model.JenkinsLocationConfiguration; +import jenkins.model.logging.LoggingMethod; import jenkins.model.PeepholePermalink; import jenkins.model.RunAction2; import jenkins.model.StandardArtifactManager; import jenkins.model.lazy.BuildReference; import jenkins.model.lazy.LazyBuildMixIn; +import jenkins.model.logging.LoggingMethodLocator; import jenkins.security.MasterToSlaveCallable; import jenkins.util.VirtualFile; import jenkins.util.io.OnMaster; @@ -121,7 +121,6 @@ import org.acegisecurity.Authentication; import org.apache.commons.io.IOUtils; import org.apache.commons.jelly.XMLOutput; -import org.apache.commons.lang.ArrayUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.HttpResponse; @@ -148,7 +147,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, Loggable { /** * The original {@link Queue.Item#getId()} has not yet been mapped onto the {@link Run} instance. @@ -293,6 +292,19 @@ private static enum State { */ private @CheckForNull ArtifactManager artifactManager; + /** + * Loging method associated with this build, if any. + * @since TODO + */ + private @CheckForNull LoggingMethod loggingMethod; + + /** + * Log browser associated with this build, if any. + * @since TODO + */ + private @CheckForNull LogBrowser logBrowser; + + /** * Creates a new {@link Run}. * @param job Owner job @@ -349,6 +361,13 @@ public void reload() throws IOException { LOGGER.log(WARNING, "reload {0} @{1} with anomalous state {2}", new Object[] {this, hashCode(), state}); } + if (logBrowser == null) { + logBrowser = new FileLogBrowser(this); + } + if (loggingMethod == null) { + loggingMethod = new FileLogStorage(this); + } + // not calling onLoad upon reload. partly because we don't want to call that from Run constructor, // and partly because some existing use of onLoad isn't assuming that it can be invoked multiple times. } @@ -370,9 +389,12 @@ protected void onLoad() { ((RunAction) a).onLoad(); } } + if (artifactManager != null) { artifactManager.onLoad(this); } + LogHandler.onLoad(this, logBrowser); + LogHandler.onLoad(this, loggingMethod); } /** @@ -523,6 +545,11 @@ public boolean isLogUpdated() { return state.compareTo(State.COMPLETED) < 0; } + @Override + public boolean isLoggingFinished() { + return !isLogUpdated(); + } + /** * Gets the {@link Executor} building this job, if it's being built. * Otherwise null. @@ -557,7 +584,8 @@ public boolean isLogUpdated() { * Gets the charset in which the log file is written. * @return never null. * @since 1.257 - */ + */ + @Override public final @Nonnull Charset getCharset() { if(charset==null) return Charset.defaultCharset(); return Charset.forName(charset); @@ -1427,12 +1455,34 @@ public Collection getBuildFingerprints() { } return Collections.emptyList(); } - - /** - * Returns the log file. - * @return The file may reference both uncompressed or compressed logs - */ - public @Nonnull File getLogFile() { + + @Override + @Exported + public LogBrowser getLogBrowser() { + if (logBrowser == null) { + //TODO(oleg_nenashev) WorkflowRun does not allow capturing Logging method in a better way + // In order to prevent cases between new objects and data migration, we rely on reload() + // which acts as readResolve() for the Run object + logBrowser = LoggingMethodLocator.locateBrowser(this); + } + return logBrowser; + } + + @Override + @Exported + public LoggingMethod getLoggingMethod() { + if (loggingMethod == null) { + //TODO(oleg_nenashev) WorkflowRun does not allow capturing Logging method in a better way + // In order to prevent cases between new objects and data migration, we rely on reload() + // which acts as readResolve() for the Run object + loggingMethod = LoggingMethodLocator.locate(this); + } + return loggingMethod; + } + + @CheckForNull + @Override + public File getLogFileCompatLocation() { File rawF = new File(getRootDir(), "log"); if (rawF.isFile()) { return rawF; @@ -1445,39 +1495,52 @@ public Collection getBuildFingerprints() { return rawF; } + /** + * Gets Log file. + * @deprecated Not all {@link jenkins.model.logging.LogBrowser} implementations + * are able to produce the log file efficiently. + * It is recommended to use the {@link #getLogBrowser()} + * to get browser with better API options. + * @return Log file. + * It is never {@code null}, but the file may not exist + */ + @Deprecated + @Nonnull + public File getLogFile() { + try { + return getLogBrowser().getLogFile(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to locate log file for " + this, e); + return new File(getRootDir(), "log"); + } + } + + /** + * Deletes the log in the storage. + * @return {@code true} if the log was deleted. + * {@code false} if Log deletion is not supported. + * @throws IOException Failed to delete the log. + * @since TODO + */ + public boolean deleteLog() throws IOException { + return getLogBrowser().deleteLog(); + } + /** * Returns an input stream that reads from the log file. * It will use a gzip-compressed log file (log.gz) if that exists. * - * @throws IOException + * @throws IOException Operation error * @return An input stream from the log file. * If the log file does not exist, the error message will be returned to the output. * @since 1.349 */ public @Nonnull InputStream getLogInputStream() throws IOException { - File logFile = getLogFile(); - - if (logFile.exists() ) { - // Checking if a ".gz" file was return - try { - InputStream fis = Files.newInputStream(logFile.toPath()); - if (logFile.getName().endsWith(".gz")) { - return new GZIPInputStream(fis); - } else { - return fis; - } - } catch (InvalidPathException e) { - throw new IOException(e); - } - } - - String message = "No such file: " + logFile; - return new ByteArrayInputStream(charset != null ? message.getBytes(charset) : message.getBytes()); + return getLogBrowser().getLogInputStream(); } public @Nonnull Reader getLogReader() throws IOException { - if (charset==null) return new InputStreamReader(getLogInputStream()); - else return new InputStreamReader(getLogInputStream(),charset); + return getLogBrowser().getLogReader(); } /** @@ -1517,7 +1580,7 @@ public void writeWholeLogTo(@Nonnull OutputStream out) throws IOException, Inter * @return A {@link Run} log with annotations */ public @Nonnull AnnotatedLargeText getLogText() { - return new AnnotatedLargeText(getLogFile(),getCharset(),!isLogUpdated(),this); + return getLogBrowser().overallLog(); } @Override @@ -1576,6 +1639,11 @@ public void delete() throws IOException { deleteArtifacts(); } // for StandardArtifactManager, deleting the whole build dir suffices + final LogBrowser browser = getLogBrowser(); + if (browser instanceof FileLogBrowser) { + browser.deleteLog(); + } // for standard FileLogBrowser, deleting the whole build dir suffices + synchronized (this) { // avoid holding a lock while calling plugin impls of onDeleted File tmp = new File(rootDir.getParentFile(),'.'+rootDir.getName()); @@ -1774,8 +1842,7 @@ protected final void execute(@Nonnull RunExecution job) { if(result!=null) return; // already built. - OutputStream logger = null; - StreamBuildListener listener=null; + BuildListener listener = null; runner = job; onStartBuilding(); @@ -1788,13 +1855,11 @@ protected final void execute(@Nonnull RunExecution job) { try { try { Computer computer = Computer.currentComputer(); - Charset charset = null; if (computer != null) { - charset = computer.getDefaultCharset(); + Charset charset = computer.getDefaultCharset(); this.charset = charset.name(); } - logger = createLogger(); - listener = createBuildListener(job, logger, charset); + listener = getLoggingMethod().createBuildListener(); listener.started(getCauses()); Authentication auth = Jenkins.getAuthentication(); @@ -1866,7 +1931,13 @@ protected final void execute(@Nonnull RunExecution job) { // too late to update the result now } listener.finished(result); - listener.closeQuietly(); + if (listener instanceof Closeable) { + try { + ((Closeable)listener).close(); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "Failed to close the build listener for " + Run.this, ex); + } + } } try { @@ -1883,46 +1954,17 @@ protected final void execute(@Nonnull RunExecution job) { } } finally { onEndBuilding(); - if (logger != null) { + //TODO(oleg_nenashev): DRY? there is Close operation above + if (listener instanceof Closeable) { try { - logger.close(); - } catch (IOException x) { - LOGGER.log(Level.WARNING, "failed to close log for " + Run.this, x); + ((Closeable)listener).close(); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "Failed to close the build listener for " + Run.this, ex); } } } } - private OutputStream createLogger() throws IOException { - // don't do buffering so that what's written to the listener - // gets reflected to the file immediately, which can then be - // served to the browser immediately - try { - return Files.newOutputStream(getLogFile().toPath(), StandardOpenOption.CREATE, StandardOpenOption.APPEND); - } catch (InvalidPathException e) { - throw new IOException(e); - } - } - - private StreamBuildListener createBuildListener(@Nonnull RunExecution job, OutputStream logger, Charset charset) throws IOException, InterruptedException { - RunT build = job.getBuild(); - - // Global log filters - for (ConsoleLogFilter filter : ConsoleLogFilter.all()) { - logger = filter.decorateLogger(build, logger); - } - - // Project specific log filters - if (project instanceof BuildableItemWithBuildWrappers && build instanceof AbstractBuild) { - BuildableItemWithBuildWrappers biwbw = (BuildableItemWithBuildWrappers) project; - for (BuildWrapper bw : biwbw.getBuildWrappersList()) { - logger = bw.decorateLogger((AbstractBuild) build, logger); - } - } - - return new StreamBuildListener(logger,charset); - } - /** * Makes sure that {@code lastSuccessful} and {@code lastStable} legacy links in the project’s root directory exist. * Normally you do not need to call this explicitly, since {@link #execute} does so, @@ -2039,7 +2081,7 @@ private Object readResolve() { */ @Deprecated public @Nonnull String getLog() throws IOException { - return Util.loadFile(getLogFile(),getCharset()); + return getLogBrowser().getLog(); } /** @@ -2052,54 +2094,7 @@ private Object readResolve() { * @throws IOException If there is a problem reading the log file. */ public @Nonnull List getLog(int maxLines) throws IOException { - if (maxLines == 0) { - return Collections.emptyList(); - } - - int lines = 0; - long filePointer; - final List lastLines = new ArrayList<>(Math.min(maxLines, 128)); - final List bytes = new ArrayList<>(); - - try (RandomAccessFile fileHandler = new RandomAccessFile(getLogFile(), "r")) { - long fileLength = fileHandler.length() - 1; - - for (filePointer = fileLength; filePointer != -1 && maxLines != lines; filePointer--) { - fileHandler.seek(filePointer); - byte readByte = fileHandler.readByte(); - - if (readByte == 0x0A) { - if (filePointer < fileLength) { - lines = lines + 1; - lastLines.add(convertBytesToString(bytes)); - bytes.clear(); - } - } else if (readByte != 0xD) { - bytes.add(readByte); - } - } - } - - if (lines != maxLines) { - lastLines.add(convertBytesToString(bytes)); - } - - Collections.reverse(lastLines); - - // If the log has been truncated, include that information. - // Use set (replaces the first element) rather than add so that - // the list doesn't grow beyond the specified maximum number of lines. - if (lines == maxLines) { - lastLines.set(0, "[...truncated " + Functions.humanReadableByteSize(filePointer)+ "...]"); - } - - return ConsoleNote.removeNotes(lastLines); - } - - private String convertBytesToString(List bytes) { - Collections.reverse(bytes); - Byte[] byteArray = bytes.toArray(new Byte[bytes.size()]); - return new String(ArrayUtils.toPrimitive(byteArray), getCharset()); + return getLogBrowser().getLog(maxLines); } public void doBuildStatus( StaplerRequest req, StaplerResponse rsp ) throws IOException { @@ -2581,6 +2576,15 @@ public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) } @Override + public LoggingMethod getDefaultLoggingMethod() { + return new FileLogStorage(this); + } + + @Override + public LogBrowser getDefaultLogBrowser() { + return new FileLogBrowser(this); + } + @Restricted(NoExternalUse.class) public Object getTarget() { if (!SKIP_PERMISSION_CHECK) { @@ -2599,7 +2603,6 @@ public Object getTarget() { @Restricted(NoExternalUse.class) public static /* Script Console modifiable */ boolean SKIP_PERMISSION_CHECK = Boolean.getBoolean(Run.class.getName() + ".skipPermissionCheck"); - public static class RedirectUp { public void doDynamic(StaplerResponse rsp) throws IOException { // Compromise to handle both browsers (auto-redirect) and programmatic access diff --git a/core/src/main/java/jenkins/model/logging/LogBrowser.java b/core/src/main/java/jenkins/model/logging/LogBrowser.java new file mode 100644 index 000000000000..fcabe952eee9 --- /dev/null +++ b/core/src/main/java/jenkins/model/logging/LogBrowser.java @@ -0,0 +1,138 @@ +/* + * The MIT License + * + * Copyright 2018 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.logging; + +import hudson.console.AnnotatedLargeText; +import hudson.console.ConsoleNote; +import hudson.model.Run; +import jenkins.util.io.OnMaster; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.util.LinkedList; +import java.util.List; + +/** + * Defines how the logs should be browsed. + * @author Oleg Nenashev + * @author Jesse Glick + * @since TODO + */ +@Restricted(Beta.class) +public abstract class LogBrowser extends LogHandler implements OnMaster { + + public LogBrowser(Loggable loggable) { + super(loggable); + } + + /** + * Gets log for an object. + * @return Created log or {@link jenkins.model.logging.impl.BrokenAnnotatedLargeText} if it cannot be retrieved + */ + @Nonnull + public abstract AnnotatedLargeText overallLog(); + + //TODO: jglick requests justification of why it needs to be in the core + /** + * Gets log for a part of the object. + * @param stepId Identifier of the step to be displayed. + * It may be Pipeline step or other similar abstraction + * @param completed indicates that the step is completed + * @return Created log or {@link jenkins.model.logging.impl.BrokenAnnotatedLargeText} if it cannot be retrieved + */ + @Nonnull + public abstract AnnotatedLargeText stepLog(@CheckForNull String stepId, boolean completed); + + public InputStream getLogInputStream() throws IOException { + // Inefficient but probably rarely used anyway. + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + overallLog().writeRawLogTo(0, baos); + return new ByteArrayInputStream(baos.toByteArray()); + } + + public Reader getLogReader() throws IOException { + // As above. + return overallLog().readAll(); + } + + @SuppressWarnings("deprecation") + public String getLog() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + overallLog().writeRawLogTo(0, baos); + return baos.toString("UTF-8"); + } + + public List getLog(int maxLines) throws IOException { + int lineCount = 0; + List logLines = new LinkedList<>(); + if (maxLines == 0) { + return logLines; + } + try (BufferedReader reader = new BufferedReader(getLogReader())) { + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + logLines.add(line); + ++lineCount; + if (lineCount > maxLines) { + logLines.remove(0); + } + } + } + if (lineCount > maxLines) { + logLines.set(0, "[...truncated " + (lineCount - (maxLines - 1)) + " lines...]"); + } + return ConsoleNote.removeNotes(logLines); + } + + /** + * Gets log as a file. + * This is a compatibility method, which is used in {@link Run#getLogFile()}. + * {@link LogBrowser} implementations may provide it, e.g. by creating temporary files if needed. + * @return Log file. If it does not exist, {@link IOException} should be thrown + * @throws IOException Log file cannot be retrieved + * @deprecated The method is available for compatibility purposes only + */ + @Deprecated + @Nonnull + public abstract File getLogFile() throws IOException; + + /** + * Deletes the log in the storage. + * @return {@code true} if the log was deleted. + * {@code false} if Log deletion is not supported. + * @throws IOException Failed to delete the log. + */ + public abstract boolean deleteLog() throws IOException; + +} diff --git a/core/src/main/java/jenkins/model/logging/LogHandler.java b/core/src/main/java/jenkins/model/logging/LogHandler.java new file mode 100644 index 000000000000..42e1655c6be4 --- /dev/null +++ b/core/src/main/java/jenkins/model/logging/LogHandler.java @@ -0,0 +1,78 @@ +/* + * The MIT License + * + * Copyright 2018 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.logging; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; +import org.kohsuke.stapler.export.Exported; +import org.kohsuke.stapler.export.ExportedBean; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.Serializable; + +/** + * Object which operates with {@link Loggable} items. + * @author Oleg Nenashev + * @since TODO + */ +@ExportedBean +@Restricted(Beta.class) +public abstract class LogHandler { + + protected transient Loggable loggable; + + public LogHandler(@Nonnull Loggable loggable) { + this.loggable = loggable; + } + + @Exported + public String getId() { + return getClass().getName(); + } + + /** + * Called when the owner is loaded from disk. + * The owner may be persisted on the disk, so the build reference should be {@code transient} (quasi-{@code final}) and restored here. + * @param loggable an owner to which this component is associated. + */ + public void onLoad(@Nonnull Loggable loggable) { + this.loggable = loggable; + } + + public static void onLoad(@Nonnull Loggable loggable, @CheckForNull LogHandler logHandler) { + if (logHandler != null) { + logHandler.onLoad(loggable); + } + } + + @Nonnull + protected Loggable getOwner() { + if (loggable == null) { + throw new IllegalStateException("Owner has not been assigned to the object yet"); + } + return loggable; + + } +} diff --git a/core/src/main/java/jenkins/model/logging/Loggable.java b/core/src/main/java/jenkins/model/logging/Loggable.java new file mode 100644 index 000000000000..038c33a761de --- /dev/null +++ b/core/src/main/java/jenkins/model/logging/Loggable.java @@ -0,0 +1,107 @@ +/* + * The MIT License + * + * Copyright 2018 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.logging; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.File; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * Interface which indicates that custom logging is applicable to the object. + * @author Oleg Nenashev + * @since TODO + * @see LogBrowser + * @see LoggingMethod + */ +@Restricted(Beta.class) +public interface Loggable { + + @Nonnull + default LoggingMethod getLoggingMethod() { + return LoggingMethodLocator.locate(this); + } + + /** + * Determines a default logger to be used. + * @return Default logger. + */ + @Nonnull + LoggingMethod getDefaultLoggingMethod(); + + @Nonnull + default LogBrowser getLogBrowser() { + return LoggingMethodLocator.locateBrowser(this); + } + + /** + * Determines a default log browser to be used. + * @return Default log browser. + */ + @Nonnull + LogBrowser getDefaultLogBrowser(); + + /** + * Returns {@code true} if the log file is no longer being updated. + */ + public boolean isLoggingFinished(); + + /** + * Returns charset to be used. + * New implementations are recommended to use {@code UTF-8}-only (default), + * but the method can be overridden by legacy implementations. + * @return Charset to be used. + */ + @Nonnull + default Charset getCharset() { + return StandardCharsets.UTF_8; + } + + /** + * Provides legacy File storage location for compatibility implementations. + * @return Log file or {@code null} if it is not supported. + * A non-existent file may be returned if log is missing in the compatibility location + * @see jenkins.model.logging.impl.FileLogBrowser + * @see jenkins.model.logging.impl.FileLogStorage + */ + @CheckForNull + default File getLogFileCompatLocation() { + return null; + } + + + /** + * Get temporary directory of the Loggable object (if exists). + * This loggable directory may be used to store temporary files if needed. + * @return Temporary directory or {@code null} if not defined + */ + @CheckForNull + default File getTmpDir() { + return null; + } +} diff --git a/core/src/main/java/jenkins/model/logging/LoggingMethod.java b/core/src/main/java/jenkins/model/logging/LoggingMethod.java new file mode 100644 index 000000000000..9e6c4fa1bfb3 --- /dev/null +++ b/core/src/main/java/jenkins/model/logging/LoggingMethod.java @@ -0,0 +1,101 @@ +/* + * The MIT License + * + * Copyright 2016-2018 CloudBees Inc., Google 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.logging; + +import hudson.Launcher; +import hudson.model.BuildListener; +import hudson.model.Node; +import hudson.model.Run; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +import hudson.model.TaskListener; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +import java.io.IOException; + +/** + * Defines logging method for Jenkins runs. + * This method defines how the log is persisted to the disk. + * @author Oleg Nenashev + * @author Xing Yan + * @see LoggingMethodLocator + * @since TODO + */ +@Restricted(Beta.class) +public abstract class LoggingMethod extends LogHandler { + + public LoggingMethod(Loggable loggable) { + super(loggable); + } + + /** + * Decorates logging on the Jenkins master side for non-{@link Run} loggable items. + * @return Log filter on the master. + * {@code null} if the implementation does not support task logging + * @throws IOException initialization error or wrong {@link Loggable} type + * @throws InterruptedException one of the build listener decorators has been interrupted. + */ + @CheckForNull + public abstract TaskListener createTaskListener() throws IOException, InterruptedException; + + /** + * Decorates logging on the Jenkins master side. + * This method should be always implemented, because it will be consuming the input events. + * Streams can be converted to per-line events by higher-level abstractions. + * + * @return Build Listener + * @throws IOException initialization error or wrong {@link Loggable} type + * @throws InterruptedException one of the build listener decorators has + * been interrupted. + */ + @Nonnull + public abstract BuildListener createBuildListener() throws IOException, InterruptedException; + + /** + * Gets default Log browser which should be used with this Logging method. + * It allows setting a custom default LogBrowser if needed. + * @return Log browser or {@code null} if not defined. + */ + @CheckForNull + public LogBrowser getDefaultLogBrowser() { + return null; + } + + /** + * Decorates external process launcher running on a node. + * It may be overridden to redirect logs to external destination + * instead of sending them by default to the master. + * @param original Original launcher + * @param run Run, for which the decoration should be performed + * @param node Target node. May be {@code master} as well + * @return Decorated launcher or {@code original} launcher + */ + @Nonnull + public Launcher decorateLauncher(@Nonnull Launcher original, + @Nonnull Run run, @Nonnull Node node) { + return original; + } +} diff --git a/core/src/main/java/jenkins/model/logging/LoggingMethodLocator.java b/core/src/main/java/jenkins/model/logging/LoggingMethodLocator.java new file mode 100644 index 000000000000..e8892d381193 --- /dev/null +++ b/core/src/main/java/jenkins/model/logging/LoggingMethodLocator.java @@ -0,0 +1,92 @@ +/* + * The MIT License + * + * Copyright 2016-2018 CloudBees Inc., Google 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.logging; + +import hudson.ExtensionList; +import hudson.ExtensionPoint; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +/** + * Locates logging methods for runs. + * @author Oleg Nenashev + * @author Xing Yan + * @since TODO + */ +@Restricted(Beta.class) +public abstract class LoggingMethodLocator implements ExtensionPoint { + + /** + * Retrieve the logging method for the run. + * @param object Loggable object + * @return Logging method. {@code null} if the locator does not provide the + * implementation for the run. + */ + @CheckForNull + protected abstract LoggingMethod getLoggingMethod(Loggable object); + + /** + * Retrieve the log browser which should be used for a run. + * @param object Loggable object + * @return Logging method. {@code null} if the locator does not provide the + * implementation for the run. + */ + @CheckForNull + protected abstract LogBrowser getLogBrowser(Loggable object); + + @Nonnull + public static LoggingMethod locate(Loggable run) { + for (LoggingMethodLocator locator : all()) { + final LoggingMethod loggingMethod = locator.getLoggingMethod(run); + if (loggingMethod != null) { + return loggingMethod; + } + } + // Fallback + return run.getDefaultLoggingMethod(); + } + + @Nonnull + public static LogBrowser locateBrowser(Loggable run) { + for (LoggingMethodLocator locator : all()) { + final LogBrowser browser = locator.getLogBrowser(run); + if (browser != null) { + return browser; + } + } + + // Fallback + LogBrowser defaultFromLogStorage = locate(run).getDefaultLogBrowser(); + return defaultFromLogStorage != null + ? defaultFromLogStorage + : run.getDefaultLogBrowser(); + } + + public static ExtensionList all() { + return ExtensionList.lookup(LoggingMethodLocator.class); + } +} \ No newline at end of file diff --git a/core/src/main/java/jenkins/model/logging/impl/BrokenAnnotatedLargeText.java b/core/src/main/java/jenkins/model/logging/impl/BrokenAnnotatedLargeText.java new file mode 100644 index 000000000000..1124d0c6b606 --- /dev/null +++ b/core/src/main/java/jenkins/model/logging/impl/BrokenAnnotatedLargeText.java @@ -0,0 +1,63 @@ +/* + * The MIT License + * + * Copyright 2018 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.logging.impl; + +import hudson.Functions; +import hudson.console.AnnotatedLargeText; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; +import org.kohsuke.stapler.framework.io.ByteBuffer; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * Annotated Large Text for a case when something goes wrong. + * @param Type + */ +@Restricted(Beta.class) +public class BrokenAnnotatedLargeText extends AnnotatedLargeText { + + public BrokenAnnotatedLargeText(Throwable cause) { + this(cause, StandardCharsets.UTF_8); + } + + public BrokenAnnotatedLargeText(@Nonnull Throwable cause, @Nonnull Charset charset) { + super(makeByteBuffer(cause, charset), charset, true, null); + } + + private static ByteBuffer makeByteBuffer(Throwable x, Charset charset) { + ByteBuffer buf = new ByteBuffer(); + byte[] stack = Functions.printThrowable(x).getBytes(StandardCharsets.UTF_8); + try { + buf.write(stack, 0, stack.length); + } catch (IOException x2) { + assert false : x2; + } + return buf; + } + +} \ No newline at end of file diff --git a/core/src/main/java/jenkins/model/logging/impl/FileLogBrowser.java b/core/src/main/java/jenkins/model/logging/impl/FileLogBrowser.java new file mode 100644 index 000000000000..bd0edd620c14 --- /dev/null +++ b/core/src/main/java/jenkins/model/logging/impl/FileLogBrowser.java @@ -0,0 +1,189 @@ +/* + * The MIT License + * + * Copyright 2018 CloudBees, Inc. and other Jenkins contributors + * + * 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.logging.impl; + +import com.jcraft.jzlib.GZIPInputStream; +import hudson.Functions; +import hudson.console.AnnotatedLargeText; +import hudson.console.ConsoleNote; +import jenkins.model.logging.LogBrowser; +import jenkins.model.logging.Loggable; +import org.apache.commons.lang.ArrayUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.RandomAccessFile; +import java.io.Reader; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Compatibility implementation for local log browser. + * @author Oleg Nenashev + * @since TODO + */ +@Restricted(Beta.class) +public class FileLogBrowser extends LogBrowser implements FileLogCompatLayer { + + private static final Logger LOGGER = Logger.getLogger(FileLogBrowser.class.getName()); + + public FileLogBrowser(Loggable loggable) { + super(loggable); + } + + @Override + public File getLogFile() throws IOException { + return getLogFileOrFail(loggable); + } + + @Override + public AnnotatedLargeText overallLog() { + final File logFile; + try { + logFile = getLogFileOrFail(getOwner()); + } catch (IOException ex) { + return new BrokenAnnotatedLargeText(ex, getOwner().getCharset()); + } + + return new AnnotatedLargeText + (logFile, getOwner().getCharset(), getOwner().isLoggingFinished(), getOwner()); + } + + @Override + public AnnotatedLargeText stepLog(@CheckForNull String stepId, boolean b) { + // Not supported, there is no default implementation for "step" + return new BrokenAnnotatedLargeText( + new UnsupportedOperationException(FileLogBrowser.class.getName() + " does not support partial logs"), + getOwner().getCharset() + ); + } + + @Override + public boolean deleteLog() throws IOException { + File logFile = getLogFileOrFail(loggable); + if (logFile.exists()) { + try { + Files.delete(logFile.toPath()); + } catch (Exception ex) { + throw new IOException("Failed to delete " + logFile, ex); + } + } else { + LOGGER.log(Level.FINE, "Trying to delete Log File of {0} which does not exist: {1}", + new Object[] {loggable, logFile}); + } + return true; + } + + @Override + public InputStream getLogInputStream() throws IOException { + File logFile = getLogFileOrFail(loggable); + + if (logFile.exists() ) { + // Checking if a ".gz" file was return + try { + InputStream fis = Files.newInputStream(logFile.toPath()); + if (logFile.getName().endsWith(".gz")) { + return new GZIPInputStream(fis); + } else { + return fis; + } + } catch (InvalidPathException e) { + throw new IOException(e); + } + } + + String message = "No such file: " + logFile; + return new ByteArrayInputStream(message.getBytes(getOwner().getCharset())); + } + + public @Nonnull + Reader getLogReader() throws IOException { + return new InputStreamReader(getLogInputStream(), getOwner().getCharset()); + } + + @Override + public List getLog(int maxLines) throws IOException { + if (maxLines == 0) { + return Collections.emptyList(); + } + int lines = 0; + long filePointer; + final List lastLines = new ArrayList<>(Math.min(maxLines, 128)); + final List bytes = new ArrayList<>(); + + try (RandomAccessFile fileHandler = new RandomAccessFile( + getLogFileOrFail(loggable), "r")) { + long fileLength = fileHandler.length() - 1; + + for (filePointer = fileLength; filePointer != -1 && maxLines != lines; filePointer--) { + fileHandler.seek(filePointer); + byte readByte = fileHandler.readByte(); + + if (readByte == 0x0A) { + if (filePointer < fileLength) { + lines = lines + 1; + lastLines.add(convertBytesToString(bytes, getOwner().getCharset())); + bytes.clear(); + } + } else if (readByte != 0xD) { + bytes.add(readByte); + } + } + } + + if (lines != maxLines) { + lastLines.add(convertBytesToString(bytes, getOwner().getCharset())); + } + + Collections.reverse(lastLines); + + // If the log has been truncated, include that information. + // Use set (replaces the first element) rather than add so that + // the list doesn't grow beyond the specified maximum number of lines. + if (lines == maxLines) { + lastLines.set(0, "[...truncated " + Functions.humanReadableByteSize(filePointer)+ "...]"); + } + + return ConsoleNote.removeNotes(lastLines); + } + + private String convertBytesToString(List bytes, Charset charset) { + Collections.reverse(bytes); + Byte[] byteArray = bytes.toArray(new Byte[bytes.size()]); + return new String(ArrayUtils.toPrimitive(byteArray), charset); + } +} diff --git a/core/src/main/java/jenkins/model/logging/impl/FileLogCompatLayer.java b/core/src/main/java/jenkins/model/logging/impl/FileLogCompatLayer.java new file mode 100644 index 000000000000..fa1455ff3407 --- /dev/null +++ b/core/src/main/java/jenkins/model/logging/impl/FileLogCompatLayer.java @@ -0,0 +1,55 @@ +/* + * The MIT License + * + * Copyright 2018 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.logging.impl; + +import hudson.AbortException; +import jenkins.model.logging.Loggable; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +import javax.annotation.Nonnull; +import java.io.File; +import java.io.IOException; + +/** + * Provides compatibility layer for logging. + * @author Oleg Nenashev + * @since TODO + * @see jenkins.model.logging.Loggable + * @see FileLogStorage + * @see FileLogBrowser + */ +@Restricted(Beta.class) +public interface FileLogCompatLayer { + + @Nonnull + default File getLogFileOrFail(Loggable loggable) throws IOException { + final File file = loggable.getLogFileCompatLocation(); + if (file == null) { + throw new AbortException("File log compatibility layer is invoked for a loggable " + + "object which returned null for getLogFileCompatLocation(): " + loggable); + } + return file; + } +} diff --git a/core/src/main/java/jenkins/model/logging/impl/FileLogStorage.java b/core/src/main/java/jenkins/model/logging/impl/FileLogStorage.java new file mode 100644 index 000000000000..f3c2b5cbdd9c --- /dev/null +++ b/core/src/main/java/jenkins/model/logging/impl/FileLogStorage.java @@ -0,0 +1,71 @@ +/* + * The MIT License + * + * Copyright 2018 CloudBees, Inc. and other Jenkins contributors + * + * 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.logging.impl; + +import hudson.console.ConsoleLogFilter; +import hudson.model.Run; +import hudson.model.TaskListener; +import jenkins.model.logging.Loggable; +import jenkins.model.logging.LoggingMethod; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +import javax.annotation.CheckForNull; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; + +/** + * Legacy File Log storage implementation. + * When used, logging always goes to a file on the naster side. + * @author Oleg Nenashev + * @since TODO + */ +@Restricted(Beta.class) +public class FileLogStorage extends StreamLoggingMethod implements FileLogCompatLayer { + + public FileLogStorage(Loggable loggable) { + super(loggable); + } + + @Override + public OutputStream createOutputStream() throws IOException { + File logFile = getLogFileOrFail(getOwner()); + return Files.newOutputStream(logFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.APPEND); + } + + @CheckForNull + @Override + public TaskListener createTaskListener() { + return null; + } + + @CheckForNull + @Override + public ConsoleLogFilter getExtraConsoleLogFilter() { + return null; + } +} diff --git a/core/src/main/java/jenkins/model/logging/impl/NoopLogBrowser.java b/core/src/main/java/jenkins/model/logging/impl/NoopLogBrowser.java new file mode 100644 index 000000000000..bef10c85a2f9 --- /dev/null +++ b/core/src/main/java/jenkins/model/logging/impl/NoopLogBrowser.java @@ -0,0 +1,88 @@ +/* + * The MIT License + * + * Copyright 2018 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.logging.impl; + +import hudson.console.AnnotatedLargeText; +import jenkins.model.logging.LogBrowser; +import jenkins.model.logging.Loggable; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.logging.Logger; + +/** + * Default Log Browser implementation which does nothing. + * @author Oleg Nenashev + * @since TODO + * @see NoopLoggingMethod + */ +@Restricted(Beta.class) +public class NoopLogBrowser extends LogBrowser { + + private static final Logger LOGGER = Logger.getLogger(NoopLogBrowser.class.getName()); + + private transient File noopLogFile; + + public NoopLogBrowser(Loggable loggable) { + super(loggable); + } + + @Override + public AnnotatedLargeText overallLog() { + return new BrokenAnnotatedLargeText( + new UnsupportedOperationException("Browsing is not supported"), + getOwner().getCharset()); + } + + @Override + public AnnotatedLargeText stepLog(@CheckForNull String stepId, boolean b) { + return overallLog(); + } + + //TODO: It may be better to have a single file for all implementations, but Charsets may be different + @Nonnull + @Override + public File getLogFile() throws IOException { + if (noopLogFile == null) { + File f = File.createTempFile("deprecated", ".log", getOwner().getTmpDir()); + f.deleteOnExit(); + try (OutputStream os = new FileOutputStream(f)) { + overallLog().writeRawLogTo(0, os); + } + return f; + } + return noopLogFile; + } + + @Override + public boolean deleteLog() { + return true; + } +} diff --git a/core/src/main/java/jenkins/model/logging/impl/NoopLoggingMethod.java b/core/src/main/java/jenkins/model/logging/impl/NoopLoggingMethod.java new file mode 100644 index 000000000000..5ca3aeeb9535 --- /dev/null +++ b/core/src/main/java/jenkins/model/logging/impl/NoopLoggingMethod.java @@ -0,0 +1,73 @@ +/* + * The MIT License + * + * Copyright 2018 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.logging.impl; + +import hudson.model.BuildListener; +import hudson.model.TaskListener; +import jenkins.model.logging.LogBrowser; +import jenkins.model.logging.Loggable; +import jenkins.model.logging.LoggingMethod; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.PrintStream; + +/** + * Default Logging Method implementation which does nothing + * @author Oleg Nenashev + * @since TODO + */ +@Restricted(Beta.class) +public class NoopLoggingMethod extends LoggingMethod { + + public NoopLoggingMethod(Loggable loggable) { + super(loggable); + } + + @CheckForNull + @Override + public TaskListener createTaskListener() { + return TaskListener.NULL; + } + + @Nonnull + @Override + public BuildListener createBuildListener() throws IOException, InterruptedException { + return new BuildListener() { + @Nonnull + @Override + public PrintStream getLogger() { + return TaskListener.NULL.getLogger(); + } + }; + } + + @Override + public LogBrowser getDefaultLogBrowser() { + return new NoopLogBrowser(getOwner()); + } +} diff --git a/core/src/main/java/jenkins/model/logging/impl/StreamLoggingMethod.java b/core/src/main/java/jenkins/model/logging/impl/StreamLoggingMethod.java new file mode 100644 index 000000000000..16d837476501 --- /dev/null +++ b/core/src/main/java/jenkins/model/logging/impl/StreamLoggingMethod.java @@ -0,0 +1,102 @@ +/* + * The MIT License + * + * Copyright 2018 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.logging.impl; + +import hudson.console.ConsoleLogFilter; +import hudson.model.AbstractBuild; +import hudson.model.BuildableItemWithBuildWrappers; +import hudson.model.Job; +import hudson.model.Run; +import hudson.model.StreamBuildListener; +import hudson.tasks.BuildWrapper; +import jenkins.model.logging.Loggable; +import jenkins.model.logging.LoggingMethod; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.OutputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Logging method which takes {@link OutputStream} as a destination. + * This implementation consults with {@link ConsoleLogFilter} extensions in Jenkins. + * @author Oleg Nenashev + * @since TODO + */ +@Restricted(Beta.class) +public abstract class StreamLoggingMethod extends LoggingMethod { + + private static final Logger LOGGER = + Logger.getLogger(StreamLoggingMethod.class.getName()); + + public StreamLoggingMethod(@Nonnull Loggable loggable) { + super(loggable); + } + + public abstract OutputStream createOutputStream() throws IOException; + + /** + * Defines an additional Console Log Filter to be used with the logging method. + * This filter may be also used for log redirection and multi-reporting in very custom cases. + * @return Log filter. {@code null} if no custom implementation + */ + public ConsoleLogFilter getExtraConsoleLogFilter() { + return null; + } + + public final StreamBuildListener createBuildListener() throws IOException, InterruptedException { + + OutputStream logger = createOutputStream(); + if (!(loggable instanceof Run)) { + throw new IOException("Loggable is not a Run instance: " + loggable.getClass()); + } + Run build = (Run)loggable; + + // Global log filter + for (ConsoleLogFilter filter : ConsoleLogFilter.all()) { + logger = filter.decorateLogger(build, logger); + } + final Job project = build.getParent(); + + // Project specific log filters + if (project instanceof BuildableItemWithBuildWrappers && build instanceof AbstractBuild) { + BuildableItemWithBuildWrappers biwbw = (BuildableItemWithBuildWrappers) project; + for (BuildWrapper bw : biwbw.getBuildWrappersList()) { + logger = bw.decorateLogger((AbstractBuild) build, logger); + } + } + + // Decorate logger by logging method of this build + final ConsoleLogFilter f = getExtraConsoleLogFilter(); + if (f != null) { + LOGGER.log(Level.INFO, "Decorated run {0} by a custom log filter {1}", + new Object[]{this, f}); + logger = f.decorateLogger(build, logger); + } + return new StreamBuildListener(logger, getOwner().getCharset()); + } +}