提交 c6acb5b8 编写于 作者: K kohsuke

[FIXED HUDSON-2137]

Merged revisions 27137,27564,27566-27567,27578,27586,27599,27603,27606-27609,27611,27616,27626-27632,27640,27654-27656,27687,27701,27801,27816,27819,27843-27845,28087,28158-28161 via svnmerge from 
https://www.dev.java.net/svn/hudson/branches/HUDSON-2137

........
  r27137 | kohsuke | 2010-02-08 08:46:16 -0800 (Mon, 08 Feb 2010) | 1 line
  
  experimenting with abstractions
........
  r27564 | kohsuke | 2010-02-16 07:58:26 -0800 (Tue, 16 Feb 2010) | 1 line
  
  bundling makes it easier for caller to handle ConsoleAnnotator
........
  r27566 | kohsuke | 2010-02-16 08:49:48 -0800 (Tue, 16 Feb 2010) | 1 line
  
  this should work with Runs, not AbstractBuilds
........
  r27567 | kohsuke | 2010-02-16 08:54:06 -0800 (Tue, 16 Feb 2010) | 1 line
  
  simplified
........
  r27578 | kohsuke | 2010-02-16 12:26:11 -0800 (Tue, 16 Feb 2010) | 1 line
  
  still a work in progress, but committing once to pull in trunk changes
........
  r27586 | kohsuke | 2010-02-16 14:35:37 -0800 (Tue, 16 Feb 2010) | 1 line
  
  making more progress
........
  r27599 | kohsuke | 2010-02-16 19:22:01 -0800 (Tue, 16 Feb 2010) | 2 lines
  
  hooking up the side that writes annotations.
  still a work in progress.
........
  r27603 | kohsuke | 2010-02-16 23:43:35 -0800 (Tue, 16 Feb 2010) | 3 lines
  
  what appears to me as the first cut of hooking up the reader side of the annotations.
  
  I haven't even run it yet --- this just passed the compilation, and I'm calling it a day.
........
  r27606 | kohsuke | 2010-02-17 09:15:16 -0800 (Wed, 17 Feb 2010) | 1 line
  
  demo instance hook up
........
  r27607 | kohsuke | 2010-02-17 11:27:02 -0800 (Wed, 17 Feb 2010) | 3 lines
  
  one more utility method.
  
  I never understood what the point of letting the skip method skip less.
........
  r27608 | kohsuke | 2010-02-17 11:56:23 -0800 (Wed, 17 Feb 2010) | 3 lines
  
  - moved LargeText subtype to its own class
  - skipping log output was done incorrectly --- it had to skip N bytes where it was skipping N chars.
  - hooked up console annotations for completed builds.
........
  r27609 | kohsuke | 2010-02-17 12:01:46 -0800 (Wed, 17 Feb 2010) | 1 line
  
  copyright header
........
  r27611 | kohsuke | 2010-02-17 13:18:10 -0800 (Wed, 17 Feb 2010) | 1 line
  
  introduced a mechanism to register extension during an unit test.
........
  r27616 | kohsuke | 2010-02-17 13:38:12 -0800 (Wed, 17 Feb 2010) | 1 line
  
  test case for console annotations
........
  r27626 | kohsuke | 2010-02-17 15:23:06 -0800 (Wed, 17 Feb 2010) | 1 line
  
  removing test ConsoleAnnotator as I write tests as tests
........
  r27627 | kohsuke | 2010-02-17 16:06:13 -0800 (Wed, 17 Feb 2010) | 1 line
  
  added a multi-phase cooperative locking mechanism useful during the debugging
........
  r27628 | kohsuke | 2010-02-17 16:06:41 -0800 (Wed, 17 Feb 2010) | 1 line
  
  testing progressive log annotation
........
  r27629 | kohsuke | 2010-02-17 16:20:32 -0800 (Wed, 17 Feb 2010) | 1 line
  
  adding a test for ConsoleAnnotation that's explicitly placed.
........
  r27630 | kohsuke | 2010-02-17 16:23:02 -0800 (Wed, 17 Feb 2010) | 1 line
  
  removing test ConsoleAnnotation as I write tests as tests
........
  r27631 | kohsuke | 2010-02-17 16:28:50 -0800 (Wed, 17 Feb 2010) | 1 line
  
  tweaking the abstraction a bit.
........
  r27632 | kohsuke | 2010-02-17 16:44:21 -0800 (Wed, 17 Feb 2010) | 1 line
  
  parameterized the context type so that this can be used in places other than the build console output, such as polling logs, slave logs, and so on.
........
  r27640 | kohsuke | 2010-02-17 17:27:52 -0800 (Wed, 17 Feb 2010) | 1 line
  
  bug fix
........
  r27654 | kohsuke | 2010-02-17 22:53:04 -0800 (Wed, 17 Feb 2010) | 1 line
  
  doc improvement
........
  r27655 | kohsuke | 2010-02-17 22:53:59 -0800 (Wed, 17 Feb 2010) | 1 line
  
  doc improvement
........
  r27656 | kohsuke | 2010-02-17 23:10:42 -0800 (Wed, 17 Feb 2010) | 1 line
  
  test case for adjacent tags
........
  r27687 | kohsuke | 2010-02-19 14:39:21 -0800 (Fri, 19 Feb 2010) | 1 line
  
  introduced a facotry and descriptor. This allows them to define configuration pages, allow users to disable/enable them, plus define custom behaviors.
........
  r27701 | kohsuke | 2010-02-19 18:23:52 -0800 (Fri, 19 Feb 2010) | 1 line
  
  defining a mechanism for ConsoleAnnotatorFactory and ConsoleAnnotationDescriptor to define script.js and have them imported into the console output page
........
  r27801 | kohsuke | 2010-02-22 11:59:16 -0800 (Mon, 22 Feb 2010) | 1 line
  
  now embedding annotations inside the console output log file.
........
  r27816 | kohsuke | 2010-02-22 15:45:04 -0800 (Mon, 22 Feb 2010) | 1 line
  
  introduced a convenience method for unit tests
........
  r27819 | kohsuke | 2010-02-22 17:37:05 -0800 (Mon, 22 Feb 2010) | 5 lines
  
  As an example of using embedded console annotations, implementing Ant target invocation outlines.
  
  It might be better to make the concept of outline more generally reusable, so that different kinds of outlines can be placed into the same container element for a single hierarchical representation.
  
  Still a work in progress.
........
  r27843 | kohsuke | 2010-02-23 16:12:13 -0800 (Tue, 23 Feb 2010) | 1 line
  
  for now, disable this for production usage, until I work out a better UI.
........
  r27844 | kohsuke | 2010-02-23 19:08:00 -0800 (Tue, 23 Feb 2010) | 1 line
  
  started working on annotating exception.
........
  r27845 | kohsuke | 2010-02-23 19:08:33 -0800 (Tue, 23 Feb 2010) | 1 line
  
  more TODOs for myself
........
  r28087 | kohsuke | 2010-03-01 10:03:55 -0800 (Mon, 01 Mar 2010) | 1 line
  
  formatting changes
........
  r28158 | kohsuke | 2010-03-02 17:00:00 -0800 (Tue, 02 Mar 2010) | 1 line
  
  ConsoleAnnotation and ConsoleAnnotator are too confusing.
........
  r28159 | kohsuke | 2010-03-02 17:02:02 -0800 (Tue, 02 Mar 2010) | 1 line
  
  doc improvement
........
  r28160 | kohsuke | 2010-03-02 17:05:14 -0800 (Tue, 02 Mar 2010) | 1 line
  
  more doc improvements.
........
  r28161 | kohsuke | 2010-03-02 17:11:59 -0800 (Tue, 02 Mar 2010) | 1 line
  
  more doc improvement
........


git-svn-id: https://hudson.dev.java.net/svn/hudson/trunk/hudson/main@28173 71c3de6d-444a-0410-be80-ed276b4c234a
上级 02b9fc31
......@@ -334,7 +334,7 @@ THE SOFTWARE.
<dependency>
<groupId>org.kohsuke.stapler</groupId>
<artifactId>stapler-jelly</artifactId>
<version>1.135</version>
<version>1.136</version>
<exclusions>
<exclusion>
<groupId>commons-jelly</groupId>
......@@ -553,11 +553,6 @@ THE SOFTWARE.
<artifactId>commons-jexl</artifactId>
<version>1.1-hudson-20090508</version>
</dependency>
<dependency>
<groupId>org.jvnet.hudson</groupId>
<artifactId>commons-jelly</artifactId>
<version>1.1-hudson-20100215</version>
</dependency>
<dependency>
<groupId>org.acegisecurity</groupId>
<artifactId>acegi-security</artifactId>
......
......@@ -65,6 +65,15 @@ public abstract class AbstractMarkupText {
*/
public abstract void addMarkup( int startPos, int endPos, String startTag, String endTag );
/**
* Inserts an A tag that surrounds the given position.
*
* @since 1.349
*/
public void addHyperlink( int startPos, int endPos, String url ) {
addMarkup(startPos,endPos,"<a href='"+url+"'>","</a>");
}
/**
* Adds a start tag and end tag around the entire text
*/
......@@ -72,6 +81,21 @@ public abstract class AbstractMarkupText {
addMarkup(0,length(),startTag,endTag);
}
/**
* Find the first occurrence of the given pattern in this text, or null.
*
* @since 1.349
*/
public MarkupText.SubText findToken(Pattern pattern) {
String text = getText();
Matcher m = pattern.matcher(text);
if(m.find())
return createSubText(m);
return null;
}
/**
* Find all "tokens" that match the given pattern in this text.
*
......
......@@ -26,11 +26,12 @@ package hudson;
import net.java.sezpoz.Indexable;
import java.lang.annotation.Documented;
import static java.lang.annotation.ElementType.*;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Marks a field, a method, or a class for automatic discovery, so that Hudson can locate
* implementations of {@link ExtensionPoint}s automatically.
......@@ -64,7 +65,7 @@ import java.lang.annotation.Target;
* @see ExtensionList
*/
@Indexable
@Retention(RetentionPolicy.RUNTIME)
@Retention(RUNTIME)
@Target({TYPE, FIELD, METHOD})
@Documented
public @interface Extension {
......
......@@ -161,6 +161,17 @@ public class ExtensionList<T> extends AbstractList<T> {
add(element);
}
/**
* Used to bind extension to URLs by their class names.
*
* @since 1.349
*/
public T getDynamic(String className) {
for (T t : this)
if (t.getClass().getName().equals(className))
return t;
return null;
}
/**
......
......@@ -23,6 +23,8 @@
*/
package hudson;
import hudson.console.ConsoleAnnotationDescriptor;
import hudson.console.ConsoleAnnotatorFactory;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.Descriptor;
......@@ -1133,6 +1135,24 @@ public class Functions {
public static Date getCurrentTime() {
return new Date();
}
/**
* Generate a series of &lt;script> tags to include <tt>script.js</tt>
* from {@link ConsoleAnnotatorFactory}s and {@link ConsoleAnnotationDescriptor}s.
*/
public static String generateConsoleAnnotationScript() {
String cp = Stapler.getCurrentRequest().getContextPath();
StringBuilder buf = new StringBuilder();
for (ConsoleAnnotatorFactory f : ConsoleAnnotatorFactory.all()) {
if (f.hasScript())
buf.append("<script src='"+cp+"/extensionList/"+ConsoleAnnotatorFactory.class.getName()+"/"+f.getClass().getName()+"/script.js'></script>");
}
for (ConsoleAnnotationDescriptor d : ConsoleAnnotationDescriptor.all()) {
if (d.hasScript())
buf.append("<script src='"+cp+"/descriptor/"+d.clazz.getName()+"/script.js'></script>");
}
return buf.toString();
}
private static final Pattern SCHEME = Pattern.compile("[a-z]+://.+");
......
......@@ -52,9 +52,14 @@ public class MarkupText extends AbstractMarkupText {
* Represents one mark up inserted into text.
*/
private static final class Tag implements Comparable<Tag> {
/**
* Char position of this tag in {@link MarkupText#text}.
* This tag is placed in front of the character of this index.
*/
private final int pos;
private final String markup;
public Tag(int pos, String markup) {
this.pos = pos;
this.markup = markup;
......@@ -214,10 +219,15 @@ public class MarkupText extends AbstractMarkupText {
if(startPos>endPos) throw new IndexOutOfBoundsException();
// when multiple tags are added to the same range, we want them to show up like
// <b><i>abc</i></b>, not <b><i>abc</b></i>. Do this by inserting them to different
// places.
tags.add(0,new Tag(startPos, startTag));
tags.add(new Tag(endPos,endTag));
// <b><i>abc</i></b>, not <b><i>abc</b></i>. Also, we'd like <b>abc</b><i>def</i>,
// not <b>abc<i></b>def</i>. Do this by inserting them to different places.
tags.add(new Tag(startPos, startTag));
tags.add(0,new Tag(endPos,endTag));
}
public void addMarkup(int pos, String tag) {
rangeCheck(pos);
tags.add(new Tag(pos,tag));
}
private void rangeCheck(int pos) {
......
/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, 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 hudson.console;
import com.trilead.ssh2.crypto.Base64;
import hudson.model.Hudson;
import hudson.remoting.ObjectInputStreamEx;
import hudson.util.IOException2;
import hudson.util.TimeUnit2;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.framework.io.LargeText;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Writer;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import static java.lang.Math.abs;
/**
* Extension to {@link LargeText} that handles annotations by {@link ConsoleAnnotator}.
*
* <p>
* In addition to run each line through {@link ConsoleAnnotationOutputStream} for adding markup,
* this class persists {@link ConsoleAnnotator} into a byte sequence and send it to the client
* as an HTTP header. The client JavaScript sends it back next time it fetches the following output.
*
* <p>
* The serialized {@link ConsoleAnnotator} is encrypted to avoid malicious clients from instantiating
* arbitrary {@link ConsoleAnnotator}s.
*
* @param <T>
* Context type.
* @author Kohsuke Kawaguchi
* @since 1.349
*/
public class AnnotatedLargeText<T> extends LargeText {
private final File logFile;
private T context;
public AnnotatedLargeText(File file, Charset charset, boolean completed, T context) {
super(file, charset, completed);
this.logFile = file;
this.context = context;
}
private ConsoleAnnotator createAnnotator(StaplerRequest req) throws IOException {
try {
String base64 = req.getHeader("X-ConsoleAnnotator");
if (base64!=null) {
Cipher sym = Cipher.getInstance("AES");
sym.init(Cipher.DECRYPT_MODE, Hudson.getInstance().getSecretKeyAsAES128());
ObjectInputStream ois = new ObjectInputStreamEx(new GZIPInputStream(
new CipherInputStream(new ByteArrayInputStream(Base64.decode(base64.toCharArray())),sym)),
Hudson.getInstance().pluginManager.uberClassLoader);
long timestamp = ois.readLong();
if (TimeUnit2.HOURS.toMillis(1) > abs(System.currentTimeMillis()-timestamp))
// don't deserialize something too old to prevent a replay attack
return (ConsoleAnnotator)ois.readObject();
}
} catch (GeneralSecurityException e) {
throw new IOException2(e);
} catch (ClassNotFoundException e) {
throw new IOException2(e);
}
// start from scratch
return ConsoleAnnotator.initial(context.getClass());
}
@Override
public long writeLogTo(long start, Writer w) throws IOException {
ConsoleAnnotationOutputStream caw = new ConsoleAnnotationOutputStream(
w, createAnnotator(Stapler.getCurrentRequest()), context, charset);
long r = super.writeLogTo(start,caw);
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Cipher sym = Cipher.getInstance("AES");
sym.init(Cipher.ENCRYPT_MODE,Hudson.getInstance().getSecretKeyAsAES128());
ObjectOutputStream oos = new ObjectOutputStream(new GZIPOutputStream(new CipherOutputStream(baos,sym)));
oos.writeLong(System.currentTimeMillis()); // send timestamp to prevent a replay attack
oos.writeObject(caw.getConsoleAnnotator());
oos.close();
Stapler.getCurrentResponse().setHeader("X-ConsoleAnnotator",new String(Base64.encode(baos.toByteArray())));
} catch (GeneralSecurityException e) {
throw new IOException2(e);
}
return r;
}
}
/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, 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 hudson.console;
import hudson.DescriptorExtensionList;
import hudson.ExtensionPoint;
import hudson.model.Descriptor;
import hudson.model.Hudson;
import hudson.util.TimeUnit2;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.WebMethod;
import javax.servlet.ServletException;
import java.io.IOException;
import java.net.URL;
/**
* Descriptor for {@link ConsoleNote}.
*
* @author Kohsuke Kawaguchi
* @since 1.349
*/
public abstract class ConsoleAnnotationDescriptor extends Descriptor<ConsoleNote<?>> implements ExtensionPoint {
public ConsoleAnnotationDescriptor(Class<? extends ConsoleNote<?>> clazz) {
super(clazz);
}
public ConsoleAnnotationDescriptor() {
}
/**
* {@inheritDoc}
*
* Users use this name to enable/disable annotations.
*/
public abstract String getDisplayName();
/**
* Returns true if this descriptor has a JavaScript to be inserted on applicable console page.
*/
public boolean hasScript() {
return getScriptJs()!=null;
}
private URL getScriptJs() {
return clazz.getClassLoader().getResource(clazz.getName().replace('.','/').replace('$','/')+"/script.js");
}
@WebMethod(name="script.js")
public void doScriptJs(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
rsp.serveFile(req, getScriptJs(), TimeUnit2.DAYS.toMillis(1));
}
/**
* Returns all the registered {@link ConsoleAnnotationDescriptor} descriptors.
*/
public static DescriptorExtensionList<ConsoleNote<?>,ConsoleAnnotationDescriptor> all() {
return (DescriptorExtensionList)Hudson.getInstance().getDescriptorList(ConsoleNote.class);
}
}
/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, 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 hudson.console;
import hudson.MarkupText;
import org.apache.commons.io.output.ProxyWriter;
import org.kohsuke.stapler.framework.io.WriterOutputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Used to convert plain text console output (as byte sequence) + embedded annotations into HTML (as char sequence),
* by using {@link ConsoleAnnotator}.
*
* @param <T>
* Context type.
* @author Kohsuke Kawaguchi
* @since 1.349
*/
public class ConsoleAnnotationOutputStream<T> extends LineTransformationOutputStream {
private final Writer out;
private final T context;
private ConsoleAnnotator<T> ann;
/**
* Reused buffer that stores char representation of a single line.
*/
private final LineBuffer line = new LineBuffer(256);
/**
* {@link OutputStream} that writes to {@link #line}.
*/
private final WriterOutputStream lineOut;
/**
*
*/
public ConsoleAnnotationOutputStream(Writer out, ConsoleAnnotator<? super T> ann, T context, Charset charset) {
this.out = out;
this.ann = ConsoleAnnotator.cast(ann);
this.context = context;
this.lineOut = new WriterOutputStream(line,charset);
}
public ConsoleAnnotator getConsoleAnnotator() {
return ann;
}
/**
* Called after we read the whole line of plain text, which is stored in {@link #buf}.
* This method performs annotations and send the result to {@link #out}.
*/
protected void eol(byte[] in, int sz) throws IOException {
line.reset();
final StringBuffer strBuf = line.getStringBuffer();
int next = ConsoleNote.findPreamble(in,0,sz);
List<ConsoleAnnotator<T>> annotators=null;
{// perform byte[]->char[] while figuring out the char positions of the BLOBs
int written = 0;
while (next>=0) {
if (next>written) {
lineOut.write(in,written,next-written);
lineOut.flush();
written = next;
} else {
assert next==written;
}
// character position of this annotation in this line
final int charPos = strBuf.length();
int rest = sz - next;
ByteArrayInputStream b = new ByteArrayInputStream(in, next, rest);
try {
final ConsoleNote a = ConsoleNote.readFrom(new DataInputStream(b));
if (a!=null) {
if (annotators==null)
annotators = new ArrayList<ConsoleAnnotator<T>>();
annotators.add(new ConsoleAnnotator<T>() {
public ConsoleAnnotator annotate(T context, MarkupText text) {
return a.annotate(context,text,charPos);
}
});
}
} catch (IOException e) {
// if we failed to resurrect an annotation, ignore it.
LOGGER.log(Level.FINE,"Failed to resurrect annotation",e);
} catch (ClassNotFoundException e) {
LOGGER.log(Level.FINE,"Failed to resurrect annotation",e);
}
int bytesUsed = rest - b.available(); // bytes consumed by annotations
written += bytesUsed;
next = ConsoleNote.findPreamble(in,written,sz-written);
}
// finish the remaining bytes->chars conversion
lineOut.write(in,written,sz-written);
if (annotators!=null) {
// aggregate newly retrieved ConsoleAnnotators into the current one.
if (ann!=null) annotators.add(ann);
ann = ConsoleAnnotator.combine(annotators);
}
}
lineOut.flush();
MarkupText mt = new MarkupText(strBuf.toString());
if (ann!=null)
ann = ann.annotate(context,mt);
out.write(mt.toString());
}
@Override
public void flush() throws IOException {
out.flush();
}
@Override
public void close() throws IOException {
super.close();
out.close();
}
/**
* {@link StringWriter} enhancement that's capable of shrinking the buffer size.
*
* <p>
* The problem is that {@link StringBuffer#setLength(int)} doesn't actually release
* the underlying buffer, so for us to truncate the buffer, we need to create a new {@link StringWriter} instance.
*/
private static class LineBuffer extends ProxyWriter {
private LineBuffer(int initialSize) {
super(new StringWriter(initialSize));
}
private void reset() {
StringBuffer buf = getStringBuffer();
if (buf.length()>4096)
out = new StringWriter(256);
else
buf.setLength(0);
}
private StringBuffer getStringBuffer() {
StringWriter w = (StringWriter)out;
return w.getBuffer();
}
}
private static final Logger LOGGER = Logger.getLogger(ConsoleAnnotationOutputStream.class.getName());
}
/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, 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 hudson.console;
import hudson.MarkupText;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.ListIterator;
/**
* Annotates one line of console output.
*
* <p>
* In Hudson, console output annotation is done line by line, and
* we model this as a state machine &mdash;
* the code encapsulates some state, and it uses that to annotate one line (and possibly update the state.)
*
* <p>
* A {@link ConsoleAnnotator} instance encapsulates this state, and the {@link #annotate(Object, MarkupText)}
* method is used to annotate the next line based on the current state. The method returns another
* {@link ConsoleAnnotator} instance that represents the altered state for annotating the next line.
*
* <p>
* {@link ConsoleAnnotator}s are run when a browser requests console output, and the above-mentioned chain
* invocation is done for each client request separately. Therefore, logically you can think of this process as:
*
* <pre>
* ConsoleAnnotator ca = ...;
* ca.annotate(context,line1).annotate(context,line2)...
* </pre>
*
* <p>
* Because of a browser can request console output incrementally, in addition to above a console annotator
* can be serialized at any point and deserialized back later to continue annotation where it left off.
*
* <p>
* {@link ConsoleAnnotator} instances can be created in a few different ways. See {@link ConsoleNote}
* and {@link ConsoleAnnotatorFactory}.
*
* @author Kohsuke Kawaguchi
* @see ConsoleAnnotatorFactory
* @see ConsoleNote
* @since 1.349
*/
public abstract class ConsoleAnnotator<T> implements Serializable {
/**
* Annotates one line.
*
* @param context
* The object that owns the console output. Never null.
* @param text
* Contains a single line of console output, and defines convenient methods to add markup.
* The callee should put markup into this object. Never null.
* @return
* The {@link ConsoleAnnotator} object that will annotate the next line of the console output.
* To indicate that you are not interested in the following lines, return null.
*/
public abstract ConsoleAnnotator annotate(T context, MarkupText text );
/**
* Cast operation that restricts T.
*/
public static <T> ConsoleAnnotator<T> cast(ConsoleAnnotator<? super T> a) {
return (ConsoleAnnotator)a;
}
/**
* Bundles all the given {@link ConsoleAnnotator} into a single annotator.
*/
public static <T> ConsoleAnnotator<T> combine(Collection<? extends ConsoleAnnotator<? super T>> all) {
switch (all.size()) {
case 0: return null; // none
case 1: return cast(all.iterator().next()); // just one
}
class Aggregator extends ConsoleAnnotator<T> {
List<ConsoleAnnotator<T>> list;
Aggregator(Collection list) {
this.list = new ArrayList<ConsoleAnnotator<T>>(list);
}
public ConsoleAnnotator annotate(T context, MarkupText text) {
ListIterator<ConsoleAnnotator<T>> itr = list.listIterator();
while (itr.hasNext()) {
ConsoleAnnotator a = itr.next();
ConsoleAnnotator b = a.annotate(context,text);
if (a!=b) {
if (b==null) itr.remove();
else itr.set(b);
}
}
switch (list.size()) {
case 0: return null; // no more annotator left
case 1: return list.get(0); // no point in aggregating
default: return this;
}
}
}
return new Aggregator(all);
}
/**
* Returns the all {@link ConsoleAnnotator}s for the given context type aggregated into a single
* annotator.
*/
public static <T> ConsoleAnnotator<T> initial(T context) {
return combine(_for(context));
}
/**
* List all the console annotators that can work for the specified context type.
*/
public static <T> List<ConsoleAnnotator<T>> _for(T context) {
List<ConsoleAnnotator<T>> r = new ArrayList<ConsoleAnnotator<T>>();
for (ConsoleAnnotatorFactory f : ConsoleAnnotatorFactory.all()) {
if (f.type().isInstance(context)) {
ConsoleAnnotator ca = f.newInstance(context);
if (ca!=null)
r.add(ca);
}
}
return r;
}
private static final long serialVersionUID = 1L;
}
/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, 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 hudson.console;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.model.Hudson;
import hudson.model.Run;
import hudson.util.TimeUnit2;
import org.jvnet.tiger_types.Types;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.WebMethod;
import javax.servlet.ServletException;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URL;
/**
* Entry point to the {@link ConsoleAnnotator} extension point. This class creates a new instance
* of {@link ConsoleAnnotator} that starts a new console annotation session.
*
* <p>
* {@link ConsoleAnnotatorFactory}s are used whenever a browser requests console output (as opposed to when
* the console output is being produced &mdash; for that see {@link ConsoleNote}.)
*
* <p>
* {@link ConsoleAnnotator}s returned by {@link ConsoleAnnotatorFactory} are asked to start from
* an arbitrary line of the output, because typically browsers do not request the entire console output.
* Because of this, {@link ConsoleAnnotatorFactory} is generally suitable for peep-hole local annotation
* that only requires a small contextual information, such as keyword coloring, URL hyperlinking, and so on.
*
* @author Kohsuke Kawaguchi
* @since 1.349
*/
public abstract class ConsoleAnnotatorFactory<T> implements ExtensionPoint {
/**
* Called when a console output page is requested to create a stateful {@link ConsoleAnnotator}.
*
* <p>
* This method can be invoked concurrently by multiple threads.
*
* @param context
* The model object that owns the console output, such as {@link Run}.
* This method is only called when the context object if assignable to
* {@linkplain #type() the advertised type}.
* @return
* null if this factory is not going to participate in the annotation of this console.
*/
public abstract ConsoleAnnotator newInstance(T context);
/**
* For which context type does this annotator work?
*/
public Class type() {
Type type = Types.getBaseClass(getClass(), ConsoleAnnotator.class);
if (type instanceof ParameterizedType)
return Types.erasure(Types.getTypeArgument(type,0));
else
return Object.class;
}
/**
* Returns true if this descriptor has a JavaScript to be inserted on applicable console page.
*/
public boolean hasScript() {
return getScriptJs() !=null;
}
private URL getScriptJs() {
Class c = getClass();
return c.getClassLoader().getResource(c.getName().replace('.','/').replace('$','/')+"/script.js");
}
/**
* Serves the JavaScript file associated with this console annotator factory.
*/
@WebMethod(name="script.js")
public void doScriptJs(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
rsp.serveFile(req,getScriptJs(), TimeUnit2.DAYS.toMillis(1));
}
/**
* All the registered instances.
*/
public static ExtensionList<ConsoleAnnotatorFactory> all() {
return Hudson.getInstance().getExtensionList(ConsoleAnnotatorFactory.class);
}
}
/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, 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 hudson.console;
import hudson.CloseProofOutputStream;
import hudson.MarkupText;
import hudson.model.Describable;
import hudson.model.Hudson;
import hudson.model.Run;
import hudson.remoting.ObjectInputStreamEx;
import hudson.util.FlushProofOutputStream;
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.util.Arrays;
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.
*
* @param <T>
* Contextual model object that this console is associated with, such as {@link Run}.
*
* @author Kohsuke Kawaguchi
* @see ConsoleAnnotationDescriptor
* @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 {
out.write(PREAMBLE);
ByteArrayOutputStream buf = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(new GZIPOutputStream(buf));
oos.writeObject(this);
oos.close();
DataOutputStream dos = new DataOutputStream(new Base64OutputStream(new FlushProofOutputStream(new CloseProofOutputStream(out)),true,-1,null));
// we don't need the size by ourselves, but it's useful to gracefully recover from an error
// if the deserialization fail in the middle.
dos.writeInt(buf.size());
buf.writeTo(dos);
dos.close();
out.write(POSTAMBLE);
}
/**
* 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 {
byte[] preamble = new byte[PREAMBLE.length];
in.readFully(preamble);
if (!Arrays.equals(preamble,PREAMBLE))
return null; // not a valid preamble
DataInputStream decoded = new DataInputStream(new UnbufferedBase64InputStream(in));
int sz = decoded.readInt();
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
ObjectInputStream ois = new ObjectInputStreamEx(
new GZIPInputStream(new ByteArrayInputStream(buf)), Hudson.getInstance().pluginManager.uberClassLoader);
return (ConsoleNote) ois.readObject();
}
private static final long serialVersionUID = 1L;
/**
* Preamble of the encoded form. ANSI escape sequence to stop echo back
* plus a few magic characters.
*/
public static final byte[] PREAMBLE = "\u001B[8mha:".getBytes();
/**
* Post amble is the ANSI escape sequence that brings back the echo.
*/
public static final byte[] POSTAMBLE = "\u001B[0m".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
}
}
package hudson.console;
import hudson.Extension;
import hudson.MarkupText;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Placed on the beginning of the exception stack trace produced by Hudson, which in turn produces hyperlinked stack trace.
*
* <p>
* Exceptions in the user code (like junit etc) should be handled differently. This is only for exceptions
* that occur inside Hudson.
*
* @author Kohsuke Kawaguchi
* @since 1.349
*/
public class HudsonExceptionNote extends ConsoleNote<Object> {
@Override
public ConsoleAnnotator annotate(Object context, MarkupText text, int charPos) {
// An exception stack trace looks like this:
// org.acme.FooBarException: message
// <TAB>at org.acme.Foo.method(Foo.java:123)
// Caused by: java.lang.ClassNotFoundException:
String line = text.getText();
int end = line.indexOf(':',charPos);
if (end<0) {
if (CLASSNAME.matcher(line.substring(charPos)).matches())
end = line.length();
else
return null; // unexpected format. abort.
}
text.addHyperlink(charPos,end,annotateClassName(line.substring(charPos,end)));
return new ConsoleAnnotator() {
public ConsoleAnnotator annotate(Object context, MarkupText text) {
String line = text.getText();
Matcher m = STACK_TRACE_ELEMENT.matcher(line);
if (m.find()) {// allow the match to happen in the middle of a line to cope with prefix. Ant and Maven put them, among many other tools.
text.addHyperlink(m.start()+4,m.end(),annotateMethodName(m.group(1),m.group(2),m.group(3),Integer.parseInt(m.group(4))));
return this;
}
int idx = line.indexOf(CAUSED_BY);
if (idx>=0) {
int s = idx + CAUSED_BY.length();
int e = line.indexOf(':', s);
text.addHyperlink(s,e,annotateClassName(line.substring(s,e)));
return this;
}
// looks like we are done with the stack trace
return null;
}
};
}
// TODO; separate out the annotations and mark up
private String annotateMethodName(String className, String methodName, String sourceFileName, int lineNumber) {
// for now
return "http://grepcode.com/search/?query="+className+'.'+methodName+"&entity=method";
}
private String annotateClassName(String className) {
// for now
return "http://grepcode.com/search?query="+className;
}
@Extension
public static final class DescriptorImpl extends ConsoleAnnotationDescriptor {
@Override
public String getDisplayName() {
return "Exception Stack Trace";
}
}
/**
* Regular expression that represents a valid class name.
*/
private static final String CLASSNAME_PATTERN = "[\\p{L}0-9$_.]+";
private static final Pattern CLASSNAME = Pattern.compile(CLASSNAME_PATTERN+"\r?\n?");
/**
* Matches to the line like "\tat org.acme.Foo.method(File.java:123)"
* and captures class name, method name, source file name, and line number separately.
*/
private static final Pattern STACK_TRACE_ELEMENT = Pattern.compile("\tat ("+CLASSNAME_PATTERN+")\\.([\\p{L}0-9$_<>]+)\\((\\S+):([0-9]+)\\)");
private static final String CAUSED_BY = "Caused by: ";
}
/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, 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 hudson.console;
import hudson.util.ByteArrayOutputStream2;
import java.io.IOException;
import java.io.OutputStream;
/**
* Filtering {@link OutputStream} that buffers text by line, so that the derived class
* can perform some manipulation based on the contents of the whole line.
*
* TODO: Mac is supposed to be CR-only. This class needs to handle that.
*
* @author Kohsuke Kawaguchi
* @since 1.349
*/
public abstract class LineTransformationOutputStream extends OutputStream {
private ByteArrayOutputStream2 buf = new ByteArrayOutputStream2();
/**
* Called for each end of the line.
*
* @param b
* Contents of the whole line, including the EOL code like CR/LF.
* @param len
* Specifies the length of the valid contents in 'b'. The rest is garbage.
* This is so that the caller doesn't have to allocate an array of the exact size.
*/
protected abstract void eol(byte[] b, int len) throws IOException;
public void write(int b) throws IOException {
buf.write(b);
if (b==LF) eol();
}
private void eol() throws IOException {
eol(buf.getBuffer(),buf.size());
// reuse the buffer under normal circumstances, but don't let the line buffer grow unbounded
if (buf.size()>4096)
buf = new ByteArrayOutputStream2();
else
buf.reset();
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
int end = off+len;
for( int i=off; i<end; i++ )
write(b[i]);
}
@Override
public void close() throws IOException {
forceEol();
}
/**
* Forces the EOL behavior.
*
* Useful if the caller wants to make sure the buffered content is all processed, but without
* actually neither flushing nor closing the stream.
*/
public void forceEol() throws IOException {
if (buf.size()>0) {
/*
because LargeText cuts output at the line end boundary, this is
possible only for the very end of the console output, if the output ends without NL.
*/
eol();
}
}
private static final int LF = 0x0A;
}
/**
* Beef up the plain text console output by adding HTML markup.
*/
package hudson.console;
\ No newline at end of file
package hudson.model;
import hudson.util.StreamTaskListener;
import hudson.util.NullStream;
import hudson.security.ACL;
import hudson.util.StreamTaskListener;
import org.acegisecurity.context.SecurityContextHolder;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.logging.Level;
import org.acegisecurity.context.SecurityContextHolder;
/**
* {@link PeriodicWork} that takes a long time to run.
*
......@@ -57,7 +54,7 @@ public abstract class AsyncPeriodicWork extends PeriodicWork {
} catch (InterruptedException e) {
e.printStackTrace(l.fatalError("aborted"));
} finally {
l.close();
l.closeQuietly();
}
logger.log(Level.INFO, "Finished "+name+". "+
......@@ -73,8 +70,8 @@ public abstract class AsyncPeriodicWork extends PeriodicWork {
protected StreamTaskListener createListener() {
try {
return new StreamTaskListener(getLogFile());
} catch (FileNotFoundException e) {
return new StreamTaskListener(new NullStream());
} catch (IOException e) {
throw new Error(e);
}
}
......
......@@ -860,7 +860,7 @@ public final class Hudson extends Node implements ItemGroup<TopLevelItem>, Stapl
* this just doesn't scale.
*
* @param className
* Either fully qualified class name (recommended) or the short name.
* Either fully qualified class name (recommended) or the short name of a {@link Describable} subtype.
*/
public Descriptor getDescriptor(String className) {
// legacy descriptors that are reigstered manually doesn't show up in getExtensionList, so check them explicitly.
......@@ -1769,6 +1769,15 @@ public final class Hudson extends Node implements ItemGroup<TopLevelItem>, Stapl
return extensionLists.get(extensionType);
}
/**
* Used to bind {@link ExtensionList}s to URLs.
*
* @since 1.349
*/
public ExtensionList getExtensionList(String extensionType) throws ClassNotFoundException {
return getExtensionList(pluginManager.uberClassLoader.loadClass(extensionType));
}
/**
* Returns {@link ExtensionList} that retains the discovered {@link Descriptor} instances for the given
* kind of {@link Describable}.
......
......@@ -25,7 +25,6 @@ package hudson.model;
import hudson.AbortException;
import hudson.BulkChange;
import hudson.CloseProofOutputStream;
import hudson.EnvVars;
import hudson.ExtensionPoint;
import hudson.FeedAdapter;
......@@ -33,6 +32,7 @@ import hudson.FilePath;
import hudson.Util;
import hudson.XmlFile;
import hudson.cli.declarative.CLIMethod;
import hudson.console.AnnotatedLargeText;
import hudson.matrix.MatrixBuild;
import hudson.matrix.MatrixRun;
import hudson.model.listeners.RunListener;
......@@ -48,6 +48,7 @@ import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildStep;
import hudson.tasks.test.AbstractTestResultAction;
import hudson.util.IOException2;
import hudson.util.IOUtils;
import hudson.util.LogTaskListener;
import hudson.util.XStream2;
import hudson.util.ProcessTree;
......@@ -55,12 +56,10 @@ import hudson.util.ProcessTree;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
......@@ -90,13 +89,13 @@ import java.util.zip.GZIPInputStream;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.input.NullInputStream;
import org.apache.commons.jelly.XMLOutput;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.framework.io.LargeText;
import org.apache.commons.io.IOUtils;
import com.thoughtworks.xstream.XStream;
......@@ -977,29 +976,46 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
}
/**
* Returns a Reader that reads from the log file.
* 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
* @return a reader from the log file, or null if none exists
* @return an input stream from the log file, or null if none exists
* @since 1.349
*/
public Reader getLogReader() throws IOException {
public InputStream getLogInputStream() throws IOException {
File logFile = getLogFile();
if (logFile.exists() ) {
if (charset==null) return new FileReader(logFile); // fall back
return new InputStreamReader(new FileInputStream(logFile),charset);
}
return new FileInputStream(logFile);
}
File compressedLogFile = new File(logFile.getParentFile(), logFile.getName()+ ".gz");
if (compressedLogFile.exists()) {
GZIPInputStream is = new GZIPInputStream(new FileInputStream(compressedLogFile));
if (charset==null) return new InputStreamReader(is);
else return new InputStreamReader(is,charset);
}
return new GZIPInputStream(new FileInputStream(compressedLogFile));
}
return null;
return new NullInputStream(0);
}
public Reader getLogReader() throws IOException {
if (charset==null) return new InputStreamReader(getLogInputStream());
else return new InputStreamReader(getLogInputStream(),charset);
}
/**
* Used from <tt>console.jelly</tt> to write annotated log to the given output.
*
* @since 1.349
*/
public void writeLogTo(long offset, XMLOutput out) throws IOException {
// TODO: resurrect compressed log file support
createAnnotatedLargeText().writeLogTo(offset,out.asWriter());
}
private AnnotatedLargeText createAnnotatedLargeText() {
return new AnnotatedLargeText(getLogFile(),getCharset(),!isLogUpdated(),this);
}
@Override
protected SearchIndexBuilder makeSearchIndex() {
SearchIndexBuilder builder = super.makeSearchIndex()
......@@ -1182,8 +1198,7 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
if(result!=null)
return; // already built.
BuildListener listener=null;
PrintStream log = null;
StreamBuildListener listener=null;
runner = job;
onStartBuilding();
......@@ -1195,10 +1210,9 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
try {
try {
log = new PrintStream(new FileOutputStream(getLogFile()));
Charset charset = Computer.currentComputer().getDefaultCharset();
this.charset = charset.name();
listener = new StreamBuildListener(new PrintStream(new CloseProofOutputStream(log)),charset);
listener = new StreamBuildListener(getLogFile(),charset);
listener.started(getCauses());
......@@ -1259,8 +1273,8 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
if(listener!=null)
listener.finished(result);
if(log!=null)
log.close();
if(listener!=null)
listener.closeQuietly();
try {
save();
......@@ -1516,7 +1530,7 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
* Handles incremental log output.
*/
public void doProgressiveLog( StaplerRequest req, StaplerResponse rsp) throws IOException {
new LargeText(getLogFile(),getCharset(),!isLogUpdated()).doProgressText(req,rsp);
createAnnotatedLargeText().doProgressText(req,rsp);
}
public void doToggleLogKeep( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
......@@ -1773,4 +1787,4 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
out.flush();
}
}
}
}
\ No newline at end of file
......@@ -23,18 +23,12 @@
*/
package hudson.model;
import hudson.CloseProofOutputStream;
import hudson.remoting.RemoteOutputStream;
import hudson.util.StreamTaskListener;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.List;
......@@ -45,69 +39,43 @@ import java.util.List;
*
* @author Kohsuke Kawaguchi
*/
public class StreamBuildListener implements BuildListener, Serializable {
private PrintWriter w;
public class StreamBuildListener extends StreamTaskListener implements BuildListener {
public StreamBuildListener(OutputStream out, Charset charset) {
super(out, charset);
}
private PrintStream ps;
public StreamBuildListener(File out, Charset charset) throws IOException {
super(out, charset);
}
public StreamBuildListener(OutputStream w) {
this(new PrintStream(w));
super(w);
}
/**
* @deprecated as of 1.349
* The caller should use {@link #StreamBuildListener(OutputStream, Charset)} to pass in
* the charset and output stream separately, so that this class can handle encoding correctly.
*/
public StreamBuildListener(PrintStream w) {
this(w,null);
super(w);
}
public StreamBuildListener(PrintStream w, Charset charset) {
this.ps = w;
// unless we auto-flash, PrintStream will use BufferedOutputStream internally,
// and break ordering
this.w = new PrintWriter(new BufferedWriter(
charset==null ? new OutputStreamWriter(w) : new OutputStreamWriter(w,charset)), true);
super(w,charset);
}
public void started(List<Cause> causes) {
PrintStream l = getLogger();
if (causes==null || causes.isEmpty())
w.println("Started");
l.println("Started");
else for (Cause cause : causes) {
w.println(cause.getShortDescription());
l.println(cause.getShortDescription());
}
}
public PrintStream getLogger() {
return ps;
}
public PrintWriter error(String msg) {
w.println("ERROR: "+msg);
return w;
}
public PrintWriter error(String format, Object... args) {
return error(String.format(format,args));
}
public PrintWriter fatalError(String msg) {
w.println("FATAL: "+msg);
return w;
}
public PrintWriter fatalError(String format, Object... args) {
return fatalError(String.format(format,args));
}
public void finished(Result result) {
w.println("Finished: "+result);
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeObject(new RemoteOutputStream(new CloseProofOutputStream(ps)));
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
ps = new PrintStream((OutputStream)in.readObject(),true);
w = new PrintWriter(ps,true);
getLogger().println("Finished: "+result);
}
private static final long serialVersionUID = 1L;
......
......@@ -23,9 +23,11 @@
*/
package hudson.model;
import hudson.console.ConsoleNote;
import hudson.util.NullStream;
import hudson.util.StreamTaskListener;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.Formatter;
......@@ -60,6 +62,13 @@ public interface TaskListener {
*/
PrintStream getLogger();
/**
* Annotates the current position in the output log by using the given annotation.
* If the implementation doesn't support annotated output log, this method might be no-op.
* @since 1.349
*/
void annotate(ConsoleNote ann) throws IOException;
/**
* An error in the build.
*
......
......@@ -200,15 +200,12 @@ public abstract class ChangeLogSet<T extends ChangeLogSet.Entry> implements Iter
*
* @return never null.
*/
public String getPath();
String getPath();
/**
* Return whether the file is new/modified/deleted
*
* @return EditType
* @see EditType
*/
public EditType getEditType();
EditType getEditType();
}
}
......@@ -33,6 +33,7 @@ import hudson.Util;
import hudson.model.*;
import hudson.remoting.Callable;
import hudson.slaves.NodeSpecific;
import hudson.tasks._ant.AntConsoleAnnotator;
import hudson.tools.ToolDescriptor;
import hudson.tools.ToolInstallation;
import hudson.tools.DownloadFromUrlInstaller;
......@@ -205,7 +206,13 @@ public class Ant extends Builder {
long startTime = System.currentTimeMillis();
try {
int r = launcher.launch().cmds(args).envs(env).stdout(listener).pwd(buildFilePath.getParent()).join();
AntConsoleAnnotator aca = new AntConsoleAnnotator(listener.getLogger(),build.getCharset());
int r;
try {
r = launcher.launch().cmds(args).envs(env).stdout(aca).pwd(buildFilePath.getParent()).join();
} finally {
aca.forceEol();
}
return r==0;
} catch (IOException e) {
Util.displayIOException(e,listener);
......
/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, 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 hudson.tasks._ant;
import hudson.console.LineTransformationOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
/**
* Filter {@link OutputStream} that places an annotation that marks Ant target execution.
*
* @author Kohsuke Kawaguchi
* @sine 1.349
*/
public class AntConsoleAnnotator extends LineTransformationOutputStream {
private final OutputStream out;
private final Charset charset;
private boolean seenEmptyLine;
public AntConsoleAnnotator(OutputStream out, Charset charset) {
this.out = out;
this.charset = charset;
}
@Override
protected void eol(byte[] b, int len) throws IOException {
String line = charset.decode(ByteBuffer.wrap(b, 0, len)).toString();
// trim off CR/LF from the end
line = trimEOL(line);
if (seenEmptyLine && endsWith(line,':') && line.indexOf(' ')<0)
// put the annotation
new AntTargetNote().encodeTo(out);
seenEmptyLine = line.length()==0;
out.write(b,0,len);
}
private boolean endsWith(String line, char c) {
int len = line.length();
return len>0 && line.charAt(len-1)==c;
}
private String trimEOL(String line) {
int slen = line.length();
while (slen>0) {
char ch = line.charAt(slen-1);
if (ch=='\r' || ch=='\n') {
slen--;
continue;
}
break;
}
line = line.substring(0,slen);
return line;
}
@Override
public void close() throws IOException {
super.close();
out.close();
}
}
/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, 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 hudson.tasks._ant;
import hudson.Extension;
import hudson.MarkupText;
import hudson.console.ConsoleNote;
import hudson.console.ConsoleAnnotationDescriptor;
import hudson.console.ConsoleAnnotator;
import java.util.regex.Pattern;
/**
* Marks the log line "TARGET:" that And uses to mark the beginning of the new target.
* @sine 1.349
*/
public final class AntTargetNote extends ConsoleNote {
public AntTargetNote() {
}
@Override
public ConsoleAnnotator annotate(Object context, MarkupText text, int charPos) {
// still under development. too early to put into production
if (!ENABLED) return null;
MarkupText.SubText t = text.findToken(Pattern.compile("^[^:]+(?=:)"));
if (t!=null)
t.addMarkup(0,t.length(),"<b class=ant-target>","</b>");
return null;
}
@Extension
public static final class DescriptorImpl extends ConsoleAnnotationDescriptor {
public String getDisplayName() {
return "Ant targets";
}
}
public static boolean ENABLED = Boolean.getBoolean(AntTargetNote.class.getName()+".enabled");
}
package hudson.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* {@link ByteArrayOutputStream} with access to its raw buffer.
* @since 1.349
*/
public class ByteArrayOutputStream2 extends ByteArrayOutputStream {
public ByteArrayOutputStream2() {
}
public ByteArrayOutputStream2(int size) {
super(size);
}
public byte[] getBuffer() {
return buf;
}
/**
* Reads the given {@link InputStream} completely into the buffer.
*/
public void readFrom(InputStream is) throws IOException {
while(true) {
if(count==buf.length) {
// realllocate
byte[] data = new byte[buf.length*2];
System.arraycopy(buf,0,data,0,buf.length);
buf = data;
}
int sz = is.read(buf,count,buf.length-count);
if(sz<0) return;
count += sz;
}
}
}
package hudson.util;
import java.io.IOException;
import java.io.OutputStream;
/**
* {@link OutputStream} that blocks {@link #flush()} method.
* @author Kohsuke Kawaguchi
* @since 1.349
*/
public class FlushProofOutputStream extends DelegatingOutputStream {
public FlushProofOutputStream(OutputStream out) {
super(out);
}
@Override
public void flush() throws IOException {
}
}
......@@ -54,4 +54,14 @@ public class IOUtils extends org.apache.commons.io.IOUtils {
throw new IOException("Failed to create a directory at "+dir);
}
/**
* Fully skips the specified size from the given input stream
* @since 1.349
*/
public static InputStream skip(InputStream in, long size) throws IOException {
while (size>0)
size -= in.skip(size);
return in;
}
}
......@@ -24,6 +24,7 @@
package hudson.util;
import hudson.console.ConsoleNote;
import hudson.model.TaskListener;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
......@@ -65,6 +66,10 @@ public class LogTaskListener implements TaskListener {
return delegate.fatalError(format, args);
}
public void annotate(ConsoleNote ann) {
// no annotation support
}
public void close() {
delegate.getLogger().close();
}
......
......@@ -24,11 +24,14 @@
package hudson.util;
import hudson.CloseProofOutputStream;
import hudson.console.ConsoleNote;
import hudson.console.HudsonExceptionNote;
import hudson.model.TaskListener;
import hudson.remoting.RemoteOutputStream;
import org.kohsuke.stapler.framework.io.WriterOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
......@@ -38,9 +41,11 @@ import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import org.kohsuke.stapler.framework.io.WriterOutputStream;
import java.nio.charset.Charset;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* {@link TaskListener} that generates output into a single stream.
......@@ -50,32 +55,53 @@ import org.kohsuke.stapler.framework.io.WriterOutputStream;
*
* @author Kohsuke Kawaguchi
*/
public final class StreamTaskListener implements TaskListener, Serializable {
public class StreamTaskListener implements TaskListener, Serializable, Closeable {
private PrintStream out;
private Charset charset;
/**
* @deprecated as of 1.349
* The caller should use {@link #StreamTaskListener(OutputStream, Charset)} to pass in
* the charset and output stream separately, so that this class can handle encoding correctly.
*/
public StreamTaskListener(PrintStream out) {
this.out = out;
this(out,null);
}
public StreamTaskListener(OutputStream out) {
this(new PrintStream(out));
this(out,null);
}
public StreamTaskListener(File out) throws FileNotFoundException {
public StreamTaskListener(OutputStream out, Charset charset) {
try {
this.out = charset==null ? new PrintStream(out,false) : new PrintStream(out,false,charset.name());
this.charset = charset;
} catch (UnsupportedEncodingException e) {
// it's not very pretty to do this, but otherwise we'd have to touch too many call sites.
throw new Error(e);
}
}
public StreamTaskListener(File out) throws IOException {
this(out,null);
}
public StreamTaskListener(File out, Charset charset) 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
this(new FileOutputStream(out));
this(new FileOutputStream(out),charset);
}
public StreamTaskListener(Writer w) {
public StreamTaskListener(Writer w) throws IOException {
this(new WriterOutputStream(w));
}
/**
* Creates {@link StreamTaskListener} that swallows the result.
* @deprecated as of 1.349
* Use {@link #NULL}
*/
public StreamTaskListener() {
public StreamTaskListener() throws IOException {
this(new NullStream());
}
......@@ -83,9 +109,24 @@ public final class StreamTaskListener implements TaskListener, Serializable {
return out;
}
public PrintWriter error(String msg) {
private PrintWriter _error(String prefix, String msg) {
out.print(prefix);
out.println(msg);
return new PrintWriter(new OutputStreamWriter(out),true);
// the idiom in Hudson is to use the returned writer for writing stack trace,
// so put the marker here to indicate an exception. if the stack trace isn't actually written,
// HudsonExceptionNote.annotate recovers gracefully.
try {
annotate(new HudsonExceptionNote());
} catch (IOException e) {
// for signature compatibility, we have to swallow this error
}
return new PrintWriter(
charset!=null ? new OutputStreamWriter(out,charset) : new OutputStreamWriter(out),true);
}
public PrintWriter error(String msg) {
return _error("ERROR: ",msg);
}
public PrintWriter error(String format, Object... args) {
......@@ -93,24 +134,46 @@ public final class StreamTaskListener implements TaskListener, Serializable {
}
public PrintWriter fatalError(String msg) {
return error(msg);
return error("FATAL: ",msg);
}
public PrintWriter fatalError(String format, Object... args) {
return fatalError(String.format(format,args));
}
public void annotate(ConsoleNote ann) throws IOException {
ann.encodeTo(out);
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeObject(new RemoteOutputStream(new CloseProofOutputStream(this.out)));
out.writeObject(charset==null? null : charset.name());
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
out = new PrintStream((OutputStream)in.readObject(),true);
String name = (String)in.readObject();
charset = name==null ? null : Charset.forName(name);
}
public void close() {
public void close() throws IOException {
out.close();
}
/**
* Closes this listener and swallows any exceptions, if raised.
*
* @since 1.349
*/
public void closeQuietly() {
try {
close();
} catch (IOException e) {
LOGGER.log(Level.WARNING,"Failed to close",e);
}
}
private static final long serialVersionUID = 1L;
private static final Logger LOGGER = Logger.getLogger(StreamTaskListener.class.getName());
}
package hudson.util;
import org.apache.commons.codec.binary.Base64;
import java.io.DataInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Filter InputStream that decodes base64 without doing any buffering.
*
* <p>
* This is slower implementation, but it won't consume unnecessary bytes from the underlying {@link InputStream},
* allowing the reader to switch between the unencoded bytes and base64 bytes.
*
* @author Kohsuke Kawaguchi
* @since 1.349
*/
public class UnbufferedBase64InputStream extends FilterInputStream {
private byte[] encoded = new byte[4];
private byte[] decoded;
private int pos;
private final DataInputStream din;
public UnbufferedBase64InputStream(InputStream in) {
super(in);
din = new DataInputStream(in);
// initial placement to force the decoding in the next read()
pos = 4;
decoded = encoded;
}
@Override
public int read() throws IOException {
if (decoded.length==0)
return -1; // EOF
if (pos==decoded.length) {
din.readFully(encoded);
decoded = Base64.decodeBase64(encoded);
pos = 0;
}
return (decoded[pos++])&0xFF;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int i;
for (i=0; i<len; i++) {
int ch = read();
if (ch<0) break;
b[off+i] = (byte)ch;
}
return i==0 ? -1 : i;
}
}
......@@ -75,7 +75,7 @@ THE SOFTWARE.
<f:radio name="slaveAgentPortType" value="fixed" id="sat.fixed"
checked="${it.slaveAgentPort gt 0}" onclick="$('sat.port').disabled=false"/>
<label for="sat.fixed">${%Fixed}</label> :
<input type="text" class="setting-input number" name="slaveAgentPort" id="sat.port"
<input type="text" class="number" name="slaveAgentPort" id="sat.port"
value="${it.slaveAgentPort gt 0 ? it.slaveAgentPort : null}"
disabled="${it.slaveAgentPort gt 0 ? null : 'true'}"/>
......
......@@ -45,13 +45,16 @@ THE SOFTWARE.
${%skipSome(offset/1024,"consoleFull")}
</j:when>
<j:otherwise>
<j:set var="offset" value="0" />
<j:set var="offset" value="${0}" />
</j:otherwise>
</j:choose>
<j:out value="${h.generateConsoleAnnotationScript()}"/>
<j:choose>
<!-- Do progressive console output -->
<j:when test="${it.isLogUpdated()}">
<pre id="out"></pre>
<pre id="out" />
<div id="spinner">
<img src="${imagesURL}/spinner.gif" alt="" />
</div>
......@@ -59,9 +62,10 @@ THE SOFTWARE.
</j:when>
<!-- output is completed now. -->
<j:otherwise>
<j:set var="logReader" value="${it.logReader}" />
<j:set var="unused" value="${logReader.skip(offset)}" />
<pre><st:copyStream reader="${logReader}"/></pre>
<pre>
<st:getOutput var="output" />
<j:whitespace>${it.writeLogTo(offset,output)}</j:whitespace>
</pre>
</j:otherwise>
</j:choose>
</l:main-panel>
......
<!--
The MIT License
Copyright (c) 2004-2010, Sun Microsystems, 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.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt" xmlns:p="/lib/hudson/project">
<l:ajax>
<table class='pane' id='console-outline'>
<tr>
<td class='pane-header'>${%Executed Ant Targets}</td>
</tr>
<tr>
<td id='console-outline-body' />
</tr>
</table>
</l:ajax>
</j:jelly>
\ No newline at end of file
(function() {
// created on demand
var outline = null;
var loading = false;
var queue = []; // ant targets are queued up until we load outline.
function loadOutline() {
if (outline!=null) return false; // already loaded
if (!loading) {
loading = true;
var u = new Ajax.Updater(document.getElementById("side-panel"),
rootURL+"/descriptor/hudson.tasks._ant.AntTargetNote/outline",
{insertion: Insertion.Bottom, onComplete: function() {
if (!u.success()) return; // we can't us onSuccess because that kicks in before onComplete
outline = document.getElementById("console-outline-body");
loading = false;
queue.each(handle);
}});
}
return true;
}
function handle(e) {
if (loadOutline()) {
queue.push(e);
} else {
var id = "ant-target-"+(iota++);
var a = document.createElement("a");
a.setAttribute("name",id);
e.appendChild(a);
outline.appendChild(parseHtml("<li><a href='#"+id+"'>"+e.innerHTML+"</a></li>"))
}
}
Behaviour.register({
// insert <a name="..."> for each Ant target and put it into the outline
"b.ant-target" : function(e) {
handle(e);
}
});
}());
......@@ -40,21 +40,36 @@ THE SOFTWARE.
var scroller = new AutoScroller(document.body);
<j:if test="${requestScope.progressiveTextScript==null}">
<j:set target="${requestScope}" property="progressiveTextScript" value="initialized" />
<!-- fetches the latest update from the server -->
<!--
fetches the latest update from the server
@param e
DOM node that gets the text appended to
@param href
Where to retrieve additional text from
-->
function fetchNext(e,href) {
var headers = {};
if (e.consoleAnnotator!=undefined)
headers["X-ConsoleAnnotator"] = e.consoleAnnotator;
new Ajax.Request(href,{
method: "post",
parameters: {"start":e.fetchedBytes},
requestHeaders: headers,
onComplete: function(rsp,_) {
<!-- append text and do autoscroll if applicable-->
var stickToBottom = scroller.isSticking();
var text = rsp.responseText;
if(text!="") {
e.appendChild(document.createTextNode(text));
var p = document.createElement("DIV");
p.innerHTML = text;
e.appendChild(p);
if(stickToBottom) scroller.scrollToBottom();
}
e.fetchedBytes = rsp.getResponseHeader("X-Text-Size");
e.fetchedBytes = rsp.getResponseHeader("X-Text-Size");
e.consoleAnnotator = rsp.getResponseHeader("X-ConsoleAnnotator");
if(rsp.getResponseHeader("X-More-Data")=="true")
setTimeout(function(){fetchNext(e,href);},1000);
<j:if test="${spinner!=null}">
......
......@@ -70,5 +70,20 @@ public class MarkupTextTest extends TestCase {
assertEquals("$9AAA$9 test $9AAA$9",text.toString());
}
/**
* Start/end tag nesting should be correct regardless of the order tags are added.
*/
public void testAdjacent() {
MarkupText text = new MarkupText("abcdef");
text.addMarkup(0,3,"$","$");
text.addMarkup(3,6,"#","#");
assertEquals("$abc$#def#",text.toString());
text = new MarkupText("abcdef");
text.addMarkup(3,6,"#","#");
text.addMarkup(0,3,"$","$");
assertEquals("$abc$#def#",text.toString());
}
private static final Pattern pattern = Pattern.compile("issue #([0-9]+)");
}
......@@ -23,6 +23,7 @@
*/
package hudson.maven;
import hudson.console.ConsoleNote;
import hudson.model.BuildListener;
import hudson.model.Cause;
import hudson.model.Result;
......@@ -131,7 +132,12 @@ final class SplittableBuildListener implements BuildListener, Serializable {
return new PrintWriter(logger);
}
private Object writeReplace() {
public void annotate(ConsoleNote ann) throws IOException {
core.annotate(ann);
}
private Object writeReplace() throws IOException {
// TODO: fix this before HUDSON-2137 integration
return new StreamBuildListener(logger);
}
......
......@@ -33,7 +33,7 @@ import java.lang.reflect.Proxy;
/**
* {@link ObjectInputStream} that uses a specific class loader.
*/
class ObjectInputStreamEx extends ObjectInputStream {
public class ObjectInputStreamEx extends ObjectInputStream {
private final ClassLoader cl;
public ObjectInputStreamEx(InputStream in, ClassLoader cl) throws IOException {
......
......@@ -34,8 +34,10 @@ import hudson.EnvVars;
import hudson.ExtensionList;
import hudson.DescriptorExtensionList;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.Computer;
import hudson.model.Executor;
import hudson.model.Job;
import hudson.model.Queue.Executable;
import hudson.slaves.ComputerLauncher;
import hudson.tools.ToolProperty;
......@@ -97,6 +99,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.jar.Manifest;
import java.util.logging.Filter;
import java.util.logging.Level;
......@@ -160,7 +163,7 @@ import java.util.concurrent.CountDownLatch;
public abstract class HudsonTestCase extends TestCase implements RootAction {
public Hudson hudson;
protected final TestEnvironment env = new TestEnvironment();
protected final TestEnvironment env = new TestEnvironment(this);
protected HudsonHomeLoader homeLoader = HudsonHomeLoader.NEW;
/**
* TCP/IP port that the server is listening on.
......@@ -717,6 +720,14 @@ public abstract class HudsonTestCase extends TestCase implements RootAction {
return r;
}
public <R extends Run> R assertBuildStatusSuccess(Future<? extends R> r) throws Exception {
return assertBuildStatusSuccess(r.get());
}
public <J extends AbstractProject<J,R>,R extends AbstractBuild<J,R>> R buildAndAssertSuccess(J job) throws Exception {
return assertBuildStatusSuccess(job.scheduleBuild2(0));
}
/**
* Asserts that the console output of the build contains the given substring.
*/
......
/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, 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 org.jvnet.hudson.test;
/**
* Lock mechanism to let multiple threads execute phases sequentially.
*
* @author Kohsuke Kawaguchi
*/
public class SequenceLock {
/**
* Currently executing phase N.
*/
private int n;
/**
* This thread is executing the phase
*/
private Thread t;
private boolean aborted;
/**
* Blocks until all the previous phases are completed, and returns when the specified phase <i>i</i> is started.
* If the calling thread was executing an earlier phase, that phase is marked as completed.
*
* @throws IllegalStateException
* if the sequential lock protocol is aborted, or the thread that owns the current phase has quit.
*/
public synchronized void phase(int i) throws InterruptedException {
done(); // mark the previous phase done
while (i!=n) {
if (aborted)
throw new IllegalStateException("SequenceLock aborted");
if (t!=null && !t.isAlive())
throw new IllegalStateException("Owner thread of the current phase has quit"+t);
if (i<n)
throw new IllegalStateException("Phase "+i+" is already completed");
wait();
}
t = Thread.currentThread();
}
/**
* Marks the current phase completed that the calling thread was executing.
*
* <p>
* This is only necessary when the thread exits the last phase, as {@link #phase(int)} call implies the
* {@link #done()} call.
*/
public synchronized void done() {
if (t==Thread.currentThread()) {
// phase N done
n++;
t = null;
notifyAll();
}
}
/**
* Tell all the threads that this sequencing was aborted.
* Everyone waiting for future phases will receive an error.
*
* <p>
* Calling this method from the finally block prevents a dead lock if one of the participating thread
* aborts with an exception, as without the explicit abort operation, other threads will block forever
* for a phase that'll never come.
*/
public synchronized void abort() {
aborted = true;
notifyAll();
}
}
......@@ -30,8 +30,17 @@ package org.jvnet.hudson.test;
* @author Kohsuke Kawaguchi
*/
public class TestEnvironment {
/**
* Current test case being run.
*/
public final HudsonTestCase testCase;
public final TemporaryDirectoryAllocator temporaryDirectoryAllocator = new TemporaryDirectoryAllocator();
public TestEnvironment(HudsonTestCase testCase) {
this.testCase = testCase;
}
/**
* Associates (or pin down) this {@link TestEnvironment} to the current thread, so that
* from within the test you can access this object without referring to any context.
......
/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, 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 org.jvnet.hudson.test;
import hudson.Extension;
import net.java.sezpoz.Indexable;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Works like {@link Extension} except used for inserting extensions during unit tests.
*
* <p>
* This annotation must be used on a method/field of a test case class, or an nested type of the test case.
* The extensions are activated only when the outer test class is being run.
*
* @author Kohsuke Kawaguchi
* @see TestExtensionLoader
*/
@Indexable
@Retention(RUNTIME)
@Target({TYPE, FIELD, METHOD})
@Documented
public @interface TestExtension {
/**
* To make this extension only active for one test case, specify the test method name.
* Otherwise, leave it unspecified and it'll apply to all the test methods defined in the same class.
*
* <h2>Example</h2>
* <pre>
* class FooTest extends HudsonTestCase {
* public void test1() { ... }
* public void test2() { ... }
*
* // this only kicks in during test1
* &#64;TestExtension("test1")
* class Foo extends ConsoleAnnotator { ... }
*
* // this kicks in both for test1 and test2
* &#64;TestExtension
* class Bar extends ConsoleAnnotator { ... }
* }
* </pre>
*/
String value() default "";
}
/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, 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 org.jvnet.hudson.test;
import hudson.Extension;
import hudson.ExtensionFinder;
import hudson.model.Hudson;
import net.java.sezpoz.Index;
import net.java.sezpoz.IndexItem;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Loads {@link TestExtension}s.
*
* @author Kohsuke Kawaguchi
*/
@Extension
public class TestExtensionLoader extends ExtensionFinder {
@Override
public <T> Collection<T> findExtensions(Class<T> type, Hudson hudson) {
TestEnvironment env = TestEnvironment.get();
List<T> result = new ArrayList<T>();
ClassLoader cl = hudson.getPluginManager().uberClassLoader;
for (IndexItem<TestExtension,Object> item : Index.load(TestExtension.class, Object.class, cl)) {
try {
AnnotatedElement e = item.element();
Class<?> extType;
if (e instanceof Class) {
extType = (Class) e;
if (!isActive(env, extType)) continue;
} else
if (e instanceof Field) {
Field f = (Field) e;
if (!f.getDeclaringClass().isInstance(env.testCase))
continue; // not executing the enclosing test
extType = f.getType();
} else
if (e instanceof Method) {
Method m = (Method) e;
if (!m.getDeclaringClass().isInstance(env.testCase))
continue; // not executing the enclosing test
extType = m.getReturnType();
} else
throw new AssertionError();
String testName = item.annotation().value();
if (testName.length()>0 && !env.testCase.getName().equals(testName))
continue; // doesn't apply to this test
if(type.isAssignableFrom(extType)) {
Object instance = item.instance();
if(instance!=null)
result.add(type.cast(instance));
}
} catch (InstantiationException e) {
LOGGER.log(Level.WARNING, "Failed to load "+item.className(),e);
}
}
return result;
}
private boolean isActive(TestEnvironment env, Class<?> extType) {
for (Class<?> outer = extType; outer!=null; outer=outer.getEnclosingClass())
if (outer.isInstance(env.testCase))
return true; // enclosed
return false;
}
private static final Logger LOGGER = Logger.getLogger(TestExtensionLoader.class.getName());
}
......@@ -147,7 +147,7 @@ public class MatrixProjectTest extends HudsonTestCase {
p.getBuildersList().add(new Shell("touch p"));
p.getPublishersList().add(new ArtifactArchiver("p",null,false));
p.getPublishersList().add(new Fingerprinter("",true));
assertBuildStatusSuccess(p.scheduleBuild2(0).get());
buildAndAssertSuccess(p);
}
void assertRectangleTable(MatrixProject p) {
......
......@@ -39,7 +39,7 @@ public class AbstractBuildTest extends GroovyHudsonTestCase {
def builder = new CaptureEnvironmentBuilder();
project.getBuildersList().add(builder);
assertBuildStatusSuccess(project.scheduleBuild2(0).get());
buildAndAssertSuccess(project);
def envVars = builder.getEnvVars();
Assert.assertEquals("value", envVars.get("KEY1"));
......@@ -56,7 +56,7 @@ public class AbstractBuildTest extends GroovyHudsonTestCase {
p.buildersList.add(builder { builder,launcher,BuildListener listener ->
listener.logger.println(out);
})
def b = assertBuildStatusSuccess(p.scheduleBuild2(0).get());
def b = buildAndAssertSuccess(p);
Page rsp = createWebClient().goTo("${b.url}/consoleText", "text/plain");
println "Output:\n"+rsp.webResponse.contentAsString
assertTrue(rsp.webResponse.contentAsString.contains(out));
......
package hudson.console;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.WebRequestSettings;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import hudson.Launcher;
import hudson.MarkupText;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import hudson.model.Run;
import org.jvnet.hudson.test.HudsonTestCase;
import org.jvnet.hudson.test.SequenceLock;
import org.jvnet.hudson.test.TestBuilder;
import org.jvnet.hudson.test.TestExtension;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Future;
/**
* @author Kohsuke Kawaguchi
*/
public class ConsoleAnnotatorTest extends HudsonTestCase {
/**
* Let the build complete, and see if stateless {@link ConsoleAnnotator} annotations happen as expected.
*/
public void testCompletedStatelessLogAnnotation() throws Exception {
FreeStyleProject p = createFreeStyleProject();
p.getBuildersList().add(new TestBuilder() {
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
listener.getLogger().println("---");
listener.getLogger().println("ooo");
listener.getLogger().println("ooo");
return true;
}
});
FreeStyleBuild b = buildAndAssertSuccess(p);
// make sure we see the annotation
HtmlPage rsp = createWebClient().getPage(b, "console");
assertEquals(1,rsp.selectNodes("//B[@class='demo']").size());
// there should be two 'ooo's
assertEquals(3,rsp.asXml().split("ooo").length);
}
/**
* Only annotates the first occurrence of "ooo".
*/
@TestExtension("testCompletedStatelessLogAnnotation")
public static final ConsoleAnnotatorFactory DEMO_ANNOTATOR = new ConsoleAnnotatorFactory() {
public ConsoleAnnotator newInstance(Object context) {
return new DemoAnnotator();
}
};
public static class DemoAnnotator extends ConsoleAnnotator<Object> {
@Override
public ConsoleAnnotator annotate(Object build, MarkupText text) {
if (text.getText().equals("ooo\n")) {
text.addMarkup(0,3,"<b class=demo>","</b>");
return null;
}
return this;
}
}
class ProgressiveLogClient {
WebClient wc;
Run run;
String consoleAnnotator;
String start;
private Page p;
ProgressiveLogClient(WebClient wc, Run r) {
this.wc = wc;
this.run = r;
}
String next() throws IOException {
WebRequestSettings req = new WebRequestSettings(new URL(getURL() + run.getUrl() + "/progressiveLog"+(start!=null?"?start="+start:"")));
Map headers = new HashMap();
if (consoleAnnotator!=null)
headers.put("X-ConsoleAnnotator",consoleAnnotator);
req.setAdditionalHeaders(headers);
p = wc.getPage(req);
consoleAnnotator = p.getWebResponse().getResponseHeaderValue("X-ConsoleAnnotator");
start = p.getWebResponse().getResponseHeaderValue("X-Text-Size");
return p.getWebResponse().getContentAsString();
}
}
/**
* Tests the progressive output by making sure that the state of {@link ConsoleAnnotator}s are
* maintained across different progressiveLog calls.
*/
public void testProgressiveOutput() throws Exception {
final SequenceLock lock = new SequenceLock();
WebClient wc = createWebClient();
FreeStyleProject p = createFreeStyleProject();
p.getBuildersList().add(new TestBuilder() {
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
lock.phase(0);
// make sure the build is now properly started
lock.phase(2);
listener.getLogger().println("line1");
lock.phase(4);
listener.getLogger().println("line2");
lock.done();
return true;
}
});
Future<FreeStyleBuild> f = p.scheduleBuild2(0);
lock.phase(1);
FreeStyleBuild b = p.getBuildByNumber(1);
ProgressiveLogClient plc = new ProgressiveLogClient(wc,b);
// the page should contain some output indicating the build has started why and etc.
plc.next();
lock.phase(3);
assertEquals("<b tag=1>line1</b>\r\n",plc.next());
// the new invocation should start from where the previous call left off
lock.phase(5);
assertEquals("<b tag=2>line2</b>\r\n",plc.next());
// should complete successfully
assertBuildStatusSuccess(f);
}
@TestExtension("testProgressiveOutput")
public static final ConsoleAnnotatorFactory STATEFUL_ANNOTATOR = new ConsoleAnnotatorFactory() {
public ConsoleAnnotator newInstance(Object context) {
return new StatefulAnnotator();
}
};
public static class StatefulAnnotator extends ConsoleAnnotator<Object> {
int n=1;
public ConsoleAnnotator annotate(Object build, MarkupText text) {
if (text.getText().startsWith("line"))
text.addMarkup(0,5,"<b tag="+(n++)+">","</b>");
return this;
}
}
/**
* Place {@link ConsoleNote}s and make sure it works.
*/
public void testConsoleAnnotation() throws Exception {
final SequenceLock lock = new SequenceLock();
WebClient wc = createWebClient();
FreeStyleProject p = createFreeStyleProject();
p.getBuildersList().add(new TestBuilder() {
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
lock.phase(0);
// make sure the build is now properly started
lock.phase(2);
listener.getLogger().print("abc");
listener.annotate(new DollarMark());
listener.getLogger().println("def");
lock.phase(4);
listener.getLogger().print("123");
listener.annotate(new DollarMark());
listener.getLogger().print("456");
listener.annotate(new DollarMark());
listener.getLogger().println("789");
lock.done();
return true;
}
});
Future<FreeStyleBuild> f = p.scheduleBuild2(0);
// discard the initial header portion
lock.phase(1);
FreeStyleBuild b = p.getBuildByNumber(1);
ProgressiveLogClient plc = new ProgressiveLogClient(wc,b);
plc.next();
lock.phase(3);
assertEquals("abc$$$def\r\n",plc.next());
lock.phase(5);
assertEquals("123$$$456$$$789\r\n",plc.next());
// should complete successfully
assertBuildStatusSuccess(f);
}
/**
* Places a triple dollar mark at the specified position.
*/
public static final class DollarMark extends ConsoleNote<Run> {
public ConsoleAnnotator annotate(Run build, MarkupText text, int charPos) {
text.addMarkup(charPos,"$$$");
return null;
}
@TestExtension
public static final class DescriptorImpl extends ConsoleAnnotationDescriptor {
public String getDisplayName() {
return "Dollar mark";
}
}
}
/**
* script.js defined in the annotator needs to be incorporated into the console page.
*/
public void testScriptInclusion() throws Exception {
FreeStyleProject p = createFreeStyleProject();
FreeStyleBuild b = buildAndAssertSuccess(p);
HtmlPage html = createWebClient().getPage(b, "console");
// verify that there's an element inserted by the script
assertNotNull(html.getElementById("inserted-by-test1"));
assertNotNull(html.getElementById("inserted-by-test2"));
}
public static final class JustToIncludeScript extends ConsoleNote<Object> {
public ConsoleAnnotator annotate(Object build, MarkupText text, int charPos) {
return null;
}
@TestExtension("testScriptInclusion")
public static final class DescriptorImpl extends ConsoleAnnotationDescriptor {
public String getDisplayName() {
return "just to include a script";
}
}
}
@TestExtension("testScriptInclusion")
public static final class JustToIncludeScriptAnnotator extends ConsoleAnnotatorFactory {
public ConsoleAnnotator newInstance(Object context) {
return null;
}
}
}
package hudson.console;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import org.jvnet.hudson.test.HudsonTestCase;
import org.jvnet.hudson.test.TestBuilder;
import java.io.IOException;
/**
* @author Kohsuke Kawaguchi
*/
public class ExceptionAnnotationTest extends HudsonTestCase {
public void test1() throws Exception {
FreeStyleProject p = createFreeStyleProject();
p.getBuildersList().add(new TestBuilder() {
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
new Throwable().printStackTrace(listener.error("Injecting a failure"));
return true;
}
});
FreeStyleBuild b = buildAndAssertSuccess(p);
createWebClient().getPage(b,"console");
// TODO: check if the annotation is placed
// TODO: test an exception with cause and message
// interactiveBreak();
}
}
......@@ -35,7 +35,7 @@ public class MavenBuildTest extends HudsonTestCase {
MavenModuleSet m = createMavenProject();
m.getReporters().add(new TestReporter());
m.setScm(new ExtractResourceSCM(getClass().getResource("HUDSON-4192.zip")));
assertBuildStatusSuccess(m.scheduleBuild2(0).get());
buildAndAssertSuccess(m);
}
/**
......@@ -82,12 +82,12 @@ public class MavenBuildTest extends HudsonTestCase {
setJavaNetCredential(scm);
m.setScm(scm);
assertBuildStatusSuccess(m.scheduleBuild2(0).get());
buildAndAssertSuccess(m);
m.setAggregatorStyleBuild(false);
// run module builds
assertBuildStatusSuccess(m.getModule("test$module1").scheduleBuild2(0).get());
assertBuildStatusSuccess(m.getModule("test$module1").scheduleBuild2(0).get());
buildAndAssertSuccess(m.getModule("test$module1"));
buildAndAssertSuccess(m.getModule("test$module1"));
}
}
......@@ -26,7 +26,7 @@ public class MavenEmptyModuleTest extends HudsonTestCase {
MavenModuleSet m = createMavenProject();
m.getReporters().add(new TestReporter());
m.setScm(new ExtractResourceSCM(getClass().getResource("maven-empty-mod.zip")));
assertBuildStatusSuccess(m.scheduleBuild2(0).get());
buildAndAssertSuccess(m);
}
private static class TestReporter extends MavenReporter {
......
......@@ -29,8 +29,8 @@ public class MavenMultiModuleTest extends HudsonTestCase {
MavenModuleSet m = createMavenProject();
m.getReporters().add(new TestReporter());
m.setScm(new ExtractResourceSCM(getClass().getResource("maven-multimod.zip")));
assertFalse("MavenModuleSet.isNonRecursive() should be false", m.isNonRecursive());
assertBuildStatusSuccess(m.scheduleBuild2(0).get());
assertFalse("MavenModuleSet.isNonRecursive() should be false", m.isNonRecursive());
buildAndAssertSuccess(m);
}
public void testIncrementalMultiModMaven() throws Exception {
......@@ -40,11 +40,11 @@ public class MavenMultiModuleTest extends HudsonTestCase {
m.setScm(new ExtractResourceWithChangesSCM(getClass().getResource("maven-multimod.zip"),
getClass().getResource("maven-multimod-changes.zip")));
assertBuildStatusSuccess(m.scheduleBuild2(0).get());
buildAndAssertSuccess(m);
// Now run a second build with the changes.
m.setIncrementalBuild(true);
assertBuildStatusSuccess(m.scheduleBuild2(0).get());
buildAndAssertSuccess(m);
MavenModuleSetBuild pBuild = m.getLastBuild();
ExtractChangeLogSet changeSet = (ExtractChangeLogSet) pBuild.getChangeSet();
......@@ -73,12 +73,11 @@ public class MavenMultiModuleTest extends HudsonTestCase {
configureDefaultMaven("apache-maven-2.2.1", MavenInstallation.MAVEN_21);
MavenModuleSet m = createMavenProject();
m.getReporters().add(new TestReporter());
m.setScm(new ExtractResourceWithChangesSCM(getClass().getResource("maven-multimod.zip"),
getClass().getResource("maven-multimod-changes.zip")));
m.setIncrementalBuild(true);
m.setScm(new ExtractResourceWithChangesSCM(getClass().getResource("maven-multimod.zip"),
getClass().getResource("maven-multimod-changes.zip")));
assertBuildStatusSuccess(m.scheduleBuild2(0).get());
m.setIncrementalBuild(true);
buildAndAssertSuccess(m);
}
/**
......@@ -92,7 +91,7 @@ public class MavenMultiModuleTest extends HudsonTestCase {
m.getReporters().add(new TestReporter());
m.setScm(new ExtractResourceSCM(getClass().getResource("maven-multimod.zip")));
assertBuildStatusSuccess(m.scheduleBuild2(0).get());
buildAndAssertSuccess(m);
MavenModuleSetBuild pBuild = m.getLastBuild();
......
......@@ -26,7 +26,7 @@ public class MavenOptsTest extends HudsonTestCase {
m.setGoals("validate");
m.setAssignedLabel(createSlave(null, new EnvVars("MAVEN_OPTS", "-Dhudson.mavenOpt.test=foo")).getSelfLabel());
assertBuildStatusSuccess(m.scheduleBuild2(0).get());
buildAndAssertSuccess(m);
assertLogContains("[hudson.mavenOpt.test=foo]", m.getLastBuild());
}
......@@ -40,7 +40,7 @@ public class MavenOptsTest extends HudsonTestCase {
m.setMavenOpts("-Dhudson.mavenOpt.test=bar");
m.setAssignedLabel(createSlave(null, new EnvVars("MAVEN_OPTS", "-Dhudson.mavenOpt.test=foo")).getSelfLabel());
assertBuildStatusSuccess(m.scheduleBuild2(0).get());
buildAndAssertSuccess(m);
assertLogContains("[hudson.mavenOpt.test=bar]", m.getLastBuild());
}
......@@ -54,7 +54,7 @@ public class MavenOptsTest extends HudsonTestCase {
m.setAssignedLabel(createSlave(null, new EnvVars("MAVEN_OPTS", "-Dhudson.mavenOpt.test=foo")).getSelfLabel());
m.setMavenOpts("-Dhudson.mavenOpt.test=baz");
assertBuildStatusSuccess(m.scheduleBuild2(0).get());
buildAndAssertSuccess(m);
assertLogContains("[hudson.mavenOpt.test=baz]", m.getLastBuild());
}
......@@ -67,7 +67,7 @@ public class MavenOptsTest extends HudsonTestCase {
m.setGoals("validate");
m.DESCRIPTOR.setGlobalMavenOpts("-Dhudson.mavenOpt.test=bar");
assertBuildStatusSuccess(m.scheduleBuild2(0).get());
buildAndAssertSuccess(m);
assertLogContains("[hudson.mavenOpt.test=bar]", m.getLastBuild());
}
......@@ -80,7 +80,7 @@ public class MavenOptsTest extends HudsonTestCase {
m.DESCRIPTOR.setGlobalMavenOpts("-Dhudson.mavenOpt.test=bar");
m.setMavenOpts("-Dhudson.mavenOpt.test=foo");
assertBuildStatusSuccess(m.scheduleBuild2(0).get());
buildAndAssertSuccess(m);
assertLogContains("[hudson.mavenOpt.test=foo]", m.getLastBuild());
}
......
......@@ -44,7 +44,7 @@ public class MavenProjectTest extends HudsonTestCase {
MavenModuleSet project = createSimpleProject();
project.setGoals("validate");
assertBuildStatusSuccess(project.scheduleBuild2(0).get());
buildAndAssertSuccess(project);
}
private MavenModuleSet createSimpleProject() throws Exception {
......@@ -61,7 +61,7 @@ public class MavenProjectTest extends HudsonTestCase {
project.setGoals("validate");
project.setAssignedLabel(createSlave().getSelfLabel());
assertBuildStatusSuccess(project.scheduleBuild2(0).get());
buildAndAssertSuccess(project);
}
/**
......@@ -72,7 +72,7 @@ public class MavenProjectTest extends HudsonTestCase {
MavenModuleSet project = createSimpleProject();
project.setGoals("site");
assertBuildStatusSuccess(project.scheduleBuild2(0).get());
buildAndAssertSuccess(project);
// this should succeed
HudsonTestCase.WebClient wc = new WebClient();
......
......@@ -23,11 +23,11 @@ public class MavenSnapshotTriggerTest extends HudsonTestCase {
projB.setQuietPeriod(0);
projB.setScm(new ExtractResourceSCM(getClass().getResource("maven-dep-test-B.zip")));
assertBuildStatusSuccess(projA.scheduleBuild2(0).get());
assertBuildStatusSuccess(projB.scheduleBuild2(0).get());
buildAndAssertSuccess(projA);
buildAndAssertSuccess(projB);
projA.setScm(new ExtractResourceSCM(getClass().getResource("maven-dep-test-A-changed.zip")));
assertBuildStatusSuccess(projA.scheduleBuild2(0).get());
buildAndAssertSuccess(projA);
// at this point runB2 should be in the queue, so wait until that completes.
waitUntilNoActivity();
......@@ -58,13 +58,13 @@ public class MavenSnapshotTriggerTest extends HudsonTestCase {
projC.setQuietPeriod(0);
projC.setScm(new ExtractResourceSCM(getClass().getResource("maven-dep-test-C.zip")));
assertBuildStatusSuccess(projA.scheduleBuild2(0).get());
assertBuildStatusSuccess(projB.scheduleBuild2(0).get());
assertBuildStatusSuccess(projC.scheduleBuild2(0).get());
buildAndAssertSuccess(projA);
buildAndAssertSuccess(projB);
buildAndAssertSuccess(projC);
projA.setScm(new ExtractResourceSCM(getClass().getResource("maven-dep-test-A-changed.zip")));
assertBuildStatusSuccess(projA.scheduleBuild2(0).get());
buildAndAssertSuccess(projA);
waitUntilNoActivity(); // wait until dependency build trickles down
assertEquals("Expected most recent build of second project to be #2", 2, projB.getLastBuild().getNumber());
......
......@@ -162,7 +162,7 @@ public class AbstractProjectTest extends HudsonTestCase {
final OneShotEvent sync = new OneShotEvent();
final FreeStyleProject p = createFreeStyleProject();
FreeStyleBuild b1 = assertBuildStatusSuccess(p.scheduleBuild2(0).get());
FreeStyleBuild b1 = buildAndAssertSuccess(p);
p.setScm(new NullSCM() {
@Override
......@@ -194,7 +194,7 @@ public class AbstractProjectTest extends HudsonTestCase {
// release the polling
sync.signal();
FreeStyleBuild b2 = assertBuildStatusSuccess(f.get());
FreeStyleBuild b2 = assertBuildStatusSuccess(f);
// they should have used the same workspace.
assertEquals(b1.getWorkspace(), b2.getWorkspace());
......
......@@ -103,7 +103,7 @@ public class FreeStyleProjectTest extends HudsonTestCase {
FreeStyleProject f = createFreeStyleProject();
File d = createTmpDir();
f.setCustomWorkspace(d.getPath());
assertBuildStatusSuccess(f.scheduleBuild2(0).get());
buildAndAssertSuccess(f);
}
/**
......@@ -114,7 +114,7 @@ public class FreeStyleProjectTest extends HudsonTestCase {
FreeStyleProject f = createFreeStyleProject();
File d = new File(createTmpDir(),"${JOB_NAME}");
f.setCustomWorkspace(d.getPath());
FreeStyleBuild b = assertBuildStatusSuccess(f.scheduleBuild2(0).get());
FreeStyleBuild b = buildAndAssertSuccess(f);
String path = b.getWorkspace().getRemote();
System.out.println(path);
......
package hudson.tasks._ant;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import hudson.tasks.Ant;
import org.jvnet.hudson.test.HudsonTestCase;
import org.jvnet.hudson.test.SingleFileSCM;
/**
* @author Kohsuke Kawaguchi
*/
public class AntTargetAnnotationTest extends HudsonTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
AntTargetNote.ENABLED = true;
}
public void test1() throws Exception {
FreeStyleProject p = createFreeStyleProject();
p.getBuildersList().add(new Ant("foo",null,null,null,null));
p.setScm(new SingleFileSCM("build.xml",getClass().getResource("simple-build.xml")));
FreeStyleBuild b = buildAndAssertSuccess(p);
AntTargetNote.ENABLED = true;
try {
HtmlPage c = createWebClient().getPage(b, "console");
System.out.println(c.asText());
HtmlElement o = c.getElementById("console-outline");
assertEquals(2,o.selectNodes("LI").size());
} finally {
AntTargetNote.ENABLED = false;
}
}
}
......@@ -80,7 +80,7 @@ public class JDKInstallerTest extends HudsonTestCase {
FreeStyleProject p = createFreeStyleProject();
p.setJDK(jdk);
p.getBuildersList().add(new Shell("java -fullversion\necho $JAVA_HOME"));
FreeStyleBuild b = assertBuildStatusSuccess(p.scheduleBuild2(0).get());
FreeStyleBuild b = buildAndAssertSuccess(p);
@SuppressWarnings("deprecation") String log = b.getLog();
System.out.println(log);
// make sure it runs with the JDK that just got installed
......
document.write("<div id='inserted-by-test1'></div>");
\ No newline at end of file
document.write("<div id='inserted-by-test2'></div>");
\ No newline at end of file
<project>
<target name="foo" depends="bar">
<echo>abc</echo>
</target>
<target name="bar">
<echo>def</echo>
</target>
</project>
......@@ -236,6 +236,16 @@ function findNextFormItem(src,name) {
return findFormItem(src,name,findNext);
}
/**
* Parse HTML into DOM.
*/
function parseHtml(html) {
var c = document.createElement("div");
c.innerHTML = html;
return c.firstChild;
}
// shared tooltip object
var tooltip;
......
......@@ -1017,6 +1017,14 @@ Ajax.Base.prototype = {
this.options.method = this.options.method.toLowerCase();
if (typeof this.options.parameters == 'string')
this.options.parameters = this.options.parameters.toQueryParams();
// KK patch -- handle crumb for POST automatically by adding a header
if(this.options.method=="post") {
if(this.options.requestHeaders==undefined)
this.options.requestHeaders = {};
crumb.wrap(this.options.requestHeaders);
}
// KK patch until here
}
}
......@@ -1030,13 +1038,6 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
initialize: function(url, options) {
this.transport = Ajax.getTransport();
this.setOptions(options);
// KK patch -- handle crumb for POST automatically by adding a header
if(this.options.method=="post") {
if(this.options.requestHeaders==undefined)
this.options.requestHeaders = {};
crumb.wrap(this.options.requestHeaders);
}
// KK patch until here
this.request(url);
},
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册