/******************************************************************************* * * 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()); } } }