diff --git a/pom.xml b/pom.xml index 1f76f7c..e870c9d 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,9 @@ 1.0-alpha-1 -SNAPSHOT - 2.135-rc15088.42aa6febbbed + + 2.137-20180806.095555-1 + 2.137-20180806.095623-1 8 true diff --git a/src/main/java/io/jenkins/plugins/extlogging/api/Event.java b/src/main/java/io/jenkins/plugins/extlogging/api/Event.java index c6e8aac..9ba1c5d 100644 --- a/src/main/java/io/jenkins/plugins/extlogging/api/Event.java +++ b/src/main/java/io/jenkins/plugins/extlogging/api/Event.java @@ -15,6 +15,7 @@ public class Event { final long timestamp; final long id; + //TODO:Consider so that Key-Value storages could be quickly implemented Map data = new HashMap<>(); public Event(long id, String message, long timestamp) { diff --git a/src/main/java/io/jenkins/plugins/extlogging/api/ExternalLogBrowser.java b/src/main/java/io/jenkins/plugins/extlogging/api/ExternalLogBrowser.java index c09e3dc..bece115 100644 --- a/src/main/java/io/jenkins/plugins/extlogging/api/ExternalLogBrowser.java +++ b/src/main/java/io/jenkins/plugins/extlogging/api/ExternalLogBrowser.java @@ -1,18 +1,121 @@ package io.jenkins.plugins.extlogging.api; -import jenkins.model.logging.LogBrowser; +import hudson.console.AnnotatedLargeText; +import hudson.console.ConsoleNote; +import hudson.model.Run; import jenkins.model.logging.Loggable; +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; /** * Base abstract class for External Log Browsers. + * This implementation also implements a convenience method for caching of files. * @author Oleg Nenashev * @since TODO */ -public abstract class ExternalLogBrowser extends LogBrowser { +public abstract class ExternalLogBrowser extends LoggableHandler { public ExternalLogBrowser(@Nonnull Loggable loggable) { super(loggable); } + + //TODO: Push warnings to Telemetry API + //Pipeline: LOGGER.log(Level.WARNING, "Avoid calling getLogFile on " + this, + // new UnsupportedOperationException()); + /** + * Gets log as a file. + * This is a compatibility method, which is used in {@link Run#getLogFile()}. + * {@link ExternalLogBrowser} 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 + */ + public File getLogFile() throws IOException { + File f = File.createTempFile("deprecated", ".log", getOwner().getTmpDir()); + f.deleteOnExit(); + try (OutputStream os = new FileOutputStream(f)) { + overallLog().writeRawLogTo(0, os); + } + return f; + } + + /** + * 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(loggable.getCharset().name()); + } + + 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); + } + + /** + * 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/src/main/java/io/jenkins/plugins/extlogging/api/ExternalLogBrowserFactory.java b/src/main/java/io/jenkins/plugins/extlogging/api/ExternalLogBrowserFactory.java index 6ded090..f93fb06 100644 --- a/src/main/java/io/jenkins/plugins/extlogging/api/ExternalLogBrowserFactory.java +++ b/src/main/java/io/jenkins/plugins/extlogging/api/ExternalLogBrowserFactory.java @@ -4,12 +4,12 @@ import hudson.model.Describable; import hudson.model.Descriptor; import jenkins.model.Jenkins; -import jenkins.model.logging.LogBrowser; import jenkins.model.logging.Loggable; import javax.annotation.CheckForNull; /** + * Produces {@link ExternalLogBrowser}s. * @author Oleg Nenashev * @since TODO */ @@ -17,7 +17,7 @@ public abstract class ExternalLogBrowserFactory implements Describable, ExtensionPoint { @CheckForNull - public abstract LogBrowser create(Loggable loggable); + public abstract ExternalLogBrowser create(Loggable loggable); @Override public Descriptor getDescriptor() { diff --git a/src/main/java/io/jenkins/plugins/extlogging/api/ExternalLoggingEventWriter.java b/src/main/java/io/jenkins/plugins/extlogging/api/ExternalLoggingEventWriter.java index 307baa3..3f2d815 100644 --- a/src/main/java/io/jenkins/plugins/extlogging/api/ExternalLoggingEventWriter.java +++ b/src/main/java/io/jenkins/plugins/extlogging/api/ExternalLoggingEventWriter.java @@ -1,12 +1,21 @@ package io.jenkins.plugins.extlogging.api; +import io.jenkins.plugins.extlogging.api.impl.ExternalLoggingOutputStream; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import java.io.IOException; +import java.io.PrintStream; import java.io.Serializable; +import java.io.UnsupportedEncodingException; import java.io.Writer; +import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; +//TODO: jglick is concerned about the event-based logic, maybe we should add a lower-level implementation +//TODO: jglick: "Why do we need events?" in JEP /** * Implements logging of events * @author Oleg Nenashev @@ -14,9 +23,17 @@ */ public abstract class ExternalLoggingEventWriter extends Writer implements Serializable { + String charset; Map metadata = new HashMap<>(); AtomicLong messageCounter = new AtomicLong(); + @CheckForNull + private transient PrintStream printStream; + + public ExternalLoggingEventWriter(@Nonnull Charset charset) { + this.charset = charset.name(); + } + public abstract void writeEvent(Event event) throws IOException; public void writeMessage(String message) throws IOException { @@ -25,6 +42,7 @@ public void writeMessage(String message) throws IOException { writeEvent(event); } + //TODO(oleg-nenashev): jglick requests example of complex event public void addMetadataEntry(String key, Serializable value) { metadata.put(key, value); } @@ -37,11 +55,24 @@ public void write(char[] cbuf, int off, int len) throws IOException { @Override public void close() throws IOException { - // noop + // noop by default } @Override public void flush() throws IOException { - // noop + if (printStream != null) { + printStream.flush(); + } + } + + public Charset getCharset() { + return Charset.forName(charset); + } + + public PrintStream getLogger() throws UnsupportedEncodingException { + if (printStream == null) { + printStream = new PrintStream(new ExternalLoggingOutputStream(this), true, charset); + } + return printStream; } } diff --git a/src/main/java/io/jenkins/plugins/extlogging/api/ExternalLoggingMethod.java b/src/main/java/io/jenkins/plugins/extlogging/api/ExternalLoggingMethod.java index be82b04..d4bbff2 100644 --- a/src/main/java/io/jenkins/plugins/extlogging/api/ExternalLoggingMethod.java +++ b/src/main/java/io/jenkins/plugins/extlogging/api/ExternalLoggingMethod.java @@ -18,7 +18,6 @@ import io.jenkins.plugins.extlogging.api.util.UniqueIdHelper; import jenkins.model.Jenkins; import jenkins.model.logging.Loggable; -import jenkins.model.logging.LoggingMethod; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; @@ -28,21 +27,29 @@ * @author Oleg Nenashev * @since TODO */ -public abstract class ExternalLoggingMethod extends LoggingMethod { +public abstract class ExternalLoggingMethod extends LoggableHandler { public ExternalLoggingMethod(@Nonnull Loggable loggable) { super(loggable); } + /** + * 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 ExternalLogBrowser getDefaultLogBrowser() { + return null; + } + //TODO: implement @CheckForNull - @Override public TaskListener createTaskListener() { return null; } // TODO: Implement event-based logic instead of the - @Override public BuildListener createBuildListener() throws IOException, InterruptedException { return new ExternalLoggingBuildListener(createWriter()); } @@ -98,7 +105,6 @@ public final ExternalLoggingEventWriter createWriter() throws IOException, Inter protected abstract ExternalLoggingEventWriter _createWriter() throws IOException, InterruptedException; @Nonnull - @Override public Launcher decorateLauncher(@Nonnull Launcher original, @Nonnull Run run, @Nonnull Node node) { if (node instanceof Jenkins) { diff --git a/src/main/java/io/jenkins/plugins/extlogging/api/LoggableHandler.java b/src/main/java/io/jenkins/plugins/extlogging/api/LoggableHandler.java new file mode 100644 index 0000000..3e27e90 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/extlogging/api/LoggableHandler.java @@ -0,0 +1,79 @@ +/* + * 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 io.jenkins.plugins.extlogging.api; + +import jenkins.model.logging.Loggable; +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; + +/** + * Object which operates with {@link Loggable} items. + * @param Loggable type + * @author Oleg Nenashev + * @since TODO + */ +@ExportedBean +@Restricted(Beta.class) +public abstract class LoggableHandler { + + protected transient TLoggable loggable; + + public LoggableHandler(@Nonnull TLoggable 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 TLoggable loggable) { + this.loggable = loggable; + } + + public static void onLoad(@Nonnull T loggable, @CheckForNull LoggableHandler logHandler) { + if (logHandler != null) { + logHandler.onLoad(loggable); + } + } + + @Nonnull + protected TLoggable getOwner() { + if (loggable == null) { + throw new IllegalStateException("Owner has not been assigned to the object yet"); + } + return loggable; + + } +} diff --git a/src/main/java/io/jenkins/plugins/extlogging/api/impl/DisabledExternalLogBrowserFactory.java b/src/main/java/io/jenkins/plugins/extlogging/api/impl/DisabledExternalLogBrowserFactory.java index 626d679..042ee34 100644 --- a/src/main/java/io/jenkins/plugins/extlogging/api/impl/DisabledExternalLogBrowserFactory.java +++ b/src/main/java/io/jenkins/plugins/extlogging/api/impl/DisabledExternalLogBrowserFactory.java @@ -1,9 +1,9 @@ package io.jenkins.plugins.extlogging.api.impl; import hudson.Extension; +import io.jenkins.plugins.extlogging.api.ExternalLogBrowser; import io.jenkins.plugins.extlogging.api.ExternalLogBrowserFactory; import io.jenkins.plugins.extlogging.api.ExternalLogBrowserFactoryDescriptor; -import jenkins.model.logging.LogBrowser; import jenkins.model.logging.Loggable; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; @@ -21,7 +21,7 @@ public DisabledExternalLogBrowserFactory() { } @Override - public LogBrowser create(Loggable loggable) { + public ExternalLogBrowser create(Loggable loggable) { return null; } diff --git a/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLogStorage.java b/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLogStorage.java new file mode 100644 index 0000000..b39ba44 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLogStorage.java @@ -0,0 +1,108 @@ +package io.jenkins.plugins.extlogging.api.impl; + +import hudson.console.AnnotatedLargeText; +import hudson.model.BuildListener; +import hudson.model.TaskListener; +import io.jenkins.plugins.extlogging.api.ExternalLogBrowser; +import io.jenkins.plugins.extlogging.api.ExternalLoggingMethod; +import jenkins.model.logging.LogStorage; +import jenkins.model.logging.Loggable; +import org.kohsuke.stapler.export.Exported; +import org.kohsuke.stapler.export.ExportedBean; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +/** + * External Log Storage implementation. + * @author Oleg Nenashev + * @since TODO + */ +@ExportedBean +public class ExternalLogStorage extends LogStorage { + + @Nonnull + private final ExternalLoggingMethod reporter; + @Nonnull + private final ExternalLogBrowser browser; + + public ExternalLogStorage(TLoggable loggable, + @Nonnull ExternalLoggingMethod reporter, + @Nonnull ExternalLogBrowser browser) { + super(loggable); + this.reporter = reporter; + this.browser = browser; + + } + + @Override + public void onLoad(@Nonnull TLoggable loggable) { + super.onLoad(loggable); + reporter.onLoad(loggable); + browser.onLoad(loggable); + } + + @CheckForNull + @Override + public TaskListener createTaskListener() throws IOException, InterruptedException { + return getReporter().createTaskListener(); + } + + @Nonnull + @Override + public BuildListener createBuildListener() throws IOException, InterruptedException { + return getReporter().createBuildListener(); + } + + @Nonnull + @Override + public AnnotatedLargeText overallLog() { + return getBrowser().overallLog(); + } + + @Nonnull + @Override + public AnnotatedLargeText stepLog(@CheckForNull String s, boolean completed) { + return getBrowser().stepLog(s, completed); + } + + @Override + public InputStream getLogInputStream() throws IOException { + return getBrowser().getLogInputStream(); + } + + @Override + public List getLog(int i) throws IOException { + return getBrowser().getLog(i); + } + + @Nonnull + @Override + public File getLogFile() throws IOException { + return getBrowser().getLogFile(); + } + + @Override + public boolean deleteLog() throws IOException { + return getBrowser().deleteLog(); + } + + @Nonnull + @Exported + public ExternalLogBrowser getBrowser() { + return browser; + } + + @Nonnull + @Exported + public ExternalLoggingMethod getReporter() { + return reporter; + } + + + +} diff --git a/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLogStorageFactory.java b/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLogStorageFactory.java new file mode 100644 index 0000000..4c4052c --- /dev/null +++ b/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLogStorageFactory.java @@ -0,0 +1,69 @@ +package io.jenkins.plugins.extlogging.api.impl; + +import hudson.Extension; +import io.jenkins.plugins.extlogging.api.ExternalLogBrowser; +import io.jenkins.plugins.extlogging.api.ExternalLogBrowserFactory; +import io.jenkins.plugins.extlogging.api.ExternalLoggingMethod; +import io.jenkins.plugins.extlogging.api.ExternalLoggingMethodFactory; +import jenkins.model.logging.LogStorage; +import jenkins.model.logging.LogStorageFactory; +import jenkins.model.logging.Loggable; + +import javax.annotation.CheckForNull; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Locator which provides logging methods and browsers from {@link ExternalLoggingGlobalConfiguration}. + * @author Oleg Nenashev + * @see ExternalLoggingGlobalConfiguration + */ +@Extension +public class ExternalLogStorageFactory extends LogStorageFactory { + + private static final Logger LOGGER = + Logger.getLogger(ExternalLogStorageFactory.class.getName()); + + @CheckForNull + @Override + protected LogStorage getLogStorage(Loggable loggable) { + ExternalLoggingMethod method = getLoggingMethod(loggable); + ExternalLogBrowser browser = getLogBrowser(loggable); + if (method == null || browser == null) { + LOGGER.log(Level.FINE, "Cannot find external log reporter or browser for {0}. " + + "Reporter: {1}, browser: {2}", + new Object[] {loggable, method, browser}); + return null; + } + + return new ExternalLogStorage(loggable, method, browser); + } + + @CheckForNull + protected ExternalLoggingMethod getLoggingMethod(Loggable loggable) { + ExternalLoggingMethodFactory factory = ExternalLoggingGlobalConfiguration.getInstance().getLoggingMethod(); + if (factory != null) { + return factory.create(loggable); + } + return null; + } + + @CheckForNull + protected ExternalLogBrowser getLogBrowser(Loggable loggable) { + ExternalLogBrowser browser = null; + ExternalLogBrowserFactory factory = ExternalLoggingGlobalConfiguration.getInstance().getLogBrowser(); + if (factory != null) { + browser = factory.create(loggable); + } + + // If factories cannot provide the implementation, try the default + if (browser == null) { + ExternalLoggingMethod method = getLoggingMethod(loggable); + if (method != null) { + browser = method.getDefaultLogBrowser(); + } + } + + return browser; + } +} diff --git a/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLoggingBuildListener.java b/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLoggingBuildListener.java index bf024e5..a4af101 100644 --- a/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLoggingBuildListener.java +++ b/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLoggingBuildListener.java @@ -4,6 +4,7 @@ import io.jenkins.plugins.extlogging.api.ExternalLoggingEventWriter; import java.io.PrintStream; +import java.io.UnsupportedEncodingException; /** * @author Oleg Nenashev @@ -11,14 +12,19 @@ */ public class ExternalLoggingBuildListener implements BuildListener { - private final ExternalLoggingEventWriter writer; + protected final ExternalLoggingEventWriter writer; public ExternalLoggingBuildListener(ExternalLoggingEventWriter writer) { this.writer = writer; } + //TODO: do something better about it @Override public PrintStream getLogger() { - return new PrintStream(new ExternalLoggingOutputStream(writer)); + try { + return writer.getLogger(); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } } } diff --git a/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLoggingGlobalConfiguration.java b/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLoggingGlobalConfiguration.java index 411354f..415aa93 100644 --- a/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLoggingGlobalConfiguration.java +++ b/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLoggingGlobalConfiguration.java @@ -1,5 +1,6 @@ package io.jenkins.plugins.extlogging.api.impl; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; import io.jenkins.plugins.extlogging.api.ExternalLogBrowserFactory; import io.jenkins.plugins.extlogging.api.ExternalLoggingMethodFactory; @@ -28,6 +29,8 @@ public class ExternalLoggingGlobalConfiguration extends GlobalConfiguration { private ExternalLogBrowserFactory logBrowser; @Nonnull + @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", + justification = "Findbugs does not handle asserts right") public static ExternalLoggingGlobalConfiguration getInstance() { ExternalLoggingGlobalConfiguration cfg = GlobalConfiguration.all().get(ExternalLoggingGlobalConfiguration.class); assert cfg != null : "Global configuration should be present"; diff --git a/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLoggingLauncher.java b/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLoggingLauncher.java index 7995edb..126e1a5 100644 --- a/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLoggingLauncher.java +++ b/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLoggingLauncher.java @@ -23,14 +23,15 @@ */ package io.jenkins.plugins.extlogging.api.impl; +import hudson.FilePath; import hudson.Launcher; import hudson.Proc; import hudson.model.TaskListener; import hudson.remoting.Channel; import hudson.remoting.RemoteInputStream; +import hudson.remoting.VirtualChannel; import io.jenkins.plugins.extlogging.api.ExternalLoggingMethod; import io.jenkins.plugins.extlogging.api.OutputStreamWrapper; -import jenkins.model.logging.LoggingMethod; import jenkins.security.MasterToSlaveCallable; import org.apache.commons.io.input.NullInputStream; import org.kohsuke.accmod.Restricted; @@ -43,6 +44,7 @@ import java.util.ArrayList; import java.util.List; +//TODO: Likely YAGNI in the current implementation /** * Provides {@link Launcher} implementations for External Logging. * These implementations plug-in {@link ExternalLoggingMethod} into the on-agent executions. @@ -61,7 +63,7 @@ public DefaultLocalLauncher(Launcher inner) { } /** - * Default remote launcher which redirects all the output and error to the stream {@link LoggingMethod} provides. + * Default remote launcher which redirects all the output and error to the stream {@link ExternalLoggingMethod} provides. */ public static class DefaultRemoteLauncher extends Launcher.DecoratedLauncher { private static final NullInputStream NULL_INPUT_STREAM = new NullInputStream(0); @@ -85,16 +87,22 @@ public Proc launch(Launcher.ProcStarter ps) throws IOException { // RemoteLogstashReporterStream(new CloseProofOutputStream(ps.stdout() final OutputStream out = ps.stdout() == null ? null : (streamOut == null ? ps.stdout() : streamOut.toSerializableOutputStream()); final OutputStream err = ps.stdout() == null ? null : (streamErr == null ? ps.stdout() : streamErr.toSerializableOutputStream()); - final InputStream in = (ps.stdin() == null || ps.stdin() == NULL_INPUT_STREAM) ? null : new RemoteInputStream(ps.stdin(), false); - final String workDir = ps.pwd() == null ? null : ps.pwd().getRemote(); + final InputStream in = (ps.stdin() == null || ps.stdin() == NULL_INPUT_STREAM) + ? null : new RemoteInputStream(ps.stdin(), false); + + final FilePath pwd = ps.pwd(); + final String workDir = pwd == null ? null : pwd.getRemote(); // TODO: we do not reverse streams => the parameters try { final RemoteLaunchCallable callable = new RemoteLaunchCallable( ps.cmds(), ps.masks(), ps.envs(), in, out, err, ps.quiet(), workDir, listener); - - return new Launcher.RemoteLauncher.ProcImpl(getChannel().call(callable)); + final VirtualChannel channel = getChannel(); + if (channel == null) { + throw new IOException("Channel is null"); + } + return new Launcher.RemoteLauncher.ProcImpl(channel.call(callable)); } catch (InterruptedException e) { throw (IOException) new InterruptedIOException().initCause(e); } @@ -134,14 +142,14 @@ public Launcher.RemoteProcess call() throws IOException { final Proc p = ps.start(); - return Channel.current().export(Launcher.RemoteProcess.class, new Launcher.RemoteProcess() { + return Channel.currentOrFail().export(Launcher.RemoteProcess.class, new Launcher.RemoteProcess() { public int join() throws InterruptedException, IOException { try { return p.join(); } finally { // make sure I/O is delivered to the remote before we return try { - Channel.current().syncIO(); + Channel.currentOrFail().syncIO(); } catch (Throwable th) { // this includes a failure to sync, slave.jar too old, etc } diff --git a/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLoggingMethodLocator.java b/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLoggingMethodLocator.java deleted file mode 100644 index f8f6bce..0000000 --- a/src/main/java/io/jenkins/plugins/extlogging/api/impl/ExternalLoggingMethodLocator.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.jenkins.plugins.extlogging.api.impl; - -import hudson.Extension; -import io.jenkins.plugins.extlogging.api.ExternalLogBrowserFactory; -import io.jenkins.plugins.extlogging.api.ExternalLoggingMethodFactory; -import jenkins.model.logging.LogBrowser; -import jenkins.model.logging.Loggable; -import jenkins.model.logging.LoggingMethod; -import jenkins.model.logging.LoggingMethodLocator; - -import javax.annotation.CheckForNull; - -/** - * Locator which provides logging nethods and browsers from {@link ExternalLoggingGlobalConfiguration}. - * @author Oleg Nenashev - * @see ExternalLoggingGlobalConfiguration - */ -@Extension -public class ExternalLoggingMethodLocator extends LoggingMethodLocator { - - @CheckForNull - @Override - protected LoggingMethod getLoggingMethod(Loggable loggable) { - ExternalLoggingMethodFactory factory = ExternalLoggingGlobalConfiguration.getInstance().getLoggingMethod(); - if (factory != null) { - return factory.create(loggable); - } - return null; - } - - @CheckForNull - @Override - protected LogBrowser getLogBrowser(Loggable loggable) { - ExternalLogBrowserFactory factory = ExternalLoggingGlobalConfiguration.getInstance().getLogBrowser(); - if (factory != null) { - return factory.create(loggable); - } - return null; - } -} diff --git a/src/main/java/io/jenkins/plugins/extlogging/api/integrations/pipeline/ExternalPipelineLogStorage.java b/src/main/java/io/jenkins/plugins/extlogging/api/integrations/pipeline/ExternalPipelineLogStorage.java index f4875bc..5edd8d8 100644 --- a/src/main/java/io/jenkins/plugins/extlogging/api/integrations/pipeline/ExternalPipelineLogStorage.java +++ b/src/main/java/io/jenkins/plugins/extlogging/api/integrations/pipeline/ExternalPipelineLogStorage.java @@ -28,11 +28,10 @@ import hudson.console.AnnotatedLargeText; import hudson.model.BuildListener; import hudson.model.TaskListener; -import io.jenkins.plugins.extlogging.api.ExternalLoggingEventWriter; +import io.jenkins.plugins.extlogging.api.ExternalLogBrowser; import io.jenkins.plugins.extlogging.api.ExternalLoggingMethod; -import io.jenkins.plugins.extlogging.api.impl.ExternalLoggingOutputStream; +import io.jenkins.plugins.extlogging.api.impl.ExternalLoggingBuildListener; import io.jenkins.plugins.extlogging.api.SensitiveStringsProvider; -import jenkins.model.logging.LogBrowser; import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; import org.jenkinsci.plugins.workflow.graph.FlowNode; import org.jenkinsci.plugins.workflow.job.WorkflowRun; @@ -40,7 +39,6 @@ import javax.annotation.Nonnull; import java.io.IOException; -import java.io.PrintStream; import java.util.Collection; import java.util.logging.Logger; @@ -54,10 +52,11 @@ public class ExternalPipelineLogStorage implements LogStorage { Logger.getLogger(ExternalPipelineLogStorage.class.getName()); private final ExternalLoggingMethod lm; - private final LogBrowser logBrowser; + private final ExternalLogBrowser logBrowser; private final WorkflowRun run; - ExternalPipelineLogStorage(@Nonnull WorkflowRun run, @Nonnull ExternalLoggingMethod lm, @Nonnull LogBrowser browser) { + ExternalPipelineLogStorage(@Nonnull WorkflowRun run, @Nonnull ExternalLoggingMethod lm, + @Nonnull ExternalLogBrowser browser) { this.run = run; this.lm = lm; this.logBrowser = browser; @@ -88,16 +87,15 @@ public AnnotatedLargeText stepLog(@Nonnull FlowNode flowNode, boolean return logBrowser.stepLog(flowNode.getId(), completed); } - private static class PipelineListener implements BuildListener { + private static class PipelineListener extends ExternalLoggingBuildListener { private static final long serialVersionUID = 1; + //TODO: unused, remove? private final Collection sensitiveStrings; - private final ExternalLoggingEventWriter writer; - private transient PrintStream logger; PipelineListener(WorkflowRun run, ExternalLoggingMethod method) throws IOException, InterruptedException { - this.writer = method.createWriter(); + super(method.createWriter()); this.sensitiveStrings = SensitiveStringsProvider.getAllSensitiveStrings(run); } @@ -106,14 +104,6 @@ private static class PipelineListener implements BuildListener { writer.addMetadataEntry("stepId", node.getId()); } - @Override - public PrintStream getLogger() { - if (logger == null) { - logger = new PrintStream(ExternalLoggingOutputStream.createOutputStream(writer, sensitiveStrings)); - } - return logger; - } - } } diff --git a/src/main/java/io/jenkins/plugins/extlogging/api/integrations/pipeline/ExternalPipelineLogStorageFactory.java b/src/main/java/io/jenkins/plugins/extlogging/api/integrations/pipeline/ExternalPipelineLogStorageFactory.java index 5005715..b9c0e0b 100644 --- a/src/main/java/io/jenkins/plugins/extlogging/api/integrations/pipeline/ExternalPipelineLogStorageFactory.java +++ b/src/main/java/io/jenkins/plugins/extlogging/api/integrations/pipeline/ExternalPipelineLogStorageFactory.java @@ -3,11 +3,8 @@ import hudson.Extension; import hudson.model.Queue; import hudson.model.Run; -import io.jenkins.plugins.extlogging.api.ExternalLoggingMethod; +import io.jenkins.plugins.extlogging.api.impl.ExternalLogStorage; import io.jenkins.plugins.extlogging.api.integrations.MaskPasswordsSensitiveStringsProvider.MaskSensitiveStringsProvider; -import jenkins.model.logging.LogBrowser; -import jenkins.model.logging.LoggingMethod; -import jenkins.model.logging.LoggingMethodLocator; import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.jenkinsci.plugins.workflow.log.LogStorage; @@ -53,13 +50,12 @@ public LogStorage forBuild(@Nonnull FlowExecutionOwner flowExecutionOwner) { return null; } - final LoggingMethod loggingMethod = LoggingMethodLocator.locate(run); - final LogBrowser browser = LoggingMethodLocator.locateBrowser(run); - - if (loggingMethod instanceof ExternalLoggingMethod) { + jenkins.model.logging.LogStorage logStorage = run.getLogStorage(); + if (logStorage instanceof ExternalLogStorage) { + ExternalLogStorage extLogStorage = (ExternalLogStorage)logStorage; return new ExternalPipelineLogStorage(run, - (ExternalLoggingMethod) loggingMethod, - browser); + extLogStorage.getReporter(), + extLogStorage.getBrowser()); } return null; diff --git a/src/test/java/io/jenkins/plugins/extlogging/api/FreestyleJobTest.java b/src/test/java/io/jenkins/plugins/extlogging/api/FreestyleJobTest.java index dcf2fea..e62baa8 100644 --- a/src/test/java/io/jenkins/plugins/extlogging/api/FreestyleJobTest.java +++ b/src/test/java/io/jenkins/plugins/extlogging/api/FreestyleJobTest.java @@ -3,6 +3,7 @@ import hudson.model.FreeStyleBuild; import hudson.model.FreeStyleProject; import hudson.tasks.Shell; +import io.jenkins.plugins.extlogging.api.impl.ExternalLogStorage; import io.jenkins.plugins.extlogging.api.impl.ExternalLoggingGlobalConfiguration; import io.jenkins.plugins.extlogging.api.util.MockExternalLoggingEventWriter; import io.jenkins.plugins.extlogging.api.util.MockLogBrowserFactory; @@ -19,6 +20,9 @@ import java.io.File; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertThat; + /** * @author Oleg Nenashev * @since TODO @@ -47,7 +51,9 @@ public void spotcheck_Mock() throws Exception { project.getBuildersList().add(new Shell("echo hello")); FreeStyleBuild build = j.buildAndAssertSuccess(project); - MockLoggingMethod lm = (MockLoggingMethod)build.getLoggingMethod(); + assertThat(build.getLogStorage(), instanceOf(ExternalLogStorage.class)); + ExternalLogStorage storage = (ExternalLogStorage) build.getLogStorage(); + MockLoggingMethod lm = (MockLoggingMethod)storage.getReporter(); MockExternalLoggingEventWriter writer = lm.getWriter(); Assert.assertTrue(writer.isEventWritten()); j.assertLogContains("hello", build); diff --git a/src/test/java/io/jenkins/plugins/extlogging/api/PipelineSmokeTest.java b/src/test/java/io/jenkins/plugins/extlogging/api/PipelineSmokeTest.java index 9fe0898..c808fa5 100644 --- a/src/test/java/io/jenkins/plugins/extlogging/api/PipelineSmokeTest.java +++ b/src/test/java/io/jenkins/plugins/extlogging/api/PipelineSmokeTest.java @@ -1,6 +1,7 @@ package io.jenkins.plugins.extlogging.api; import hudson.model.Run; +import io.jenkins.plugins.extlogging.api.impl.ExternalLogStorage; import io.jenkins.plugins.extlogging.api.impl.ExternalLoggingGlobalConfiguration; import io.jenkins.plugins.extlogging.api.util.MockExternalLoggingEventWriter; import io.jenkins.plugins.extlogging.api.util.MockLogBrowser; @@ -50,8 +51,10 @@ public void spotcheck_Mock() throws Exception { project.setDefinition(new CpsFlowDefinition("echo 'Hello'", true)); Run build = j.buildAndAssertSuccess(project); - assertThat(build.getLoggingMethod(), instanceOf(MockLoggingMethod.class)); - MockLoggingMethod loggingMethod = (MockLoggingMethod)build.getLoggingMethod(); + assertThat(build.getLogStorage(), instanceOf(ExternalLogStorage.class)); + ExternalLogStorage storage = (ExternalLogStorage) build.getLogStorage(); + assertThat(storage.getReporter(), instanceOf(MockLoggingMethod.class)); + MockLoggingMethod loggingMethod = (MockLoggingMethod)storage.getReporter(); MockExternalLoggingEventWriter writer = loggingMethod.getWriter(); // Do not try to add it. Pipeline creates separate PipelineLogListeners for each call // Assert.assertTrue(writer.isEventWritten()); diff --git a/src/test/java/io/jenkins/plugins/extlogging/api/util/MockExternalLoggingEventWriter.java b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockExternalLoggingEventWriter.java index c4233f7..dc0a15a 100644 --- a/src/test/java/io/jenkins/plugins/extlogging/api/util/MockExternalLoggingEventWriter.java +++ b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockExternalLoggingEventWriter.java @@ -6,6 +6,7 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.nio.charset.Charset; import java.util.logging.Logger; /** @@ -23,7 +24,8 @@ public class MockExternalLoggingEventWriter extends ExternalLoggingEventWriter { // Debug flags private boolean eventWritten; - public MockExternalLoggingEventWriter(File dest) { + public MockExternalLoggingEventWriter(File dest, Charset charset) { + super(charset); this.dest = dest; } diff --git a/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLogBrowser.java b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLogBrowser.java index 789c972..670e8b7 100644 --- a/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLogBrowser.java +++ b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLogBrowser.java @@ -1,12 +1,16 @@ package io.jenkins.plugins.extlogging.api.util; +import hudson.console.AnnotatedLargeText; import hudson.model.Run; +import io.jenkins.plugins.extlogging.api.ExternalLogBrowser; import jenkins.model.logging.Loggable; -import jenkins.model.logging.impl.FileLogBrowser; +import jenkins.model.logging.impl.BrokenAnnotatedLargeText; +import javax.annotation.CheckForNull; import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.util.logging.Level; import java.util.logging.Logger; @@ -14,7 +18,7 @@ * @author Oleg Nenashev * @since TODO */ -public class MockLogBrowser extends FileLogBrowser { +public class MockLogBrowser extends ExternalLogBrowser { private static final Logger LOGGER = Logger.getLogger(MockLogBrowser.class.getName()); @@ -31,8 +35,45 @@ public MockLogBrowser(Run run, File baseDir) { return (Run)super.getOwner(); } - @Override public File getLogFileOrFail(Loggable loggable) throws IOException { return new File(baseDir, getOwner().getFullDisplayName() + ".txt"); } + + @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(MockLogBrowser.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; + } } diff --git a/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLogBrowserFactory.java b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLogBrowserFactory.java index bb939ff..8ae676e 100644 --- a/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLogBrowserFactory.java +++ b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLogBrowserFactory.java @@ -1,13 +1,14 @@ package io.jenkins.plugins.extlogging.api.util; import hudson.model.Run; +import io.jenkins.plugins.extlogging.api.ExternalLogBrowser; import io.jenkins.plugins.extlogging.api.ExternalLogBrowserFactory; -import jenkins.model.logging.LogBrowser; import jenkins.model.logging.Loggable; import java.io.File; /** + * Producer for {@link MockLogBrowser}. * @author Oleg Nenashev * @since TODO */ @@ -20,7 +21,7 @@ public MockLogBrowserFactory(File baseDir) { } @Override - public LogBrowser create(Loggable loggable) { + public ExternalLogBrowser create(Loggable loggable) { return new MockLogBrowser((Run)loggable, baseDir); } } diff --git a/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLoggingMethod.java b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLoggingMethod.java index 01e8dc2..c872c5b 100644 --- a/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLoggingMethod.java +++ b/src/test/java/io/jenkins/plugins/extlogging/api/util/MockLoggingMethod.java @@ -1,9 +1,9 @@ package io.jenkins.plugins.extlogging.api.util; import hudson.model.Run; +import io.jenkins.plugins.extlogging.api.ExternalLogBrowser; import io.jenkins.plugins.extlogging.api.ExternalLoggingEventWriter; import io.jenkins.plugins.extlogging.api.ExternalLoggingMethod; -import jenkins.model.logging.LogBrowser; import javax.annotation.CheckForNull; import java.io.File; @@ -30,7 +30,9 @@ public MockLoggingMethod(Run run, File baseDir) { @Override public ExternalLoggingEventWriter _createWriter() { - writer = new MockExternalLoggingEventWriter(new File(baseDir, getOwner().getFullDisplayName() + ".txt")); + writer = new MockExternalLoggingEventWriter(new File(baseDir, + getOwner().getFullDisplayName() + ".txt"), + loggable.getCharset()); return writer; } @@ -40,7 +42,7 @@ public MockExternalLoggingEventWriter getWriter() { } @Override - public LogBrowser getDefaultLogBrowser() { + public ExternalLogBrowser getDefaultLogBrowser() { return new MockLogBrowser(getOwner(), baseDir); } }