/*******************************************************************************
*
* Copyright (c) 2004-2010, Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
*
*
*
*******************************************************************************/
package hudson.console;
import hudson.Functions;
import hudson.MarkupText;
import hudson.model.Describable;
import hudson.model.Hudson;
import hudson.model.Run;
import hudson.remoting.ObjectInputStreamEx;
import hudson.util.IOException2;
import hudson.util.IOUtils;
import hudson.util.UnbufferedBase64InputStream;
import org.apache.commons.codec.binary.Base64OutputStream;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.tools.ant.BuildListener;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
/**
* Data that hangs off from a console output.
*
* <p> A {@link ConsoleNote} can be put into a console output while it's being
* written, and it represents a machine readable information about a particular
* position of the console output.
*
* <p> When Hudson is reading back a console output for display, a
* {@link ConsoleNote} is used to trigger {@link ConsoleAnnotator}, which in
* turn uses the information in the note to generate markup. In this way, we can
* overlay richer information on top of the console output.
*
* <h2>Comparison with {@link ConsoleAnnotatorFactory}</h2> <p> Compared to
* {@link ConsoleAnnotatorFactory}, the main advantage of {@link ConsoleNote} is
* that it can be emitted into the output by the producer of the output (or by a
* filter), which can have a much better knowledge about the context of what's
* being executed.
*
* <ol> <li> For example, when your plugin is about to report an error message,
* you can emit a {@link ConsoleNote} that indicates an error, instead of
* printing an error message as plain text. The
* {@link #annotate(Object, MarkupText, int)} method will then generate the
* proper error message, with all the HTML markup that makes error message more
* user friendly.
*
* <li> Or consider annotating output from Ant. A modified {@link BuildListener}
* can place a {@link ConsoleNote} every time a new target execution starts.
* These notes can be then later used to build the outline that shows what
* targets are executed, hyperlinked to their corresponding locations in the
* build output. </ol>
*
* <p> Doing these things by {@link ConsoleAnnotatorFactory} would be a lot
* harder, as they can only rely on the pattern matching of the output.
*
* <h2>Persistence</h2> <p> {@link ConsoleNote}s are serialized and gzip
* compressed into a byte sequence and then embedded into the console output
* text file, with a bit of preamble/postamble to allow tools to ignore them. In
* this way {@link ConsoleNote} always sticks to a particular point in the
* console output.
*
* <p> This design allows descendant processes of Hudson to emit
* {@link ConsoleNote}s. For example, Ant forked by a shell forked by Hudson can
* put an encoded note in its stdout, and Hudson will correctly understands
* that. The preamble and postamble includes a certain ANSI escape sequence
* designed in such a way to minimize garbage if this output is observed by a
* human being directly.
*
* <p> Because of this persistence mechanism, {@link ConsoleNote}s need to be
* serializable, and care should be taken to reduce footprint of the notes, if
* you are putting a lot of notes. Serialization format compatibility is also
* important, although {@link ConsoleNote}s that failed to deserialize will be
* simply ignored, so the worst thing that can happen is that you just lose some
* notes.
*
* <h2>Behaviour, JavaScript, and CSS</h2> <p> {@link ConsoleNote} can have
* associated <tt>script.js</tt> and <tt>style.css</tt> (put them in the same
* resource directory that you normally put Jelly scripts), which will be loaded
* into the HTML page whenever the console notes are used. This allows you to
* use minimal markup in code generation, and do the styling in CSS and perform
* the rest of the interesting work as a CSS behaviour/JavaScript.
*
* @param <T> Contextual model object that this console is associated with, such
* as {@link Run}.
*
* @author Kohsuke Kawaguchi
* @see ConsoleAnnotationDescriptor
* @see Functions#generateConsoleAnnotationScriptAndStylesheet()
* @since 1.349
*/
public abstract class ConsoleNote<T> implements Serializable, Describable<ConsoleNote<?>> {
/**
* When the line of a console output that this annotation is attached is
* read by someone, a new {@link ConsoleNote} is de-serialized and this
* method is invoked to annotate that line.
*
* @param context The object that owns the console output in question.
* @param text Represents a line of the console output being annotated.
* @param charPos The character position in 'text' where this annotation is
* attached.
*
* @return if non-null value is returned, this annotator will handle the
* next line. this mechanism can be used to annotate multiple lines starting
* at the annotated position.
*/
public abstract ConsoleAnnotator annotate(T context, MarkupText text, int charPos);
public ConsoleAnnotationDescriptor getDescriptor() {
return (ConsoleAnnotationDescriptor) Hudson.getInstance().getDescriptorOrDie(getClass());
}
/**
* Prints this note into a stream.
*
* <p> The most typical use of this is {@code n.encodedTo(System.out)} where
* stdout is connected to Hudson. The encoded form doesn't include any new
* line character to work better in the line-oriented nature of
* {@link ConsoleAnnotator}.
*/
public void encodeTo(OutputStream out) throws IOException {
// atomically write to the final output, to minimize the chance of something else getting in between the output.
// even with this, it is still technically possible to get such a mix-up to occur (for example,
// if Java program is reading stdout/stderr separately and copying them into the same final stream.)
out.write(encodeToBytes().toByteArray());
}
/**
* Prints this note into a writer.
*
* <p> Technically, this method only works if the {@link Writer} to
* {@link OutputStream} encoding is ASCII compatible.
*/
public void encodeTo(Writer out) throws IOException {
out.write(encodeToBytes().toString());
}
private ByteArrayOutputStream encodeToBytes() throws IOException {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new GZIPOutputStream(buf));
oos.writeObject(this);
} finally {
IOUtils.closeQuietly(oos);
}
ByteArrayOutputStream buf2 = new ByteArrayOutputStream();
DataOutputStream dos = null;
try {
dos = new DataOutputStream(new Base64OutputStream(buf2, true, -1, null));
buf2.write(PREAMBLE);
dos.writeInt(buf.size());
buf.writeTo(dos);
} finally {
IOUtils.closeQuietly(dos);
buf2.write(POSTAMBLE);
}
return buf2;
}
/**
* Works like {@link #encodeTo(Writer)} but obtain the result as a string.
*/
public String encode() throws IOException {
StringWriter sw = new StringWriter();
encodeTo(sw);
return sw.toString();
}
/**
* Reads a note back from
* {@linkplain #encodeTo(OutputStream) its encoded form}.
*
* @param in Must point to the beginning of a preamble.
*
* @return null if the encoded form is malformed.
*/
public static ConsoleNote readFrom(DataInputStream in) throws IOException, ClassNotFoundException {
ObjectInputStream ois = null;
DataInputStream decoded = null;
try {
byte[] preamble = new byte[PREAMBLE.length];
in.readFully(preamble);
if (!Arrays.equals(preamble, PREAMBLE)) {
return null; // not a valid preamble
}
decoded = new DataInputStream(new UnbufferedBase64InputStream(in));
int sz = decoded.readInt();
//Size should be greater than Zero. See http://issues.hudson-ci.org/browse/HUDSON-6558
if (sz < 0) {
return null;
}
byte[] buf = new byte[sz];
decoded.readFully(buf);
byte[] postamble = new byte[POSTAMBLE.length];
in.readFully(postamble);
if (!Arrays.equals(postamble, POSTAMBLE)) {
return null; // not a valid postamble
}
ois = new ObjectInputStreamEx(
new GZIPInputStream(new ByteArrayInputStream(buf)), Hudson.getInstance().pluginManager.uberClassLoader);
return (ConsoleNote) ois.readObject();
} catch (Error e) {
// for example, bogus 'sz' can result in OutOfMemoryError.
// package that up as IOException so that the caller won't fatally die.
throw new IOException2(e);
} finally {
IOUtils.closeQuietly(ois);
IOUtils.closeQuietly(decoded);
}
}
/**
* Skips the encoded console note.
*/
public static void skip(DataInputStream in) throws IOException {
byte[] preamble = new byte[PREAMBLE.length];
in.readFully(preamble);
if (!Arrays.equals(preamble, PREAMBLE)) {
return; // not a valid preamble
}
DataInputStream decoded = new DataInputStream(new UnbufferedBase64InputStream(in));
int sz = decoded.readInt();
IOUtils.skip(decoded, sz);
byte[] postamble = new byte[POSTAMBLE.length];
in.readFully(postamble);
}
private static final long serialVersionUID = 1L;
public static final String PREAMBLE_STR = "\u001B[8mha:";
public static final String POSTAMBLE_STR = "\u001B[0m";
/**
* Preamble of the encoded form. ANSI escape sequence to stop echo back plus
* a few magic characters.
*/
public static final byte[] PREAMBLE = PREAMBLE_STR.getBytes();
/**
* Post amble is the ANSI escape sequence that brings back the echo.
*/
public static final byte[] POSTAMBLE = POSTAMBLE_STR.getBytes();
/**
* Locates the preamble in the given buffer.
*/
public static int findPreamble(byte[] buf, int start, int len) {
int e = start + len - PREAMBLE.length + 1;
OUTER:
for (int i = start; i < e; i++) {
if (buf[i] == PREAMBLE[0]) {
// check for the rest of the match
for (int j = 1; j < PREAMBLE.length; j++) {
if (buf[i + j] != PREAMBLE[j]) {
continue OUTER;
}
}
return i; // found it
}
}
return -1; // not found
}
/**
* Removes the embedded console notes in the given log lines.
*
* @since 1.350
*/
public static List<String> removeNotes(Collection<String> logLines) {
List<String> r = new ArrayList<String>(logLines.size());
for (String l : logLines) {
r.add(removeNotes(l));
}
return r;
}
/**
* Removes the embedded console notes in the given log line.
*
* @since 1.350
*/
public static String removeNotes(String line) {
while (true) {
int idx = line.indexOf(PREAMBLE_STR);
if (idx < 0) {
return line;
}
int e = line.indexOf(POSTAMBLE_STR, idx);
if (e < 0) {
return line;
}
line = line.substring(0, idx) + line.substring(e + POSTAMBLE_STR.length());
}
}
}