提交 19860fa0 编写于 作者: K kohsuke

merging remoting-integration branch


git-svn-id: https://hudson.dev.java.net/svn/hudson/trunk/hudson/main@1523 71c3de6d-444a-0410-be80-ed276b4c234a
上级 9dd1f0b5
......@@ -13,6 +13,17 @@
<build>
<plugins>
<plugin>
<groupId>org.kohsuke.stapler</groupId>
<artifactId>maven-stapler-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>stapler</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antlr-plugin</artifactId>
<configuration>
......@@ -43,7 +54,7 @@
<tasks>
<taskdef name="retrotranslator" classpathref="maven.test.classpath" classname="net.sf.retrotranslator.transformer.RetrotranslatorTask" />
<mkdir dir="target/classes14" />
<retrotranslator destdir="target/classes14" verify="true">
<retrotranslator destdir="target/classes14"><!-- verify="true" detects false-positive errors against some references to remoting-->
<src path="target/classes" />
</retrotranslator>
<jar basedir="target/classes14" destfile="target/${artifactId}-${version}-jdk14.jar" />
......@@ -197,7 +208,7 @@
<dependency>
<groupId>org.kohsuke.stapler</groupId>
<artifactId>stapler</artifactId>
<version>1.13</version>
<version>1.14</version>
</dependency>
<dependency>
<groupId>antlr</groupId>
......
package hudson;
import hudson.model.Hudson;
import hudson.model.ModelObject;
import hudson.model.Node;
import hudson.model.Project;
import hudson.model.Run;
import hudson.model.Hudson;
import org.kohsuke.stapler.Ancestor;
import org.kohsuke.stapler.StaplerRequest;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.util.Calendar;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.Calendar;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.logging.LogRecord;
import java.util.logging.SimpleFormatter;
import java.io.File;
import javax.servlet.http.HttpServletResponse;
/**
* Utility functions used in views.
......@@ -259,7 +259,11 @@ public class Functions {
if (param != null) {
return Boolean.parseBoolean(param);
}
for (Cookie c : request.getCookies()) {
Cookie[] cookies = request.getCookies();
if(cookies==null)
return false; // when API design messes it up, we all suffer
for (Cookie c : cookies) {
if (c.getName().equals("hudson_auto_refresh")) {
return Boolean.parseBoolean(c.getValue());
}
......
package hudson;
import hudson.model.Hudson;
import hudson.model.TaskListener;
import hudson.remoting.VirtualChannel;
import hudson.Proc.LocalProc;
import java.io.File;
import java.io.IOException;
......@@ -28,12 +31,27 @@ import java.util.Map;
*
* @author Kohsuke Kawaguchi
*/
public class Launcher {
public abstract class Launcher {
protected final TaskListener listener;
public Launcher(TaskListener listener) {
protected final VirtualChannel channel;
public Launcher(TaskListener listener, VirtualChannel channel) {
this.listener = listener;
this.channel = channel;
}
/**
* Gets the channel that can be used to run a program remotely.
*
* @return
* null if the target node is not configured to support this.
* this is a transitional measure.
* Note that a launcher for the master is always non-null.
*/
public VirtualChannel getChannel() {
return channel;
}
public final Proc launch(String cmd, Map<String,String> env, OutputStream out, FilePath workDir) throws IOException {
......@@ -52,16 +70,25 @@ public class Launcher {
return launch(Util.tokenize(cmd),env,out,workDir);
}
public Proc launch(String[] cmd,String[] env,OutputStream out, FilePath workDir) throws IOException {
printCommandLine(cmd, workDir);
return new Proc(cmd,Util.mapToEnv(inherit(env)),out,workDir.getLocal());
public final Proc launch(String[] cmd,String[] env,OutputStream out, FilePath workDir) throws IOException {
return launch(cmd,env,null,out,workDir);
}
public Proc launch(String[] cmd,String[] env,InputStream in,OutputStream out) throws IOException {
printCommandLine(cmd, null);
return new Proc(cmd,inherit(env),in,out);
public final Proc launch(String[] cmd,String[] env,InputStream in,OutputStream out) throws IOException {
return launch(cmd,env,in,out,null);
}
/**
* @param in
* null if there's no input.
* @param workDir
* null if the working directory could be anything.
* @param out
* stdout and stderr of the process will be sent to this stream.
* the stream won't be closed.
*/
public abstract Proc launch(String[] cmd,String[] env,InputStream in,OutputStream out, FilePath workDir) throws IOException;
/**
* Returns true if this {@link Launcher} is going to launch on Unix.
*/
......@@ -70,23 +97,9 @@ public class Launcher {
}
/**
* Expands the list of environment variables by inheriting current env variables.
* Prints out the command line to the listener so that users know what we are doing.
*/
private Map<String,String> inherit(String[] env) {
Map<String,String> m = new HashMap<String,String>(EnvVars.masterEnvVars);
for (String e : env) {
int index = e.indexOf('=');
String key = e.substring(0,index);
String value = e.substring(index+1);
if(value.length()==0)
m.remove(key);
else
m.put(key,value);
}
return m;
}
private void printCommandLine(String[] cmd, FilePath workDir) {
protected final void printCommandLine(String[] cmd, FilePath workDir) {
StringBuffer buf = new StringBuffer();
if (workDir != null) {
buf.append('[');
......@@ -99,4 +112,36 @@ public class Launcher {
}
listener.getLogger().println(buf.toString());
}
public static class LocalLauncher extends Launcher {
public LocalLauncher(TaskListener listener) {
this(listener,Hudson.MasterComputer.localChannel);
}
public LocalLauncher(TaskListener listener, VirtualChannel channel) {
super(listener, channel);
}
public Proc launch(String[] cmd,String[] env,InputStream in,OutputStream out, FilePath workDir) throws IOException {
printCommandLine(cmd, workDir);
return new LocalProc(cmd,Util.mapToEnv(inherit(env)),in,out, workDir==null ? null : new File(workDir.getRemote()));
}
/**
* Expands the list of environment variables by inheriting current env variables.
*/
private Map<String,String> inherit(String[] env) {
Map<String,String> m = new HashMap<String,String>(EnvVars.masterEnvVars);
for (String e : env) {
int index = e.indexOf('=');
String key = e.substring(0,index);
String value = e.substring(index+1);
if(value.length()==0)
m.remove(key);
else
m.put(key,value);
}
return m;
}
}
}
......@@ -124,7 +124,7 @@ public class Main {
List<String> cmd = new ArrayList<String>();
for( int i=1; i<args.length; i++ )
cmd.add(args[i]);
Proc proc = new Proc(cmd.toArray(new String[0]),(String[])null,System.in,
Proc proc = new Proc.LocalProc(cmd.toArray(new String[0]),(String[])null,System.in,
new DualOutputStream(System.out,new EncodingStream(os)));
int ret = proc.join();
......
package hudson;
import hudson.model.Hudson;
import hudson.scm.SCM;
import hudson.tasks.Builder;
import hudson.tasks.Publisher;
import hudson.triggers.Trigger;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
......@@ -9,12 +14,6 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URL;
import hudson.tasks.Publisher;
import hudson.tasks.Builder;
import hudson.model.Hudson;
import hudson.triggers.Trigger;
import hudson.scm.SCM;
/**
* Base class of Hudson plugin.
*
......
......@@ -2,7 +2,6 @@ package hudson;
import hudson.model.Hudson;
import hudson.util.Service;
import java.util.logging.Level;
import javax.servlet.ServletContext;
import java.io.File;
......@@ -14,6 +13,7 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
......
package hudson;
import hudson.util.IOException2;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.taskdefs.Expand;
import org.apache.tools.ant.types.FileSet;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.FileReader;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
......@@ -22,8 +23,6 @@ import java.util.List;
import java.util.jar.Manifest;
import java.util.logging.Logger;
import hudson.util.IOException2;
/**
* Represents a Hudson plug-in and associated control information
* for Hudson to control {@link Plugin}.
......@@ -341,6 +340,9 @@ public final class PluginWrapper {
LOGGER.info("Extracting "+archive);
// delete the contents so that old files won't interfere with new files
Util.deleteContentsRecursive(destDir);
try {
Expand e = new Expand();
e.setProject(new Project());
......
package hudson;
import hudson.remoting.Channel;
import hudson.util.StreamCopyThread;
import hudson.util.IOException2;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
......@@ -16,117 +22,162 @@ import java.util.logging.Logger;
*
* @author Kohsuke Kawaguchi
*/
public final class Proc {
private final Process proc;
private final Thread t1,t2;
public abstract class Proc {
private Proc() {}
public Proc(String cmd, Map<String,String> env, OutputStream out, File workDir) throws IOException {
this(cmd,Util.mapToEnv(env),out,workDir);
}
/**
* Terminates the process.
*
* @throws IOException
* if there's an error killing a process
* and a stack trace could help the trouble-shooting.
*/
public abstract void kill() throws IOException;
public Proc(String[] cmd, Map<String,String> env,InputStream in, OutputStream out) throws IOException {
this(cmd,Util.mapToEnv(env),in,out);
}
/**
* Waits for the completion of the process.
*
* <p>
* If the thread is interrupted while waiting for the completion
* of the process, this method terminates the process and
* exits with a non-zero exit code.
*
* @throws IOException
* if there's an error launching/joining a process
* and a stack trace could help the trouble-shooting.
*/
public abstract int join() throws IOException;
public Proc(String cmd,String[] env,OutputStream out, File workDir) throws IOException {
this( Util.tokenize(cmd), env, out, workDir );
}
/**
* Locally launched process.
*/
public static final class LocalProc extends Proc {
private final Process proc;
private final Thread t1,t2;
public Proc(String[] cmd,String[] env,OutputStream out, File workDir) throws IOException {
this( calcName(cmd), Runtime.getRuntime().exec(cmd,env,workDir), null, out );
}
public LocalProc(String cmd, Map<String,String> env, OutputStream out, File workDir) throws IOException {
this(cmd,Util.mapToEnv(env),out,workDir);
}
public Proc(String[] cmd,String[] env,InputStream in,OutputStream out) throws IOException {
this( calcName(cmd), Runtime.getRuntime().exec(cmd,env), in, out );
}
public LocalProc(String[] cmd, Map<String,String> env,InputStream in, OutputStream out) throws IOException {
this(cmd,Util.mapToEnv(env),in,out);
}
private Proc( String name, Process proc, InputStream in, OutputStream out ) throws IOException {
Logger.getLogger(Proc.class.getName()).log(Level.FINE, "Running: {0}", name);
this.proc = proc;
t1 = new Copier(name+": stdout copier", proc.getInputStream(), out);
t1.start();
t2 = new Copier(name+": stderr copier", proc.getErrorStream(), out);
t2.start();
if(in!=null)
new ByteCopier(name+": stdin copier",in,proc.getOutputStream()).start();
else
proc.getOutputStream().close();
}
public LocalProc(String cmd,String[] env,OutputStream out, File workDir) throws IOException {
this( Util.tokenize(cmd), env, out, workDir );
}
/**
* Waits for the completion of the process.
*/
public int join() {
try {
t1.join();
t2.join();
return proc.waitFor();
} catch (InterruptedException e) {
// aborting. kill the process
proc.destroy();
return -1;
public LocalProc(String[] cmd,String[] env,OutputStream out, File workDir) throws IOException {
this(cmd,env,null,out,workDir);
}
}
/**
* Terminates the process.
*/
public void kill() {
proc.destroy();
join();
}
public LocalProc(String[] cmd,String[] env,InputStream in,OutputStream out) throws IOException {
this(cmd,env,in,out,null);
}
private static class Copier extends Thread {
private final InputStream in;
private final OutputStream out;
public LocalProc(String[] cmd,String[] env,InputStream in,OutputStream out, File workDir) throws IOException {
this( calcName(cmd), Runtime.getRuntime().exec(cmd,env,workDir), in, out );
}
public Copier(String threadName, InputStream in, OutputStream out) {
super(threadName);
this.in = in;
this.out = out;
private LocalProc( String name, Process proc, InputStream in, OutputStream out ) throws IOException {
Logger.getLogger(Proc.class.getName()).log(Level.FINE, "Running: {0}", name);
this.proc = proc;
t1 = new StreamCopyThread(name+": stdout copier", proc.getInputStream(), out);
t1.start();
t2 = new StreamCopyThread(name+": stderr copier", proc.getErrorStream(), out);
t2.start();
if(in!=null)
new ByteCopier(name+": stdin copier",in,proc.getOutputStream()).start();
else
proc.getOutputStream().close();
}
public void run() {
/**
* Waits for the completion of the process.
*/
@Override
public int join() {
try {
Util.copyStream(in,out);
in.close();
} catch (IOException e) {
// TODO: what to do?
t1.join();
t2.join();
return proc.waitFor();
} catch (InterruptedException e) {
// aborting. kill the process
proc.destroy();
return -1;
}
}
}
private static class ByteCopier extends Thread {
private final InputStream in;
private final OutputStream out;
public ByteCopier(String threadName, InputStream in, OutputStream out) {
super(threadName);
this.in = in;
this.out = out;
@Override
public void kill() {
proc.destroy();
join();
}
public void run() {
try {
while(true) {
int ch = in.read();
if(ch==-1) break;
out.write(ch);
private static class ByteCopier extends Thread {
private final InputStream in;
private final OutputStream out;
public ByteCopier(String threadName, InputStream in, OutputStream out) {
super(threadName);
this.in = in;
this.out = out;
}
public void run() {
try {
while(true) {
int ch = in.read();
if(ch==-1) break;
out.write(ch);
}
in.close();
out.close();
} catch (IOException e) {
// TODO: what to do?
}
in.close();
out.close();
} catch (IOException e) {
// TODO: what to do?
}
}
private static String calcName(String[] cmd) {
StringBuffer buf = new StringBuffer();
for (String token : cmd) {
if(buf.length()>0) buf.append(' ');
buf.append(token);
}
return buf.toString();
}
}
private static String calcName(String[] cmd) {
StringBuffer buf = new StringBuffer();
for (String token : cmd) {
if(buf.length()>0) buf.append(' ');
buf.append(token);
/**
* Retemoly launched process via {@link Channel}.
*/
public static final class RemoteProc extends Proc {
private final Future<Integer> process;
public RemoteProc(Future<Integer> process) {
this.process = process;
}
@Override
public void kill() throws IOException {
process.cancel(true);
join();
}
@Override
public int join() throws IOException {
try {
return process.get();
} catch (InterruptedException e) {
// aborting. kill the process
process.cancel(true);
return -1;
} catch (ExecutionException e) {
if(e.getCause() instanceof IOException)
throw (IOException)e.getCause();
throw new IOException2("Failed to join the process",e);
}
}
return buf.toString();
}
}
package hudson;
import hudson.model.BuildListener;
import hudson.model.TaskListener;
import hudson.util.IOException2;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.Chmod;
import org.apache.tools.ant.taskdefs.Copy;
import javax.servlet.ServletException;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
......@@ -13,20 +18,19 @@ import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.StringTokenizer;
import java.util.SimpleTimeZone;
import java.util.logging.Logger;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.text.SimpleDateFormat;
import org.apache.tools.ant.taskdefs.Chmod;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.BuildException;
import java.security.MessageDigest;
import java.security.DigestInputStream;
import java.security.NoSuchAlgorithmException;
/**
* @author Kohsuke Kawaguchi
......@@ -145,19 +149,32 @@ public class Util {
* On Windows, error messages for IOException aren't very helpful.
* This method generates additional user-friendly error message to the listener
*/
public static void displayIOException( IOException e, BuildListener listener ) {
public static void displayIOException( IOException e, TaskListener listener ) {
String msg = getWin32ErrorMessage(e);
if(msg!=null)
listener.getLogger().println(msg);
}
/**
* Extracts the Win32 error message from {@link IOException} if possible.
*
* @return
* null if there seems to be no error code or if the platform is not Win32.
*/
public static String getWin32ErrorMessage(IOException e) {
if(File.separatorChar!='\\')
return; // not Windows
return null; // not Windows
Matcher m = errorCodeParser.matcher(e.getMessage());
if(!m.matches())
return; // failed to parse
return null; // failed to parse
try {
ResourceBundle rb = ResourceBundle.getBundle("/hudson/win32errors");
listener.getLogger().println(rb.getString("error"+m.group(1)));
return rb.getString("error"+m.group(1));
} catch (Exception _) {
// silently recover from resource related failures
return null;
}
}
......@@ -210,6 +227,34 @@ public class Util {
return v;
}
/**
* Write-only buffer.
*/
private static final byte[] garbage = new byte[8192];
/**
* Computes MD5 digest of the given input stream.
*
* @param source
* The stream will be closed by this method at the end of this method.
*/
public static String getDigestOf(InputStream source) throws IOException {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
DigestInputStream in =new DigestInputStream(source,md5);
try {
while(in.read(garbage)>0)
; // simply discard the input
} finally {
in.close();
}
return toHexString(md5.digest());
} catch (NoSuchAlgorithmException e) {
throw new IOException2("MD5 not installed",e); // impossible
}
}
public static String toHexString(byte[] data, int start, int len) {
StringBuffer buf = new StringBuffer();
for( int i=0; i<len; i++ ) {
......
......@@ -5,9 +5,9 @@ import com.thoughtworks.xstream.core.JVM;
import hudson.model.Hudson;
import hudson.model.User;
import hudson.triggers.Trigger;
import hudson.util.IncompatibleServletVersionDetected;
import hudson.util.IncompatibleVMDetected;
import hudson.util.RingBufferLogHandler;
import hudson.util.IncompatibleServletVersionDetected;
import javax.naming.Context;
import javax.naming.InitialContext;
......@@ -23,8 +23,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.TimerTask;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Entry point when Hudson is used as a webapp.
......
......@@ -5,10 +5,10 @@ import com.thoughtworks.xstream.converters.ConversionException;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.io.StreamException;
import com.thoughtworks.xstream.io.xml.XppReader;
import hudson.model.Descriptor;
import hudson.util.AtomicFileWriter;
import hudson.util.IOException2;
import hudson.util.XStream2;
import hudson.model.Descriptor;
import java.io.BufferedReader;
import java.io.File;
......
package hudson.model;
import hudson.Launcher;
import hudson.Proc;
import hudson.Util;
import static hudson.model.Hudson.isWindows;
import hudson.model.Fingerprint.RangeSet;
import hudson.Proc.LocalProc;
import hudson.model.Fingerprint.BuildPtr;
import hudson.model.Fingerprint.RangeSet;
import static hudson.model.Hudson.isWindows;
import hudson.scm.CVSChangeLogParser;
import hudson.scm.ChangeLogParser;
import hudson.scm.ChangeLogSet;
import hudson.scm.SCM;
import hudson.scm.ChangeLogSet.Entry;
import hudson.scm.SCM;
import hudson.tasks.BuildStep;
import hudson.tasks.Builder;
import hudson.tasks.Publisher;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildWrapper.Environment;
import hudson.tasks.Builder;
import hudson.tasks.Fingerprinter.FingerprintAction;
import hudson.tasks.Publisher;
import hudson.tasks.test.AbstractTestResultAction;
import hudson.triggers.SCMTrigger;
import org.xml.sax.SAXException;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.xml.sax.SAXException;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Map;
import java.util.HashMap;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
/**
* @author Kohsuke Kawaguchi
......@@ -312,7 +312,7 @@ public final class Build extends Run<Project,Build> implements Runnable {
*/
private Launcher launcher;
public Result run(BuildListener listener) throws IOException {
public Result run(BuildListener listener) throws Exception {
Node node = Executor.currentExecutor().getOwner().getNode();
assert builtOn==null;
builtOn = node.getNodeName();
......@@ -357,9 +357,9 @@ public final class Build extends Run<Project,Build> implements Runnable {
if(!isWindows()) {
try {
// ignore a failure.
new Proc(new String[]{"rm","../lastSuccessful"},new String[0],listener.getLogger(),getProject().getBuildDir()).join();
new LocalProc(new String[]{"rm","../lastSuccessful"},new String[0],listener.getLogger(),getProject().getBuildDir()).join();
int r = new Proc(new String[]{
int r = new LocalProc(new String[]{
"ln","-s","builds/"+getId()/*ugly*/,"../lastSuccessful"},
new String[0],listener.getLogger(),getProject().getBuildDir()).join();
if(r!=0)
......@@ -377,11 +377,17 @@ public final class Build extends Run<Project,Build> implements Runnable {
public void post(BuildListener listener) {
// run all of them even if one of them failed
for( Publisher bs : project.getPublishers().values() )
bs.perform(Build.this, launcher, listener);
try {
for( Publisher bs : project.getPublishers().values() )
bs.perform(Build.this, launcher, listener);
} catch (InterruptedException e) {
e.printStackTrace(listener.fatalError("aborted"));
} catch (IOException e) {
e.printStackTrace(listener.fatalError("failed"));
}
}
private boolean build(BuildListener listener, Map<?, Builder> steps) {
private boolean build(BuildListener listener, Map<?, Builder> steps) throws IOException, InterruptedException {
for( Builder bs : steps.values() )
if(!bs.perform(Build.this, launcher, listener))
return false;
......
package hudson.model;
import hudson.remoting.VirtualChannel;
import hudson.remoting.Callable;
import hudson.util.DaemonThreadFactory;
import hudson.util.RunList;
import hudson.EnvVars;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
......@@ -9,6 +12,10 @@ import javax.servlet.ServletException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Represents a set of {@link Executor}s on the same computer.
......@@ -33,7 +40,7 @@ import java.util.List;
*
* @author Kohsuke Kawaguchi
*/
public final class Computer implements ModelObject {
public abstract class Computer implements ModelObject {
private final List<Executor> executors = new ArrayList<Executor>();
private int numExecutors;
......@@ -47,19 +54,26 @@ public final class Computer implements ModelObject {
* {@link Node} object may be created and deleted independently
* from this object.
*/
private String nodeName;
/**
* Represents the communication endpoint to this computer.
* Never null.
*/
private VirtualChannel channel;
protected String nodeName;
public Computer(Node node) {
assert node.getNumExecutors()!=0 : "Computer created with 0 executors";
setNode(node);
}
/**
* Gets the channel that can be used to run a program on this computer.
*
* @return
* never null when {@link #isOffline()}==false.
*/
public abstract VirtualChannel getChannel();
/**
* If {@link #getChannel()}==null, attempts to relaunch the slave agent.
*/
public abstract void doLaunchSlaveAgent( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException;
/**
* Number of {@link Executor}s that are configured for this computer.
*
......@@ -81,6 +95,23 @@ public final class Computer implements ModelObject {
return Hudson.getInstance().getSlave(nodeName);
}
public boolean isOffline() {
return temporarilyOffline || getChannel()==null;
}
/**
* Returns true if this node is marked temporarily offline by the user.
*
* <p>
* In contrast, {@link #isOffline()} represents the actual online/offline
* state. For example, this method may return false while {@link #isOffline()}
* returns true if the slave agent failed to launch.
*
* @deprecated
* You should almost always want {@link #isOffline()}.
* This method is marked as deprecated to warn people when they
* accidentally call this method.
*/
public boolean isTemporarilyOffline() {
return temporarilyOffline;
}
......@@ -91,14 +122,14 @@ public final class Computer implements ModelObject {
}
public String getIcon() {
if(temporarilyOffline)
if(isOffline())
return "computer-x.gif";
else
return "computer.gif";
}
public String getDisplayName() {
return getNode().getNodeName();
return nodeName;
}
public String getUrl() {
......@@ -121,7 +152,7 @@ public final class Computer implements ModelObject {
* Called to notify {@link Computer} that its corresponding {@link Node}
* configuration is updated.
*/
/*package*/ void setNode(Node node) {
protected void setNode(Node node) {
assert node!=null;
if(node instanceof Slave)
this.nodeName = node.getNodeName();
......@@ -134,7 +165,7 @@ public final class Computer implements ModelObject {
/**
* Called to notify {@link Computer} that it will be discarded.
*/
/*package*/ void kill() {
protected void kill() {
setNumExecutors(0);
}
......@@ -188,6 +219,39 @@ public final class Computer implements ModelObject {
}
}
/**
* Gets the system properties of the JVM on this computer.
* If this is the master, it returns the system property of the master computer.
*/
public Map<Object,Object> getSystemProperties() throws IOException, InterruptedException {
return getChannel().call(new GetSystemProperties());
}
private static final class GetSystemProperties implements Callable<Map<Object,Object>,RuntimeException> {
public Map<Object,Object> call() {
return new TreeMap<Object,Object>(System.getProperties());
}
private static final long serialVersionUID = 1L;
}
/**
* Gets the environment variables of the JVM on this computer.
* If this is the master, it returns the system property of the master computer.
*/
public Map<String,String> getEnvVars() throws IOException, InterruptedException {
return getChannel().call(new GetEnvVars());
}
private static final class GetEnvVars implements Callable<Map<String,String>,RuntimeException> {
public Map<String,String> call() {
return new TreeMap<String,String>(EnvVars.masterEnvVars);
}
private static final long serialVersionUID = 1L;
}
protected static final ExecutorService threadPoolForRemoting = Executors.newCachedThreadPool(new DaemonThreadFactory());
//
//
// UI
......
package hudson.model;
import hudson.XmlFile;
import hudson.scm.CVSSCM;
import org.kohsuke.stapler.StaplerRequest;
import javax.servlet.http.HttpServletRequest;
......
package hudson.model;
import hudson.FilePath;
import hudson.FilePath.FileCallable;
import hudson.remoting.VirtualChannel;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
......@@ -33,7 +37,7 @@ public abstract class DirectoryHolder extends Actionable {
* True to generate the directory index.
* False to serve "index.html"
*/
protected final void serveFile(StaplerRequest req, StaplerResponse rsp, File root, String icon, boolean serveDirIndex) throws IOException, ServletException {
protected final void serveFile(StaplerRequest req, StaplerResponse rsp, FilePath root, String icon, boolean serveDirIndex) throws IOException, ServletException, InterruptedException {
if(req.getQueryString()!=null) {
req.setCharacterEncoding("UTF-8");
String path = req.getParameter("path");
......@@ -54,11 +58,11 @@ public abstract class DirectoryHolder extends Actionable {
return;
}
File f = new File(root,path.substring(1));
FilePath f = new FilePath(root,path.substring(1));
boolean isFingerprint=false;
if(f.getName().equals("*fingerprint*")) {
f = f.getParentFile();
f = f.getParent();
isFingerprint = true;
}
......@@ -79,30 +83,41 @@ public abstract class DirectoryHolder extends Actionable {
req.setAttribute("parentPath",parentPaths);
req.setAttribute("topPath",
parentPaths.isEmpty() ? "." : repeat("../",parentPaths.size()));
req.setAttribute("files",buildChildPathList(f));
req.setAttribute("files", f.act(new ChildPathBuilder()));
req.setAttribute("icon",icon);
req.setAttribute("path",path);
req.getView(this,"dir.jelly").forward(req,rsp);
return;
} else {
f = new File(f,"index.html");
f = f.child("index.html");
}
}
if(isFingerprint) {
FileInputStream in = new FileInputStream(f);
try {
Hudson hudson = Hudson.getInstance();
rsp.forward(hudson.getFingerprint(hudson.getDigestOf(in)),"/",req);
} finally {
in.close();
}
rsp.forward(f.digest(),"/",req);
} else {
rsp.serveFile(req,f.toURL());
ContentInfo ci = f.act(new ContentInfo());
InputStream in = f.read();
rsp.serveFile(req, in, ci.lastModified, ci.contentLength, f.getName() );
in.close();
}
}
private static final class ContentInfo implements FileCallable<ContentInfo> {
int contentLength;
long lastModified;
public ContentInfo invoke(File f, VirtualChannel channel) throws IOException {
contentLength = (int) f.length();
lastModified = f.lastModified();
return this;
}
private static final long serialVersionUID = 1L;
}
/**
* Builds a list of {@link Path} that represents ancestors
* from a string like "/foo/bar/zot".
......@@ -120,46 +135,6 @@ public abstract class DirectoryHolder extends Actionable {
return r;
}
/**
* Builds a list of list of {@link Path}. The inner
* list of {@link Path} represents one child item to be shown
* (this mechanism is used to skip empty intermediate directory.)
*/
private List<List<Path>> buildChildPathList(File cur) {
List<List<Path>> r = new ArrayList<List<Path>>();
File[] files = cur.listFiles();
Arrays.sort(files,FILE_SORTER);
for( File f : files ) {
Path p = new Path(f.getName(),f.getName(),f.isDirectory(),f.length());
if(!f.isDirectory()) {
r.add(Collections.singletonList(p));
} else {
// find all empty intermediate directory
List<Path> l = new ArrayList<Path>();
l.add(p);
String relPath = f.getName();
while(true) {
// files that don't start with '.' qualify for 'meaningful files', nor SCM related files
File[] sub = f.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return !name.startsWith(".") && !name.equals("CVS") && !name.equals(".svn");
}
});
if(sub.length!=1 || !sub[0].isDirectory())
break;
f = sub[0];
relPath += '/'+f.getName();
l.add(new Path(relPath,f.getName(),true,0));
}
r.add(l);
}
}
return r;
}
private static String repeat(String s,int times) {
StringBuffer buf = new StringBuffer(s.length()*times);
for(int i=0; i<times; i++ )
......@@ -170,7 +145,7 @@ public abstract class DirectoryHolder extends Actionable {
/**
* Represents information about one file or folder.
*/
public final class Path {
public static final class Path implements Serializable {
/**
* Relative URL to this path from the current page.
*/
......@@ -213,11 +188,13 @@ public abstract class DirectoryHolder extends Actionable {
public long getSize() {
return size;
}
private static final long serialVersionUID = 1L;
}
private static final Comparator<File> FILE_SORTER = new Comparator<File>() {
private static final class FileComparator implements Comparator<File> {
public int compare(File lhs, File rhs) {
// directories first, files next
int r = dirRank(lhs)-dirRank(rhs);
......@@ -230,5 +207,49 @@ public abstract class DirectoryHolder extends Actionable {
if(f.isDirectory()) return 0;
else return 1;
}
};
}
/**
* Builds a list of list of {@link Path}. The inner
* list of {@link Path} represents one child item to be shown
* (this mechanism is used to skip empty intermediate directory.)
*/
private static final class ChildPathBuilder implements FileCallable<List<List<Path>>> {
public List<List<Path>> invoke(File cur, VirtualChannel channel) throws IOException {
List<List<Path>> r = new ArrayList<List<Path>>();
File[] files = cur.listFiles();
Arrays.sort(files,new FileComparator());
for( File f : files ) {
Path p = new Path(f.getName(),f.getName(),f.isDirectory(),f.length());
if(!f.isDirectory()) {
r.add(Collections.singletonList(p));
} else {
// find all empty intermediate directory
List<Path> l = new ArrayList<Path>();
l.add(p);
String relPath = f.getName();
while(true) {
// files that don't start with '.' qualify for 'meaningful files', nor SCM related files
File[] sub = f.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return !name.startsWith(".") && !name.equals("CVS") && !name.equals(".svn");
}
});
if(sub.length!=1 || !sub[0].isDirectory())
break;
f = sub[0];
relPath += '/'+f.getName();
l.add(new Path(relPath,f.getName(),true,0));
}
r.add(l);
}
}
return r;
}
private static final long serialVersionUID = 1L;
}
}
package hudson.model;
import hudson.Util;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import java.io.IOException;
import hudson.Functions;
import hudson.Util;
/**
* Thread that executes builds.
......
......@@ -38,7 +38,7 @@ public class ExternalRun extends Run<ExternalJob,ExternalRun> {
public void run(final String[] cmd) {
run(new Runner() {
public Result run(BuildListener listener) throws Exception {
Proc proc = new Proc(cmd,getEnvVars(),System.in,new DualOutputStream(System.out,listener.getLogger()));
Proc proc = new Proc.LocalProc(cmd,getEnvVars(),System.in,new DualOutputStream(System.out,listener.getLogger()));
return proc.join()==0?Result.SUCCESS:Result.FAILURE;
}
......
......@@ -4,6 +4,7 @@ import com.thoughtworks.xstream.XStream;
import groovy.lang.GroovyShell;
import hudson.FeedAdapter;
import hudson.Launcher;
import hudson.Launcher.LocalLauncher;
import hudson.Plugin;
import hudson.PluginManager;
import hudson.PluginWrapper;
......@@ -11,6 +12,8 @@ import hudson.Util;
import hudson.XmlFile;
import hudson.model.Descriptor.FormException;
import hudson.model.listeners.JobListener;
import hudson.remoting.LocalChannel;
import hudson.remoting.VirtualChannel;
import hudson.scm.CVSSCM;
import hudson.scm.SCM;
import hudson.scm.SCMS;
......@@ -41,12 +44,8 @@ import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.AbstractList;
import java.util.ArrayList;
......@@ -273,7 +272,7 @@ public final class Hudson extends JobCollection implements Node {
}
public Launcher createLauncher(TaskListener listener) {
return new Launcher(listener);
return new LocalLauncher(listener);
}
/**
......@@ -283,7 +282,7 @@ public final class Hudson extends JobCollection implements Node {
* This method tries to reuse existing {@link Computer} objects
* so that we won't upset {@link Executor}s running in it.
*/
private void updateComputerList() {
private void updateComputerList() throws IOException {
synchronized(computers) {
Map<String,Computer> byName = new HashMap<String,Computer>();
for (Computer c : computers.values()) {
......@@ -313,11 +312,11 @@ public final class Hudson extends JobCollection implements Node {
private void updateComputer(Node n, Map<String,Computer> byNameMap, Set<Computer> used) {
Computer c;
c = byNameMap.get(n.getNodeName());
if(c==null) {
if(n.getNumExecutors()>0)
computers.put(n,c=new Computer(n));
if (c!=null) {
c.setNode(n); // reuse
} else {
c.setNode(n);
if(n.getNumExecutors()>0)
computers.put(n,c=n.createComputer());
}
used.add(c);
}
......@@ -673,6 +672,10 @@ public final class Hudson extends JobCollection implements Node {
return Mode.NORMAL;
}
public Computer createComputer() {
return new MasterComputer();
}
private synchronized void load() throws IOException {
XmlFile cfg = getConfigFile();
if(cfg.exists())
......@@ -714,8 +717,10 @@ public final class Hudson extends JobCollection implements Node {
public void cleanUp() {
terminating = true;
synchronized(computers) {
for( Computer c : computers.values() )
for( Computer c : computers.values() ) {
c.interrupt();
c.kill();
}
}
ExternalJob.reloadThread.interrupt();
Trigger.timer.cancel();
......@@ -752,23 +757,10 @@ public final class Hudson extends JobCollection implements Node {
{// update slave list
List<Slave> newSlaves = new ArrayList<Slave>();
String[] names = req.getParameterValues("slave_name");
String[] descriptions = req.getParameterValues("slave_description");
String[] executors = req.getParameterValues("slave_executors");
String[] cmds = req.getParameterValues("slave_command");
String[] rfs = req.getParameterValues("slave_remoteFS");
String[] lfs = req.getParameterValues("slave_localFS");
String[] mode = req.getParameterValues("slave_mode");
if(names!=null && descriptions!=null && executors!=null && cmds!=null && rfs!=null && lfs!=null && mode!=null) {
int len = Util.min(names.length,descriptions.length,executors.length,cmds.length,rfs.length, lfs.length, mode.length);
for(int i=0;i<len;i++) {
int n = 2;
try {
n = Integer.parseInt(executors[i].trim());
} catch(NumberFormatException e) {
// ignore
}
newSlaves.add(new Slave(names[i],descriptions[i],cmds[i],rfs[i],new File(lfs[i]),n, Mode.valueOf(mode[i])));
String[] names = req.getParameterValues("slave.name");
if(names!=null) {
for(int i=0;i< names.length;i++) {
newSlaves.add(req.bindParameters(Slave.class,"slave.",i));
}
}
this.slaves = newSlaves;
......@@ -1060,7 +1052,7 @@ public final class Hudson extends JobCollection implements Node {
List<FileItem> items = upload.parseRequest(req);
rsp.sendRedirect2(req.getContextPath()+"/fingerprint/"+
getDigestOf(items.get(0).getInputStream())+'/');
Util.getDigestOf(items.get(0).getInputStream())+'/');
// if an error occur and we fail to do this, it will still be cleaned up
// when GC-ed.
......@@ -1071,24 +1063,6 @@ public final class Hudson extends JobCollection implements Node {
}
}
public String getDigestOf(InputStream source) throws IOException, ServletException {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
DigestInputStream in =new DigestInputStream(source,md5);
byte[] buf = new byte[8192];
try {
while(in.read(buf)>0)
; // simply discard the input
} finally {
in.close();
}
return Util.toHexString(md5.digest());
} catch (NoSuchAlgorithmException e) {
throw new ServletException(e); // impossible
}
}
/**
* Serves static resources without the "Last-Modified" header to work around
* a bug in Firefox.
......@@ -1266,6 +1240,28 @@ public final class Hudson extends JobCollection implements Node {
return r;
}
public static final class MasterComputer extends Computer {
private MasterComputer() {
super(Hudson.getInstance());
}
@Override
public VirtualChannel getChannel() {
return localChannel;
}
public void doLaunchSlaveAgent(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
// this computer never returns null from channel, so
// this method shall never be invoked.
rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
}
/**
* {@link LocalChannel} instance that can be used to execute programs locally.
*/
public static final LocalChannel localChannel = new LocalChannel(threadPoolForRemoting);
}
public static boolean adminCheck(StaplerRequest req,StaplerResponse rsp) throws IOException {
if(!getInstance().isUseSecurity())
return true;
......
......@@ -7,13 +7,13 @@ import hudson.XmlFile;
import hudson.tasks.BuildTrigger;
import hudson.tasks.LogRotator;
import hudson.util.ChartUtil;
import hudson.util.ColorPalette;
import hudson.util.DataSetBuilder;
import hudson.util.IOException2;
import hudson.util.RunList;
import hudson.util.ShiftedCategoryAxis;
import hudson.util.TextFile;
import hudson.util.XStream2;
import hudson.util.ColorPalette;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.types.FileSet;
import org.jfree.chart.ChartFactory;
......@@ -37,9 +37,9 @@ import java.awt.Paint;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.SortedMap;
import java.util.Collections;
/**
* A job is an runnable entity under the monitoring of Hudson.
......
package hudson.model;
import hudson.Util;
import hudson.scm.ChangeLogSet.Entry;
import hudson.util.RunList;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.Collections;
import java.util.GregorianCalendar;
import hudson.scm.ChangeLogSet.Entry;
import hudson.Util;
import hudson.util.RunList;
/**
* Collection of {@link Job}s.
......
......@@ -2,16 +2,24 @@ package hudson.model;
import hudson.util.CountingOutputStream;
import hudson.util.WriterOutputStream;
import hudson.util.CharSpool;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.Writer;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
/**
* Represents a large text data.
*
* <p>
* This class defines methods for handling progressive text update.
*
* @author Kohsuke Kawaguchi
*/
public class LargeText {
......@@ -66,6 +74,41 @@ public class LargeText {
return os.getCount()+start;
}
/**
* Implements the progressive text handling.
* This method is used as a "web method" with progressiveText.jelly.
*/
public void doProgressText(StaplerRequest req, StaplerResponse rsp) throws IOException {
rsp.setContentType("text/plain");
rsp.setCharacterEncoding("UTF-8");
rsp.setStatus(HttpServletResponse.SC_OK);
if(!file.exists()) {
// file doesn't exist yet
rsp.addHeader("X-Text-Size","0");
rsp.addHeader("X-More-Data","true");
return;
}
long start = 0;
String s = req.getParameter("start");
if(s!=null)
start = Long.parseLong(s);
if(file.length() < start )
start = 0; // text rolled over
CharSpool spool = new CharSpool();
long r = writeLogTo(start,spool);
rsp.addHeader("X-Text-Size",String.valueOf(r));
if(!completed)
rsp.addHeader("X-More-Data","true");
spool.writeTo(rsp.getWriter());
}
/**
* Points to a byte in the buffer.
*/
......
package hudson.model;
import hudson.Launcher;
import hudson.util.EnumConverter;
import org.apache.commons.beanutils.ConvertUtils;
/**
* Commonality between {@link Slave} and master {@link Hudson}.
......@@ -41,6 +43,8 @@ public interface Node {
*/
Mode getMode();
Computer createComputer();
public enum Mode {
NORMAL("Utilize this slave as much as possible"),
EXCLUSIVE("Leave this machine for tied jobs only");
......@@ -58,5 +62,9 @@ public interface Node {
Mode(String description) {
this.description = description;
}
static {
ConvertUtils.register(new EnumConverter(),Mode.class);
}
}
}
......@@ -2,7 +2,7 @@ package hudson.model;
import hudson.FilePath;
import hudson.Launcher;
import hudson.util.EditDistance;
import hudson.Launcher.LocalLauncher;
import hudson.model.Descriptor.FormException;
import hudson.model.Fingerprint.RangeSet;
import hudson.model.RunMap.Constructor;
......@@ -11,14 +11,15 @@ import hudson.scm.SCM;
import hudson.scm.SCMS;
import hudson.tasks.BuildStep;
import hudson.tasks.BuildTrigger;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildWrappers;
import hudson.tasks.Builder;
import hudson.tasks.Fingerprinter;
import hudson.tasks.Publisher;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildWrappers;
import hudson.tasks.test.AbstractTestResultAction;
import hudson.triggers.Trigger;
import hudson.triggers.Triggers;
import hudson.util.EditDistance;
import org.kohsuke.stapler.Ancestor;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
......@@ -323,10 +324,15 @@ public class Project extends Job<Project,Build> {
if(scm==null)
return true; // no SCM
FilePath workspace = getWorkspace();
workspace.mkdirs();
try {
FilePath workspace = getWorkspace();
workspace.mkdirs();
return scm.checkout(build, launcher, workspace, listener, changelogFile);
return scm.checkout(build, launcher, workspace, listener, changelogFile);
} catch (InterruptedException e) {
e.printStackTrace(listener.fatalError("SCM check out aborted"));
return false;
}
}
/**
......@@ -342,21 +348,23 @@ public class Project extends Job<Project,Build> {
return false; // no SCM
}
FilePath workspace = getWorkspace();
if(!workspace.exists()) {
// no workspace. build now, or nothing will ever be built
listener.getLogger().println("No workspace is available, so can't check for updates.");
listener.getLogger().println("Scheduling a new build to get a workspace.");
return true;
}
try {
FilePath workspace = getWorkspace();
if(!workspace.exists()) {
// no workspace. build now, or nothing will ever be built
listener.getLogger().println("No workspace is available, so can't check for updates.");
listener.getLogger().println("Scheduling a new build to get a workspace.");
return true;
}
// TODO: do this by using the right slave
return scm.pollChanges(this, new Launcher(listener), workspace, listener );
return scm.pollChanges(this, new LocalLauncher(listener), workspace, listener );
} catch (IOException e) {
e.printStackTrace(listener.fatalError(e.getMessage()));
return false;
} catch (InterruptedException e) {
e.printStackTrace(listener.fatalError("SCM polling aborted"));
return false;
}
}
......@@ -690,13 +698,13 @@ public class Project extends Job<Project,Build> {
/**
* Serves the workspace files.
*/
public void doWs( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
File dir = getWorkspace().getLocal();
if(!dir.exists()) {
public void doWs( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, InterruptedException {
FilePath ws = getWorkspace();
if(!ws.exists()) {
// if there's no workspace, report a nice error message
rsp.forward(this,"noWorkspace",req);
} else {
serveFile(req, rsp, dir, "folder.gif", true);
serveFile(req, rsp, ws, "folder.gif", true);
}
}
......
......@@ -3,6 +3,13 @@ package hudson.model;
import hudson.model.Node.Mode;
import hudson.util.OneShotEvent;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.Calendar;
import java.util.Comparator;
import java.util.GregorianCalendar;
......@@ -17,13 +24,6 @@ import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.io.PrintWriter;
import java.io.FileOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
/**
* Build queue.
......@@ -96,7 +96,7 @@ public class Queue {
}
public boolean isAvailable() {
return project==null && !executor.getOwner().isTemporarilyOffline();
return project==null && !executor.getOwner().isOffline();
}
public Node getNode() {
......
......@@ -8,12 +8,7 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Iterator;
/**
* RSS related code.
......
package hudson.model;
import static hudson.Util.combine;
import com.thoughtworks.xstream.XStream;
import hudson.CloseProofOutputStream;
import hudson.ExtensionPoint;
import hudson.FeedAdapter;
import hudson.Util;
import static hudson.Util.combine;
import hudson.XmlFile;
import hudson.FeedAdapter;
import hudson.FilePath;
import hudson.tasks.LogRotator;
import hudson.tasks.BuildStep;
import hudson.tasks.test.AbstractTestResultAction;
import hudson.util.CharSpool;
import hudson.util.IOException2;
......@@ -507,33 +509,12 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
long start = System.currentTimeMillis();
BuildListener listener=null;
PrintStream log = null;
try {
try {
final PrintStream log = new PrintStream(new FileOutputStream(getLogFile()));
listener = new BuildListener() {
final PrintWriter pw = new PrintWriter(new CloseProofOutputStream(log),true);
public void started() {}
public PrintStream getLogger() {
return log;
}
public PrintWriter error(String msg) {
pw.println("ERROR: "+msg);
return pw;
}
public PrintWriter fatalError(String msg) {
return error(msg);
}
public void finished(Result result) {
pw.close();
log.close();
}
};
log = new PrintStream(new FileOutputStream(getLogFile()));
listener = new StreamBuildListener(new CloseProofOutputStream(log));
listener.started();
......@@ -562,6 +543,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();
try {
save();
......@@ -716,8 +699,8 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
/**
* Serves the artifacts.
*/
public void doArtifact( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
serveFile(req, rsp, getArtifactsDir(), "package.gif", true);
public void doArtifact( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, InterruptedException {
serveFile(req, rsp, new FilePath(getArtifactsDir()), "package.gif", true);
}
/**
......@@ -734,32 +717,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 {
rsp.setContentType("text/plain");
rsp.setCharacterEncoding("UTF-8");
rsp.setStatus(HttpServletResponse.SC_OK);
boolean completed = !isBuilding();
File logFile = getLogFile();
if(!logFile.exists()) {
// file doesn't exist yet
rsp.addHeader("X-Text-Size","0");
rsp.addHeader("X-More-Data","true");
return;
}
LargeText text = new LargeText(logFile,completed);
long start = 0;
String s = req.getParameter("start");
if(s!=null)
start = Long.parseLong(s);
CharSpool spool = new CharSpool();
long r = text.writeLogTo(start,spool);
rsp.addHeader("X-Text-Size",String.valueOf(r));
if(!completed)
rsp.addHeader("X-More-Data","true");
spool.writeTo(rsp.getWriter());
new LargeText(getLogFile(),!isBuilding()).doProgressText(req,rsp);
}
public void doToggleLogKeep( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
......
package hudson.model;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.AbstractMap;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.Comparator;
import java.util.Collections;
import java.util.Map;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
/**
* {@link Map} from build number to {@link Run}.
......
......@@ -3,49 +3,58 @@ package hudson.model;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Proc;
import hudson.Proc.RemoteProc;
import hudson.Util;
import hudson.CloseProofOutputStream;
import hudson.Launcher.LocalLauncher;
import hudson.model.Descriptor.FormException;
import hudson.util.ArgumentListBuilder;
import hudson.remoting.Callable;
import hudson.remoting.Channel;
import hudson.remoting.RemoteInputStream;
import hudson.remoting.RemoteOutputStream;
import hudson.remoting.VirtualChannel;
import hudson.remoting.Channel.Listener;
import hudson.util.StreamCopyThread;
import hudson.util.StreamTaskListener;
import hudson.util.NullStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import java.io.Serializable;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
/**
* Information about a Hudson slave node.
*
* @author Kohsuke Kawaguchi
*/
public final class Slave implements Node {
public final class Slave implements Node, Serializable {
/**
* Name of this slave node.
*/
private final String name;
protected final String name;
/**
* Description of this node.
*/
private final String description;
/**
* Commands to run to post a job on this machine.
*/
private final String command;
/**
* Path to the root of the workspace
* from within this node, such as "/hudson"
*/
private final String remoteFS;
/**
* Path to the root of the remote workspace of this node,
* such as "/net/slave1/hudson"
* from the view point of this node, such as "/hudson"
*/
private final File localFS;
protected final String remoteFS;
/**
* Number of executors of this node.
......@@ -57,17 +66,26 @@ public final class Slave implements Node {
*/
private Mode mode;
public Slave(String name, String description, String command, String remoteFS, File localFS, int numExecutors, Mode mode) throws FormException {
/**
* Command line to launch the agent, like
* "ssh myslave java -jar /path/to/hudson-remoting.jar"
*/
private String agentCommand;
/**
* @stapler-constructor
*/
public Slave(String name, String description, String command, String remoteFS, int numExecutors, Mode mode) throws FormException {
this.name = name;
this.description = description;
this.command = command;
this.remoteFS = remoteFS;
this.localFS = localFS;
this.numExecutors = numExecutors;
this.mode = mode;
this.agentCommand = command;
this.remoteFS = remoteFS;
if (name.equals(""))
throw new FormException("Invalid slave configuration. Name is empty", null);
// this prevents the config from being saved when slaves are offline.
// on a large deployment with a lot of slaves, some slaves are bound to be offline,
// so this check is harmful.
......@@ -77,24 +95,16 @@ public final class Slave implements Node {
throw new FormException("Invalid slave configuration for " + name + ". No remote directory given", null);
}
public String getNodeName() {
return name;
}
public String getCommand() {
return command;
}
public String[] getCommandTokens() {
return Util.tokenize(command);
return agentCommand;
}
public String getRemoteFS() {
return remoteFS;
}
public File getLocalFS() {
return localFS;
public String getNodeName() {
return name;
}
public String getNodeDescription() {
......@@ -102,7 +112,7 @@ public final class Slave implements Node {
}
public FilePath getFilePath() {
return new FilePath(localFS,remoteFS);
return new FilePath(getComputer().getChannel(),remoteFS);
}
public int getNumExecutors() {
......@@ -118,22 +128,29 @@ public final class Slave implements Node {
*
* @return
* difference in milli-seconds.
* a large positive value indicates that the master is ahead of the slave,
* a positive value indicates that the master is ahead of the slave,
* and negative value indicates otherwise.
*/
public long getClockDifference() throws IOException {
File testFile = new File(localFS,"clock.skew");
FileOutputStream os = new FileOutputStream(testFile);
long now = new Date().getTime();
os.close();
long r = now - testFile.lastModified();
VirtualChannel channel = getComputer().getChannel();
if(channel==null) return 0; // can't check
testFile.delete();
try {
long startTime = System.currentTimeMillis();
long slaveTime = channel.call(new Callable<Long,RuntimeException>() {
public Long call() {
return System.currentTimeMillis();
}
});
long endTime = System.currentTimeMillis();
return r;
return (startTime+endTime)/2 - slaveTime;
} catch (InterruptedException e) {
return 0; // couldn't check
}
}
/**
* Gets the clock difference in HTML string.
*/
......@@ -160,61 +177,172 @@ public final class Slave implements Node {
}
}
public Launcher createLauncher(TaskListener listener) {
if(command.length()==0) // local alias
return new Launcher(listener);
public Computer createComputer() {
return new ComputerImpl(this);
}
/**
* Root directory on this slave where all the job workspaces are laid out.
*/
public FilePath getWorkspaceRoot() {
return getFilePath().child("workspace");
}
public static final class ComputerImpl extends Computer {
private volatile Channel channel;
return new Launcher(listener) {
@Override
public Proc launch(String[] cmd, String[] env, OutputStream out, FilePath workDir) throws IOException {
return super.launch(prepend(cmd,env,workDir), env, null, out);
}
/**
* This is where the log from the remote agent goes.
*/
private File getLogFile() {
return new File(Hudson.getInstance().getRootDir(),"slave-"+nodeName+".log");
}
@Override
public Proc launch(String[] cmd, String[] env, InputStream in, OutputStream out) throws IOException {
return super.launch(prepend(cmd,env,CURRENT_DIR), env, in, out);
private ComputerImpl(Slave slave) {
super(slave);
}
/**
* Launches a remote agent.
*/
private void launch(final Slave slave) {
closeChannel();
OutputStream os;
try {
os = new FileOutputStream(getLogFile());
} catch (FileNotFoundException e) {
logger.log(Level.SEVERE, "Failed to create log file "+getLogFile(),e);
os = new NullStream();
}
final OutputStream launchLog = os;
// launch the slave agent asynchronously
threadPoolForRemoting.execute(new Runnable() {
// TODO: do this only for nodes that are so configured.
// TODO: support passive connection via JNLP
public void run() {
final StreamTaskListener listener = new StreamTaskListener(launchLog);
try {
listener.getLogger().println("Launching slave agent");
listener.getLogger().println("$ "+slave.agentCommand);
Process proc = Runtime.getRuntime().exec(slave.agentCommand);
// capture error information from stderr. this will terminate itself
// when the process is killed.
new StreamCopyThread("stderr copier for remote agent on "+slave.getNodeName(),
proc.getErrorStream(), launchLog).start();
channel = new Channel(nodeName,threadPoolForRemoting,
proc.getInputStream(),proc.getOutputStream(), launchLog);
channel.addListener(new Listener() {
public void onClosed(Channel c,IOException cause) {
cause.printStackTrace(listener.error("slave agent was terminated"));
channel = null;
}
});
logger.info("slave agent launched for "+slave.getNodeName());
} catch (IOException e) {
Util.displayIOException(e,listener);
String msg = Util.getWin32ErrorMessage(e);
if(msg==null) msg="";
else msg=" : "+msg;
msg = "Unable to launch the slave agent for " + slave.getNodeName() + msg;
logger.log(Level.SEVERE,msg,e);
e.printStackTrace(listener.error(msg));
}
}
});
}
@Override
public boolean isUnix() {
// Err on Unix, since we expect that to be the common slaves
return remoteFS.indexOf('\\')==-1;
@Override
public VirtualChannel getChannel() {
return channel;
}
public void doLaunchSlaveAgent(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
if(channel!=null) {
rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
private String[] prepend(String[] cmd, String[] env, FilePath workDir) {
ArgumentListBuilder r = new ArgumentListBuilder();
r.add(getCommandTokens());
r.add(getFilePath().child("bin").child("slave").getRemote());
r.addQuoted(workDir.getRemote());
for (String s : env) {
int index =s.indexOf('=');
r.add(s.substring(0,index));
r.add(s.substring(index+1));
}
r.add("--");
for (String c : cmd) {
// ssh passes the command and parameters in one string.
// see RFC 4254 section 6.5.
// so the consequence that we need to give
// {"ssh",...,"ls","\"a b\""} to list a file "a b".
// If we just do
// {"ssh",...,"ls","a b"} (which is correct if this goes directly to Runtime.exec),
// then we end up executing "ls","a","b" on the other end.
//
// I looked at rsh source code, and that behave the same way.
if(c.indexOf(' ')>=0)
r.addQuoted(c);
else
r.add(c);
launch((Slave) getNode());
// TODO: would be nice to redirect the user to "launching..." wait page,
// then spend a few seconds there and poll for the completion periodically.
rsp.sendRedirect("log");
}
/**
* Gets the string representation of the slave log.
*/
public String getLog() throws IOException {
return Util.loadFile(getLogFile());
}
/**
* Handles incremental log.
*/
public void doProgressiveLog( StaplerRequest req, StaplerResponse rsp) throws IOException {
new LargeText(getLogFile(),false).doProgressText(req,rsp);
}
@Override
protected void kill() {
super.kill();
closeChannel();
}
private void closeChannel() {
Channel c = channel;
channel = null;
if(c!=null)
try {
c.close();
} catch (IOException e) {
logger.log(Level.SEVERE, "Failed to terminate channel to "+getDisplayName(),e);
}
return r.toCommandArray();
}
@Override
protected void setNode(Node node) {
super.setNode(node);
if(channel==null)
// maybe the configuration was changed to relaunch the slave, so try it now.
launch((Slave)node);
}
private static final Logger logger = Logger.getLogger(ComputerImpl.class.getName());
}
public Launcher createLauncher(TaskListener listener) {
return new Launcher(listener, getComputer().getChannel()) {
public Proc launch(final String[] cmd, final String[] env, InputStream _in, OutputStream _out, FilePath _workDir) throws IOException {
printCommandLine(cmd,_workDir);
final OutputStream out = new RemoteOutputStream(new CloseProofOutputStream(_out));
final InputStream in = _in==null ? null : new RemoteInputStream(_in);
final String workDir = _workDir==null ? null : _workDir.getRemote();
return new RemoteProc(getChannel().callAsync(new RemoteLaunchCallable(cmd, env, in, out, workDir)));
}
@Override
public boolean isUnix() {
// Windows can handle '/' as a path separator but Unix can't,
// so err on Unix side
return remoteFS.indexOf("\\")==-1;
}
};
}
public FilePath getWorkspaceRoot() {
return getFilePath().child("workspace");
/**
* Gets th ecorresponding computer object.
*/
public Computer getComputer() {
return Hudson.getInstance().getComputer(getNodeName());
}
public boolean equals(Object o) {
......@@ -224,12 +352,65 @@ public final class Slave implements Node {
final Slave that = (Slave) o;
return name.equals(that.name);
}
public int hashCode() {
return name.hashCode();
}
private static final FilePath CURRENT_DIR = new FilePath(new File("."));
/**
* Invoked by XStream when this object is read into memory.
*/
private Object readResolve() {
// convert the old format to the new one
if(command!=null && agentCommand==null) {
if(command.length()>0) command += ' ';
agentCommand = command+"java -jar ~/bin/slave.jar";
}
return this;
}
//
// backwrad compatibility
//
/**
* In Hudson < 1.69 this was used to store the local file path
* to the remote workspace. No longer in use.
*
* @deprecated
* ... but still in use during the transition.
*/
private File localFS;
/**
* In Hudson < 1.69 this was used to store the command
* to connect to the remote machine, like "ssh myslave".
*
* @deprecated
*/
private transient String command;
private static class RemoteLaunchCallable implements Callable<Integer,IOException> {
private final String[] cmd;
private final String[] env;
private final InputStream in;
private final OutputStream out;
private final String workDir;
public RemoteLaunchCallable(String[] cmd, String[] env, InputStream in, OutputStream out, String workDir) {
this.cmd = cmd;
this.env = env;
this.in = in;
this.out = out;
this.workDir = workDir;
}
public Integer call() throws IOException {
Proc p = new LocalLauncher(TaskListener.NULL).launch(cmd, env, in, out,
workDir ==null ? null : new FilePath(new File(workDir)));
return p.join();
}
private static final long serialVersionUID = 1L;
}
}
package hudson.model;
import hudson.util.WriterOutputStream;
import hudson.CloseProofOutputStream;
import hudson.remoting.RemoteOutputStream;
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.Writer;
import java.io.Serializable;
/**
* {@link BuildListener} that writes to a {@link Writer}.
* {@link BuildListener} that writes to an {@link OutputStream}.
*
* This class is remotable.
*
* @author Kohsuke Kawaguchi
*/
public class StreamBuildListener implements BuildListener {
private final PrintWriter w;
public class StreamBuildListener implements BuildListener, Serializable {
private PrintWriter w;
private final PrintStream ps;
private PrintStream ps;
public StreamBuildListener(Writer w) {
this(new PrintWriter(w));
public StreamBuildListener(OutputStream w) {
this(new PrintStream(w));
}
public StreamBuildListener(PrintWriter w) {
this.w = w;
public StreamBuildListener(PrintStream w) {
this.ps = w;
// unless we auto-flash, PrintStream will use BufferedOutputStream internally,
// and break ordering
this.ps = new PrintStream(new WriterOutputStream(w),true);
this.w = new PrintWriter(w,true);
}
public void started() {
......@@ -47,4 +55,16 @@ public class StreamBuildListener implements BuildListener {
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);
}
private static final long serialVersionUID = 1L;
}
package hudson.model;
import hudson.util.StreamTaskListener;
import hudson.util.NullStream;
import hudson.util.StreamTaskListener;
import java.io.PrintStream;
import java.io.PrintWriter;
......
package hudson.model;
import com.thoughtworks.xstream.XStream;
import hudson.CopyOnWrite;
import hudson.FeedAdapter;
import hudson.XmlFile;
import hudson.CopyOnWrite;
import hudson.model.Descriptor.FormException;
import hudson.scm.ChangeLogSet;
import hudson.util.RunList;
......
package hudson.model;
import hudson.Plugin;
import hudson.ExtensionPoint;
import hudson.Plugin;
/**
* Extensible property of {@link User}.
......
package hudson.model;
import hudson.Util;
import hudson.FilePath;
import hudson.util.StreamTaskListener;
import java.io.File;
......@@ -8,7 +8,9 @@ import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
/**
......@@ -39,11 +41,12 @@ public class WorkspaceCleanupThread extends PeriodicWork {
try {
listener = new StreamTaskListener(os);
for (Slave s : h.getSlaves()) {
for (Slave s : h.getSlaves())
process(s);
}
process(h);
} catch (InterruptedException e) {
e.printStackTrace(listener.fatalError("aborted"));
} finally {
os.close();
}
......@@ -52,19 +55,21 @@ public class WorkspaceCleanupThread extends PeriodicWork {
}
}
private void process(Hudson h) {
private void process(Hudson h) throws IOException, InterruptedException {
File jobs = new File(h.getRootDir(), "jobs");
File[] dirs = jobs.listFiles(DIR_FILTER);
if(dirs==null) return;
for (File dir : dirs) {
File ws = new File(dir, "workspace");
FilePath ws = new FilePath(new File(dir, "workspace"));
if(shouldBeDeleted(dir.getName(),ws,h)) {
delete(ws);
}
}
}
private boolean shouldBeDeleted(String jobName, File dir, Node n) {
private boolean shouldBeDeleted(String jobName, FilePath dir, Node n) throws IOException, InterruptedException {
// TODO: the use of remoting is not optimal.
// One remoting can execute "exists", "lastModified", and "delete" all at once.
Job job = Hudson.getInstance().getJob(jobName);
if(job==null)
// no such project anymore
......@@ -86,34 +91,38 @@ public class WorkspaceCleanupThread extends PeriodicWork {
}
private void process(Slave s) {
// TODO: we should be using launcher to execute remote rm -rf
private void process(Slave s) throws InterruptedException {
listener.getLogger().println("Scanning "+s.getNodeName());
File[] dirs = s.getWorkspaceRoot().getLocal().listFiles(DIR_FILTER);
if(dirs ==null) return;
for (File dir : dirs) {
if(shouldBeDeleted(dir.getName(),dir,s))
delete(dir);
try {
List<FilePath> dirs = s.getWorkspaceRoot().list(DIR_FILTER);
if(dirs ==null) return;
for (FilePath dir : dirs) {
if(shouldBeDeleted(dir.getName(),dir,s))
delete(dir);
}
} catch (IOException e) {
e.printStackTrace(listener.error("Failed on "+s.getNodeName()));
}
}
private void delete(File dir) {
private void delete(FilePath dir) throws InterruptedException {
try {
listener.getLogger().println("Deleting "+dir);
Util.deleteRecursive(dir);
dir.deleteRecursive();
} catch (IOException e) {
e.printStackTrace(listener.error("Failed to delete "+dir));
}
}
private static final FileFilter DIR_FILTER = new FileFilter() {
private static class DirectoryFilter implements FileFilter, Serializable {
public boolean accept(File f) {
return f.isDirectory();
}
};
private static final long serialVersionUID = 1L;
}
private static final FileFilter DIR_FILTER = new DirectoryFilter();
private static final long DAY = 1000*60*60*24;
}
package hudson.model.listeners;
import hudson.model.Job;
import hudson.model.Hudson;
import hudson.model.Job;
/**
* Receives notifications about jobs.
......
......@@ -25,8 +25,8 @@ import org.apache.tools.ant.taskdefs.cvslib.CvsVersion;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
......@@ -77,8 +77,8 @@ public class ChangeLogTask extends AbstractCvsTask {
/** Input dir */
private File m_dir;
/** Output file */
private File m_destfile;
/** Output */
private OutputStream m_output;
/** The earliest date at which to start processing entries. */
private Date m_start;
......@@ -111,12 +111,12 @@ public class ChangeLogTask extends AbstractCvsTask {
/**
* Set the output file for the log.
* Set the output stream for the log.
*
* @param destfile The new destfile value
*/
public void setDestfile(final File destfile) {
m_destfile = destfile;
public void setDeststream(final OutputStream destfile) {
m_output = destfile;
}
......@@ -309,7 +309,7 @@ public class ChangeLogTask extends AbstractCvsTask {
if (null == m_dir) {
m_dir = getProject().getBaseDir();
}
if (null == m_destfile) {
if (null == m_output) {
final String message = "Destfile must be set.";
throw new BuildException(message);
......@@ -413,10 +413,10 @@ public class ChangeLogTask extends AbstractCvsTask {
*/
private void writeChangeLog(final CVSEntry[] entrySet)
throws BuildException {
FileOutputStream output = null;
OutputStream output = null;
try {
output = new FileOutputStream(m_destfile);
output = m_output;
final PrintWriter writer =
new PrintWriter(new OutputStreamWriter(output, "UTF-8"));
......
package hudson.scm;
import hudson.model.User;
import hudson.scm.CVSChangeLogSet.CVSChangeLog;
import hudson.util.IOException2;
import org.apache.commons.digester.Digester;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Iterator;
import hudson.model.User;
import hudson.scm.CVSChangeLogSet.CVSChangeLog;
import java.util.List;
/**
* {@link ChangeLogSet} for CVS.
......@@ -59,7 +59,13 @@ public final class CVSChangeLogSet extends ChangeLogSet<CVSChangeLog> {
digester.addCallMethod("*/entry/file/dead","setDead");
digester.addSetNext("*/entry/file","addFile");
digester.parse(f);
try {
digester.parse(f);
} catch (IOException e) {
throw new IOException2("Failed to parse "+f,e);
} catch (SAXException e) {
throw new IOException2("Failed to parse "+f,e);
}
// merge duplicate entries. Ant task somehow seems to report duplicate entries.
for(int i=r.size()-1; i>=0; i--) {
......
......@@ -7,13 +7,12 @@ import hudson.model.BuildListener;
import hudson.model.Descriptor;
import hudson.model.Project;
import hudson.model.TaskListener;
import org.kohsuke.stapler.StaplerRequest;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import org.kohsuke.stapler.StaplerRequest;
/**
* No {@link SCM}.
*
......
package hudson.scm;
import hudson.ExtensionPoint;
import hudson.FilePath;
import hudson.Launcher;
import hudson.ExtensionPoint;
import hudson.model.Build;
import hudson.model.BuildListener;
import hudson.model.Describable;
......@@ -41,8 +41,12 @@ public interface SCM extends Describable<SCM>, ExtensionPoint {
*
* @return true
* if the change is detected.
*
* @throws InterruptedException
* interruption is usually caused by the user aborting the computation.
* this exception should be simply propagated all the way up.
*/
boolean pollChanges(Project project, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException;
boolean pollChanges(Project project, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException, InterruptedException;
/**
* Obtains a fresh workspace of the module(s) into the specified directory
......@@ -65,10 +69,14 @@ public interface SCM extends Describable<SCM>, ExtensionPoint {
* When there's no change, this file should contain an empty entry.
* See {@link AbstractCVSFamilySCM#createEmptyChangeLog(File, BuildListener, String)}.
* @return
* null if the operation fails. The error should be reported to the listener.
* false if the operation fails. The error should be reported to the listener.
* Otherwise return the changes included in this update (if this was an update.)
*
* @throws InterruptedException
* interruption is usually caused by the user aborting the build.
* this exception will cause the build to fail.
*/
boolean checkout(Build build, Launcher launcher, FilePath workspace, BuildListener listener, File changelogFile) throws IOException;
boolean checkout(Build build, Launcher launcher, FilePath workspace, BuildListener listener, File changelogFile) throws IOException, InterruptedException;
/**
* Adds environmental variables for the builds to the given map.
......
......@@ -13,5 +13,5 @@ public class SCMS {
*/
@SuppressWarnings("unchecked") // generic array creation
public static final List<Descriptor<SCM>> SCMS =
Descriptor.toList(NullSCM.DESCRIPTOR,CVSSCM.DESCRIPTOR,SubversionSCM.DESCRIPTOR);
Descriptor.toList(NullSCM.DESCRIPTOR,CVSSCM.DescriptorImpl.DESCRIPTOR,SubversionSCM.DESCRIPTOR);
}
......@@ -3,6 +3,7 @@ package hudson.scm;
import hudson.model.Build;
import hudson.scm.SubversionChangeLogSet.LogEntry;
import hudson.scm.SubversionChangeLogSet.Path;
import hudson.util.IOException2;
import org.apache.commons.digester.Digester;
import org.xml.sax.SAXException;
......@@ -35,7 +36,13 @@ public class SubversionChangeLogParser extends ChangeLogParser {
digester.addBeanPropertySetter("*/logentry/paths/path","value");
digester.addSetNext("*/logentry/paths/path","addPath");
digester.parse(changelogFile);
try {
digester.parse(changelogFile);
} catch (IOException e) {
throw new IOException2("Failed to parse "+changelogFile,e);
} catch (SAXException e) {
throw new IOException2("Failed to parse "+changelogFile,e);
}
return new SubversionChangeLogSet(build,r);
}
......
......@@ -7,9 +7,9 @@ import hudson.scm.SubversionChangeLogSet.LogEntry;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Iterator;
/**
* {@link ChangeLogSet} for Subversion.
......
......@@ -2,7 +2,6 @@ package hudson.scm;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Proc;
import hudson.Util;
import hudson.model.Build;
import hudson.model.BuildListener;
......@@ -184,10 +183,10 @@ public class SubversionSCM extends AbstractCVSFamilySCM {
return revisions;
}
public boolean checkout(Build build, Launcher launcher, FilePath workspace, BuildListener listener, File changelogFile) throws IOException {
public boolean checkout(Build build, Launcher launcher, FilePath workspace, BuildListener listener, File changelogFile) throws IOException, InterruptedException {
boolean result;
if(useUpdate && isUpdatable(workspace,listener)) {
if(useUpdate && isUpdatable(workspace,launcher,listener)) {
result = update(launcher,workspace,listener);
if(!result)
return false;
......@@ -212,7 +211,7 @@ public class SubversionSCM extends AbstractCVSFamilySCM {
// write out the revision file
PrintWriter w = new PrintWriter(new FileOutputStream(getRevisionFile(build)));
try {
Map<String,SvnInfo> revMap = buildRevisionMap(workspace,listener);
Map<String,SvnInfo> revMap = buildRevisionMap(workspace,launcher,listener);
for (Entry<String,SvnInfo> e : revMap.entrySet()) {
w.println( e.getKey() +'/'+ e.getValue().revision );
}
......@@ -255,13 +254,13 @@ public class SubversionSCM extends AbstractCVSFamilySCM {
* @param subject
* The target to run "svn info". Either local path or remote URL.
*/
public static SvnInfo parse(String subject, Map env, FilePath workspace, TaskListener listener) throws IOException {
public static SvnInfo parse(String subject, Map env, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException {
String cmd = DESCRIPTOR.getSvnExe()+" info --xml "+subject;
listener.getLogger().println("$ "+cmd);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int r = new Proc(cmd,env,baos,workspace.getLocal()).join();
int r = launcher.launch(cmd,env,baos,workspace).join();
if(r!=0) {
// failed. to allow user to diagnose the problem, send output to log
listener.getLogger().write(baos.toByteArray());
......@@ -300,7 +299,7 @@ public class SubversionSCM extends AbstractCVSFamilySCM {
* @return
* null if the parsing somehow fails. Otherwise a map from module names to revisions.
*/
private Map<String,SvnInfo> buildRevisionMap(FilePath workspace, TaskListener listener) throws IOException {
private Map<String,SvnInfo> buildRevisionMap(FilePath workspace, Launcher launcher, TaskListener listener) throws IOException {
PrintStream logger = listener.getLogger();
Map<String/*module name*/,SvnInfo> revisions = new HashMap<String,SvnInfo>();
......@@ -310,7 +309,7 @@ public class SubversionSCM extends AbstractCVSFamilySCM {
// invoke the "svn info"
for( String module : getModuleDirNames() ) {
// parse the output
SvnInfo info = SvnInfo.parse(module,env,workspace,listener);
SvnInfo info = SvnInfo.parse(module,env,workspace,launcher,listener);
revisions.put(module,info);
logger.println("Revision:"+info.revision);
}
......@@ -345,15 +344,15 @@ public class SubversionSCM extends AbstractCVSFamilySCM {
/**
* Returns true if we can use "svn update" instead of "svn checkout"
*/
private boolean isUpdatable(FilePath workspace,BuildListener listener) {
private boolean isUpdatable(FilePath workspace,Launcher launcher,BuildListener listener) {
StringTokenizer tokens = new StringTokenizer(modules);
while(tokens.hasMoreTokens()) {
String url = tokens.nextToken();
String moduleName = getLastPathComponent(url);
File module = workspace.child(moduleName).getLocal();
FilePath module = workspace.child(moduleName);
try {
SvnInfo svnInfo = SvnInfo.parse(moduleName, createEnvVarMap(false), workspace, listener);
SvnInfo svnInfo = SvnInfo.parse(moduleName, createEnvVarMap(false), workspace, launcher, listener);
if(!svnInfo.url.equals(url)) {
listener.getLogger().println("Checking out a fresh workspace because the workspace is not "+url);
return false;
......@@ -369,13 +368,13 @@ public class SubversionSCM extends AbstractCVSFamilySCM {
public boolean pollChanges(Project project, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException {
// current workspace revision
Map<String,SvnInfo> wsRev = buildRevisionMap(workspace,listener);
Map<String,SvnInfo> wsRev = buildRevisionMap(workspace,launcher,listener);
Map env = createEnvVarMap(false);
// check the corresponding remote revision
for (SvnInfo localInfo : wsRev.values()) {
SvnInfo remoteInfo = SvnInfo.parse(localInfo.url,env,workspace,listener);
SvnInfo remoteInfo = SvnInfo.parse(localInfo.url,env,workspace,launcher,listener);
listener.getLogger().println("Revision:"+remoteInfo.revision);
if(remoteInfo.revision > localInfo.revision)
return true; // change found
......@@ -471,7 +470,7 @@ public class SubversionSCM extends AbstractCVSFamilySCM {
if(svnExe==null || svnExe.equals("")) svnExe="svn";
ByteArrayOutputStream out = new ByteArrayOutputStream();
l.launch(new String[]{svnExe,"--version"},new String[0],out,FilePath.RANDOM).join();
l.launch(new String[]{svnExe,"--version"},new String[0],out,null).join();
// parse the first line for version
BufferedReader r = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(out.toByteArray())));
......@@ -501,7 +500,7 @@ public class SubversionSCM extends AbstractCVSFamilySCM {
protected void check() throws IOException, ServletException {
String svnExe = request.getParameter("exe");
Version v = version(new Launcher(TaskListener.NULL),svnExe);
Version v = version(new Launcher.LocalLauncher(TaskListener.NULL),svnExe);
if(v==null) {
error("Failed to check subversion version info. Is this a valid path?");
return;
......
......@@ -12,7 +12,6 @@ import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.util.Map;
......
package hudson.tasks;
import hudson.model.BuildListener;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
/**
* {@link BuildStep} that uses Ant.
*
* Contains helper code.
*
* @author Kohsuke Kawaguchi
*/
public abstract class AntBasedPublisher extends Publisher {
protected final void execTask(Task task, BuildListener listener) {
try {
task.execute();
} catch( BuildException e ) {
// failing to archive isn't a fatal error
e.printStackTrace(listener.error(e.getMessage()));
}
}
}
package hudson.tasks;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.model.Action;
import hudson.model.Build;
import hudson.model.BuildListener;
import hudson.model.Descriptor;
import hudson.model.Project;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.taskdefs.Delete;
import org.apache.tools.ant.types.FileSet;
import org.kohsuke.stapler.StaplerRequest;
import java.io.File;
......@@ -20,7 +17,7 @@ import java.io.IOException;
*
* @author Kohsuke Kawaguchi
*/
public class ArtifactArchiver extends AntBasedPublisher {
public class ArtifactArchiver extends Publisher {
/**
* Comma-separated list of files/directories to be archived.
......@@ -45,36 +42,19 @@ public class ArtifactArchiver extends AntBasedPublisher {
return latestOnly;
}
public boolean prebuild(Build build, BuildListener listener) {
listener.getLogger().println("Removing artifacts from the previous build");
File dir = build.getArtifactsDir();
if(!dir.exists()) return true;
Delete delTask = new Delete();
delTask.setProject(new org.apache.tools.ant.Project());
delTask.setDir(dir);
delTask.setIncludes(artifacts);
execTask(delTask,listener);
return true;
}
public boolean perform(Build build, Launcher launcher, BuildListener listener) {
public boolean perform(Build build, Launcher launcher, BuildListener listener) throws InterruptedException {
Project p = build.getProject();
Copy copyTask = new Copy();
copyTask.setProject(new org.apache.tools.ant.Project());
File dir = build.getArtifactsDir();
dir.mkdirs();
copyTask.setTodir(dir);
FileSet src = new FileSet();
src.setDir(p.getWorkspace().getLocal());
src.setIncludes(artifacts);
copyTask.addFileset(src);
execTask(copyTask, listener);
try {
p.getWorkspace().copyRecursiveTo(artifacts,new FilePath(dir));
} catch (IOException e) {
Util.displayIOException(e,listener);
e.printStackTrace(listener.error("Failed to archive artifacts: "+artifacts));
return true;
}
if(latestOnly) {
Build b = p.getLastSuccessfulBuild();
......
......@@ -9,58 +9,28 @@ import hudson.model.Descriptor;
import hudson.model.Project;
import org.kohsuke.stapler.StaplerRequest;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
/**
* Executes commands by using Windows batch file.
*
* @author Kohsuke Kawaguchi
*/
public class BatchFile extends Builder {
private final String command;
public class BatchFile extends CommandInterpreter {
public BatchFile(String command) {
this.command = command;
super(command);
}
public String getCommand() {
return command;
protected String[] buildCommandLine(FilePath script) {
return new String[] {script.getRemote()};
}
public boolean perform(Build build, Launcher launcher, BuildListener listener) {
Project proj = build.getProject();
FilePath ws = proj.getWorkspace();
FilePath script=null;
try {
try {
script = ws.createTempFile("hudson",".bat");
Writer w = new FileWriter(script.getLocal());
w.write(command);
w.write("\r\nexit %ERRORLEVEL%");
w.close();
} catch (IOException e) {
Util.displayIOException(e,listener);
e.printStackTrace( listener.fatalError("Unable to produce a batch file") );
return false;
}
String[] cmd = new String[] {script.getRemote()};
protected String getContents() {
return command+"\r\nexit %ERRORLEVEL%";
}
int r;
try {
r = launcher.launch(cmd,build.getEnvVars(),listener.getLogger(),ws).join();
} catch (IOException e) {
Util.displayIOException(e,listener);
e.printStackTrace( listener.fatalError("command execution failed") );
r = -1;
}
return r==0;
} finally {
if(script!=null)
script.delete();
}
protected String getFileExtension() {
return ".bat";
}
public Descriptor<Builder> getDescriptor() {
......
......@@ -9,6 +9,7 @@ import hudson.model.Project;
import hudson.tasks.junit.JUnitResultArchiver;
import java.util.List;
import java.io.IOException;
/**
* One step of the whole build process.
......@@ -32,8 +33,20 @@ public interface BuildStep {
* @return
* true if the build can continue, false if there was an error
* and the build needs to be aborted.
*
* @throws InterruptedException
* If the build is interrupted by the user (in an attempt to abort the build.)
* Normally the {@link BuildStep} implementations may simply forward the exception
* it got from its lower-level functions.
* @throws IOException
* If the implementation wants to abort the processing when an {@link IOException}
* happens, it can simply propagate the exception to the caller. This will cause
* the build to fail, with the default error message.
* Implementations are encouraged to catch {@link IOException} on its own to
* provide a better error message, if it can do so, so that users have better
* understanding on why it failed.
*/
boolean perform(Build build, Launcher launcher, BuildListener listener);
boolean perform(Build build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException;
/**
* Returns an action object if this {@link BuildStep} has an action
......@@ -68,7 +81,7 @@ public interface BuildStep {
ArtifactArchiver.DESCRIPTOR,
Fingerprinter.DESCRIPTOR,
JavadocArchiver.DESCRIPTOR,
JUnitResultArchiver.DESCRIPTOR,
JUnitResultArchiver.DescriptorImpl.DESCRIPTOR,
BuildTrigger.DESCRIPTOR,
Mailer.DESCRIPTOR
);
......
......@@ -7,8 +7,8 @@ import hudson.model.BuildListener;
import hudson.model.Describable;
import hudson.model.Project;
import java.util.Map;
import java.io.IOException;
import java.util.Map;
/**
* Pluggability point for performing pre/post actions for the build process.
......
package hudson.tasks;
import hudson.model.Describable;
import hudson.ExtensionPoint;
import hudson.model.Action;
import hudson.model.Project;
import hudson.model.Build;
import hudson.model.BuildListener;
import hudson.ExtensionPoint;
import hudson.model.Describable;
import hudson.model.Project;
/**
* {@link BuildStep}s that perform the actual build.
......
package hudson.tasks;
import hudson.model.Build;
import hudson.model.BuildListener;
import hudson.model.Project;
import hudson.Launcher;
import hudson.FilePath;
import hudson.Util;
import java.io.IOException;
/**
* Common part between {@link Shell} and {@link BatchFile}.
*
* @author Kohsuke Kawaguchi
*/
public abstract class CommandInterpreter extends Builder {
/**
* Command to execute. The format depends on the actual {@link CommandInterpreter} implementation.
*/
protected final String command;
public CommandInterpreter(String command) {
this.command = command;
}
public final String getCommand() {
return command;
}
public boolean perform(Build build, Launcher launcher, BuildListener listener) throws InterruptedException {
Project proj = build.getProject();
FilePath ws = proj.getWorkspace();
FilePath script=null;
try {
try {
script = ws.createTextTempFile("hudson", getFileExtension(), getContents());
} catch (IOException e) {
Util.displayIOException(e,listener);
e.printStackTrace( listener.fatalError("Unable to produce a script file") );
return false;
}
String[] cmd = buildCommandLine(script);
int r;
try {
r = launcher.launch(cmd,build.getEnvVars(),listener.getLogger(),ws).join();
} catch (IOException e) {
Util.displayIOException(e,listener);
e.printStackTrace( listener.fatalError("command execution failed") );
r = -1;
}
return r==0;
} finally {
try {
if(script!=null)
script.delete();
} catch (IOException e) {
Util.displayIOException(e,listener);
e.printStackTrace( listener.fatalError("Unable to delete script file "+script) );
}
}
}
protected abstract String[] buildCommandLine(FilePath script);
protected abstract String getContents();
protected abstract String getFileExtension();
}
package hudson.tasks;
import hudson.Launcher;
import hudson.remoting.VirtualChannel;
import hudson.util.IOException2;
import hudson.FilePath.FileCallable;
import hudson.model.Action;
import hudson.model.Build;
import hudson.model.BuildListener;
import hudson.model.Descriptor;
import hudson.model.Fingerprint;
import hudson.model.Fingerprint.BuildPtr;
import hudson.model.Hudson;
import hudson.model.Project;
import hudson.model.Result;
import hudson.model.Fingerprint.BuildPtr;
import hudson.model.FingerprintMap;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.types.FileSet;
import org.kohsuke.stapler.StaplerRequest;
......@@ -17,6 +21,7 @@ import org.kohsuke.stapler.StaplerRequest;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.security.DigestInputStream;
import java.security.MessageDigest;
......@@ -24,9 +29,10 @@ import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.Set;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.List;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
......@@ -35,7 +41,7 @@ import java.util.logging.Logger;
*
* @author Kohsuke Kawaguchi
*/
public class Fingerprinter extends Publisher {
public class Fingerprinter extends Publisher implements Serializable {
/**
* Comma-separated list of files/directories to be fingerprinted.
......@@ -60,79 +66,110 @@ public class Fingerprinter extends Publisher {
return recordBuildArtifacts;
}
public boolean perform(Build build, Launcher launcher, BuildListener listener) {
listener.getLogger().println("Recording fingerprints");
public boolean perform(Build build, Launcher launcher, BuildListener listener) throws InterruptedException {
try {
listener.getLogger().println("Recording fingerprints");
Map<String,String> record = new HashMap<String,String>();
Map<String,String> record = new HashMap<String,String>();
MessageDigest md5;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
// I don't think this is possible, but check anyway
e.printStackTrace(listener.error("MD5 not installed"));
build.setResult(Result.FAILURE);
return true;
}
if(targets.length()!=0)
record(build, md5, listener, record, targets);
if(targets.length()!=0)
record(build, listener, record, targets);
if(recordBuildArtifacts) {
ArtifactArchiver aa = (ArtifactArchiver) build.getProject().getPublishers().get(ArtifactArchiver.DESCRIPTOR);
if(aa==null) {
// configuration error
listener.error("Build artifacts are supposed to be fingerprinted, but build artifact archiving is not configured");
build.setResult(Result.FAILURE);
return true;
if(recordBuildArtifacts) {
ArtifactArchiver aa = (ArtifactArchiver) build.getProject().getPublishers().get(ArtifactArchiver.DESCRIPTOR);
if(aa==null) {
// configuration error
listener.error("Build artifacts are supposed to be fingerprinted, but build artifact archiving is not configured");
build.setResult(Result.FAILURE);
return true;
}
record(build, listener, record, aa.getArtifacts() );
}
record(build, md5, listener, record, aa.getArtifacts() );
}
build.getActions().add(new FingerprintAction(build,record));
build.getActions().add(new FingerprintAction(build,record));
} catch (IOException e) {
e.printStackTrace(listener.error("Failed to record fingerprints"));
build.setResult(Result.FAILURE);
}
// failing to record fingerprints is an error but not fatal
return true;
}
private void record(Build build, MessageDigest md5, BuildListener listener, Map<String,String> record, String targets) {
Project p = build.getProject();
FileSet src = new FileSet();
File baseDir = p.getWorkspace().getLocal();
src.setDir(baseDir);
src.setIncludes(targets);
byte[] buf = new byte[8192];
private void record(Build build, BuildListener listener, Map<String,String> record, final String targets) throws IOException, InterruptedException {
final class Record implements Serializable {
final boolean produced;
final String relativePath;
final String fileName;
final byte[] md5sum;
public Record(boolean produced, String relativePath, String fileName, byte[] md5sum) {
this.produced = produced;
this.relativePath = relativePath;
this.fileName = fileName;
this.md5sum = md5sum;
}
DirectoryScanner ds = src.getDirectoryScanner(new org.apache.tools.ant.Project());
for( String f : ds.getIncludedFiles() ) {
File file = new File(baseDir,f);
Fingerprint addRecord(Build build) throws IOException {
FingerprintMap map = Hudson.getInstance().getFingerprintMap();
return map.getOrCreate(produced?build:null, fileName, md5sum);
}
// consider the file to be produced by this build only if the timestamp
// is newer than when the build has started.
boolean produced = build.getTimestamp().getTimeInMillis() <= file.lastModified();
private static final long serialVersionUID = 1L;
}
try {
md5.reset(); // technically not necessary, but hey, just to be safe
DigestInputStream in =new DigestInputStream(new FileInputStream(file),md5);
try {
while(in.read(buf)>0)
; // simply discard the input
} finally {
in.close();
Project p = build.getProject();
final long buildTimestamp = build.getTimestamp().getTimeInMillis();
List<Record> records = p.getWorkspace().act(new FileCallable<List<Record>>() {
public List<Record> invoke(File baseDir, VirtualChannel channel) throws IOException {
List<Record> results = new ArrayList<Record>();
FileSet src = new FileSet();
src.setDir(baseDir);
src.setIncludes(targets);
byte[] buf = new byte[8192];
MessageDigest md5 = createMD5();
DirectoryScanner ds = src.getDirectoryScanner(new org.apache.tools.ant.Project());
for( String f : ds.getIncludedFiles() ) {
File file = new File(baseDir,f);
// consider the file to be produced by this build only if the timestamp
// is newer than when the build has started.
boolean produced = buildTimestamp <= file.lastModified();
try {
md5.reset(); // technically not necessary, but hey, just to be safe
DigestInputStream in =new DigestInputStream(new FileInputStream(file),md5);
try {
while(in.read(buf)>0)
; // simply discard the input
} finally {
in.close();
}
results.add(new Record(produced,f,file.getName(),md5.digest()));
} catch (IOException e) {
throw new IOException2("Failed to compute digest for "+file,e);
}
}
Fingerprint fp = Hudson.getInstance().getFingerprintMap().getOrCreate(
produced?build:null, file.getName(), md5.digest());
if(fp==null) {
listener.error("failed to record fingerprint for "+file);
continue;
}
fp.add(build);
record.put(f,fp.getHashString());
} catch (IOException e) {
e.printStackTrace(listener.error("Failed to compute digest for "+file));
return results;
}
});
for (Record r : records) {
Fingerprint fp = r.addRecord(build);
if(fp==null) {
listener.error("failed to record fingerprint for "+r.relativePath);
continue;
}
fp.add(build);
record.put(r.relativePath,fp.getHashString());
}
}
......@@ -140,6 +177,15 @@ public class Fingerprinter extends Publisher {
return DESCRIPTOR;
}
private static MessageDigest createMD5() throws IOException2 {
try {
return MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
// I don't think this is possible, but check anyway
throw new IOException2("MD5 not installed",e);
}
}
public static final Descriptor<Publisher> DESCRIPTOR = new Descriptor<Publisher>(Fingerprinter.class) {
public String getDisplayName() {
......@@ -238,4 +284,6 @@ public class Fingerprinter extends Publisher {
}
private static final Logger logger = Logger.getLogger(Fingerprinter.class.getName());
private static final long serialVersionUID = 1L;
}
package hudson.tasks;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.model.Action;
import hudson.model.Build;
import hudson.model.BuildListener;
......@@ -8,8 +10,7 @@ import hudson.model.Descriptor;
import hudson.model.DirectoryHolder;
import hudson.model.Project;
import hudson.model.ProminentProjectAction;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.types.FileSet;
import hudson.model.Result;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
......@@ -22,7 +23,7 @@ import java.io.IOException;
*
* @author Kohsuke Kawaguchi
*/
public class JavadocArchiver extends AntBasedPublisher {
public class JavadocArchiver extends Publisher {
/**
* Path to the Javadoc directory in the workspace.
*/
......@@ -43,31 +44,19 @@ public class JavadocArchiver extends AntBasedPublisher {
return new File(project.getRootDir(),"javadoc");
}
public boolean perform(Build build, Launcher launcher, BuildListener listener) {
// TODO: run tar or something for better remote copy
File javadoc = new File(build.getParent().getWorkspace().getLocal(), javadocDir);
if(!javadoc.exists()) {
listener.error("The specified Javadoc directory doesn't exist: "+javadoc);
return false;
}
if(!javadoc.isDirectory()) {
listener.error("The specified Javadoc directory isn't a directory: "+javadoc);
return false;
}
public boolean perform(Build build, Launcher launcher, BuildListener listener) throws InterruptedException {
listener.getLogger().println("Publishing Javadoc");
File target = getJavadocDir(build.getParent());
target.mkdirs();
FilePath javadoc = build.getParent().getWorkspace().child(javadocDir);
FilePath target = new FilePath(getJavadocDir(build.getParent()));
Copy copyTask = new Copy();
copyTask.setProject(new org.apache.tools.ant.Project());
copyTask.setTodir(target);
FileSet src = new FileSet();
src.setDir(javadoc);
copyTask.addFileset(src);
execTask(copyTask, listener);
try {
javadoc.copyRecursiveTo("**/*",target);
} catch (IOException e) {
Util.displayIOException(e,listener);
e.printStackTrace(listener.fatalError("Unable to copy Javadoc from "+javadoc+" to "+target));
build.setResult(Result.FAILURE);
}
return true;
}
......@@ -110,8 +99,8 @@ public class JavadocArchiver extends AntBasedPublisher {
return "help.gif";
}
public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
serveFile(req, rsp, getJavadocDir(project), "help.gif", false);
public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, InterruptedException {
serveFile(req, rsp, new FilePath(getJavadocDir(project)), "help.gif", false);
}
}
}
......@@ -5,14 +5,13 @@ import hudson.model.Descriptor;
import hudson.model.Job;
import hudson.model.Run;
import hudson.scm.SCM;
import org.kohsuke.stapler.StaplerRequest;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Calendar;
import java.util.GregorianCalendar;
import org.kohsuke.stapler.StaplerRequest;
/**
* Deletes old log files.
*
......
......@@ -2,6 +2,7 @@ package hudson.tasks;
import hudson.Launcher;
import hudson.Util;
import hudson.FilePath;
import hudson.model.Build;
import hudson.model.BuildListener;
import hudson.model.Descriptor;
......@@ -71,7 +72,7 @@ public class Mailer extends Publisher {
private transient String subject;
private transient boolean failureOnly;
public boolean perform(Build build, Launcher launcher, BuildListener listener) {
public boolean perform(Build build, Launcher launcher, BuildListener listener) throws InterruptedException {
try {
MimeMessage mail = getMail(build, listener);
if(mail!=null) {
......@@ -94,7 +95,7 @@ public class Mailer extends Publisher {
return true;
}
private MimeMessage getMail(Build build, BuildListener listener) throws MessagingException {
private MimeMessage getMail(Build build, BuildListener listener) throws MessagingException, InterruptedException {
if(build.getResult()==Result.FAILURE) {
return createFailureMail(build, listener);
}
......@@ -151,7 +152,7 @@ public class Mailer extends Publisher {
}
}
private MimeMessage createFailureMail(Build build, BuildListener listener) throws MessagingException {
private MimeMessage createFailureMail(Build build, BuildListener listener) throws MessagingException, InterruptedException {
MimeMessage msg = createEmptyMail(build, listener);
msg.setSubject(getSubject(build, "Build failed in Hudson: "));
......@@ -197,7 +198,7 @@ public class Mailer extends Publisher {
// URL which has already been corrected in a subsequent build. To fix, archive.
workspaceUrl = baseUrl + Util.encode(build.getProject().getUrl()) + "ws/";
artifactUrl = baseUrl + Util.encode(build.getUrl()) + "artifact/";
File workspaceDir = build.getProject().getWorkspace().getLocal();
FilePath ws = build.getProject().getWorkspace();
// Match either file or URL patterns, i.e. either
// c:\hudson\workdir\jobs\foo\workspace\src\Foo.java
// file:/c:/hudson/workdir/jobs/foo/workspace/src/Foo.java
......@@ -208,7 +209,7 @@ public class Mailer extends Publisher {
// workspaceDir will not normally end with one;
// workspaceDir.toURI() will end with '/' if and only if workspaceDir.exists() at time of call
wsPattern = Pattern.compile("(" +
quoteRegexp(workspaceDir.getPath()) + "|" + quoteRegexp(workspaceDir.toURI().toString()) + ")[/\\\\]?([^:#\\s]*)");
quoteRegexp(ws.getRemote()) + "|" + quoteRegexp(ws.toURI().toString()) + ")[/\\\\]?([^:#\\s]*)");
}
for (int i = start; i < lines.length; i++) {
String line = lines[i];
......
......@@ -12,7 +12,6 @@ import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.util.Map;
......
package hudson.tasks;
import hudson.model.Describable;
import hudson.ExtensionPoint;
import hudson.model.Action;
import hudson.model.Build;
import hudson.model.BuildListener;
import hudson.model.Action;
import hudson.model.Describable;
import hudson.model.Project;
import hudson.ExtensionPoint;
/**
* {@link BuildStep}s that run after the build is completed.
......
......@@ -10,10 +10,7 @@ import static hudson.model.Hudson.isWindows;
import hudson.model.Project;
import org.kohsuke.stapler.StaplerRequest;
import javax.servlet.http.HttpServletRequest;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.Map;
/**
......@@ -21,17 +18,15 @@ import java.util.Map;
*
* @author Kohsuke Kawaguchi
*/
public class Shell extends Builder {
private final String command;
public class Shell extends CommandInterpreter {
public Shell(String command) {
this.command = fixCrLf(command);
super(fixCrLf(command));
}
/**
* Fix CR/LF in the string according to the platform we are running on.
*/
private String fixCrLf(String s) {
private static String fixCrLf(String s) {
// eliminate CR
int idx;
while((idx=s.indexOf("\r\n"))!=-1)
......@@ -50,41 +45,16 @@ public class Shell extends Builder {
return s;
}
public String getCommand() {
return command;
protected String[] buildCommandLine(FilePath script) {
return new String[] { DESCRIPTOR.getShell(),"-xe",script.getRemote()};
}
public boolean perform(Build build, Launcher launcher, BuildListener listener) {
Project proj = build.getProject();
FilePath ws = proj.getWorkspace();
FilePath script=null;
try {
try {
script = ws.createTempFile("hudson","sh");
Writer w = new FileWriter(script.getLocal());
w.write(command);
w.close();
} catch (IOException e) {
Util.displayIOException(e,listener);
e.printStackTrace( listener.fatalError("Unable to produce a script file") );
return false;
}
String[] cmd = new String[] { DESCRIPTOR.getShell(),"-xe",script.getRemote()};
protected String getContents() {
return command;
}
int r;
try {
r = launcher.launch(cmd,build.getEnvVars(),listener.getLogger(),ws).join();
} catch (IOException e) {
Util.displayIOException(e,listener);
e.printStackTrace( listener.fatalError("command execution failed") );
r = -1;
}
return r==0;
} finally {
if(script!=null)
script.delete();
}
protected String getFileExtension() {
return ".sh";
}
public Descriptor<Builder> getDescriptor() {
......
package hudson.tasks.junit;
import java.io.IOException;
/**
* Used to signal an orderly abort of the processing.
*/
class AbortException extends IOException {
public AbortException(String msg) {
super(msg);
}
private static final long serialVersionUID = 1L;
}
package hudson.tasks.junit;
import hudson.Launcher;
import hudson.model.Action;
import hudson.remoting.VirtualChannel;
import hudson.FilePath.FileCallable;
import hudson.model.Build;
import hudson.model.BuildListener;
import hudson.model.Descriptor;
import hudson.model.Result;
import hudson.tasks.AntBasedPublisher;
import hudson.tasks.Publisher;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.FileSet;
import org.kohsuke.stapler.StaplerRequest;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
/**
* Generates HTML report from JUnit test result XML files.
*
* @author Kohsuke Kawaguchi
*/
public class JUnitResultArchiver extends AntBasedPublisher {
public class JUnitResultArchiver extends Publisher implements Serializable {
/**
* {@link FileSet} "includes" string, like "foo/bar/*.xml"
......@@ -29,21 +33,39 @@ public class JUnitResultArchiver extends AntBasedPublisher {
this.testResults = testResults;
}
public boolean perform(Build build, Launcher launcher, BuildListener listener) {
FileSet fs = new FileSet();
Project p = new Project();
fs.setProject(p);
fs.setDir(build.getProject().getWorkspace().getLocal());
fs.setIncludes(testResults);
DirectoryScanner ds = fs.getDirectoryScanner(p);
if(ds.getIncludedFiles().length==0) {
listener.getLogger().println("No test report files were found. Configuration error?");
// no test result. Most likely a configuration error or fatal problem
public boolean perform(Build build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
TestResult result;
listener.getLogger().println("Recording test results");
try {
final long buildTime = build.getTimestamp().getTimeInMillis();
result = build.getProject().getWorkspace().act(new FileCallable<TestResult>() {
public TestResult invoke(File ws, VirtualChannel channel) throws IOException {
FileSet fs = new FileSet();
Project p = new Project();
fs.setProject(p);
fs.setDir(ws);
fs.setIncludes(testResults);
DirectoryScanner ds = fs.getDirectoryScanner(p);
if(ds.getIncludedFiles().length==0) {
// no test result. Most likely a configuration error or fatal problem
throw new AbortException("No test report files were found. Configuration error?");
}
return new TestResult(buildTime,ds);
}
});
} catch (AbortException e) {
listener.getLogger().println(e.getMessage());
build.setResult(Result.FAILURE);
return true;
}
TestResultAction action = new TestResultAction(build, ds, listener);
TestResultAction action = new TestResultAction(build, result, listener);
build.getActions().add(action);
TestResult r = action.getResult();
......@@ -66,10 +88,18 @@ public class JUnitResultArchiver extends AntBasedPublisher {
public Descriptor<Publisher> getDescriptor() {
return DESCRIPTOR;
return DescriptorImpl.DESCRIPTOR;
}
public static final Descriptor<Publisher> DESCRIPTOR = new Descriptor<Publisher>(JUnitResultArchiver.class) {
private static final long serialVersionUID = 1L;
public static class DescriptorImpl extends Descriptor<Publisher> {
public static final Descriptor<Publisher> DESCRIPTOR = new DescriptorImpl();
public DescriptorImpl() {
super(JUnitResultArchiver.class);
}
public String getDisplayName() {
return "Publish JUnit test result report";
}
......@@ -77,5 +107,5 @@ public class JUnitResultArchiver extends AntBasedPublisher {
public Publisher newInstance(StaplerRequest req) {
return new JUnitResultArchiver(req.getParameter("junitreport_includes"));
}
};
}
}
......@@ -6,6 +6,7 @@ import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
......@@ -22,7 +23,7 @@ import java.util.List;
*
* @author Kohsuke Kawaguchi
*/
public final class SuiteResult {
public final class SuiteResult implements Serializable {
private final String name;
private final String stdout;
private final String stderr;
......
......@@ -3,12 +3,14 @@ package hudson.tasks.junit;
import hudson.model.Build;
import hudson.model.ModelObject;
import java.io.Serializable;
/**
* Base class for all test result objects.
*
* @author Kohsuke Kawaguchi
*/
public abstract class TestObject implements ModelObject {
public abstract class TestObject implements ModelObject, Serializable {
public abstract Build getOwner();
/**
......
package hudson.tasks.junit;
import hudson.model.Build;
import hudson.model.BuildListener;
import hudson.util.IOException2;
import org.apache.tools.ant.DirectoryScanner;
import org.dom4j.DocumentException;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
......@@ -38,7 +39,8 @@ public final class TestResult extends MetaTabulatedResult {
*/
private transient Map<String,PackageResult> byPackages;
/*package*/ transient TestResultAction parent;
// set during the freeze phase
private transient TestResultAction parent;
/**
* Number of all tests.
......@@ -53,16 +55,12 @@ public final class TestResult extends MetaTabulatedResult {
* Creates an empty result.
*/
TestResult() {
freeze();
}
TestResult(TestResultAction parent, DirectoryScanner results, BuildListener listener) {
this.parent = parent;
TestResult(long buildTime, DirectoryScanner results) throws IOException {
String[] includedFiles = results.getIncludedFiles();
File baseDir = results.getBasedir();
long buildTime = parent.owner.getTimestamp().getTimeInMillis();
for (String value : includedFiles) {
File reportFile = new File(baseDir, value);
try {
......@@ -70,11 +68,9 @@ public final class TestResult extends MetaTabulatedResult {
// only count files that were actually updated during this build
suites.add(new SuiteResult(reportFile));
} catch (DocumentException e) {
e.printStackTrace(listener.error("Failed to read "+reportFile));
throw new IOException2("Failed to read "+reportFile,e);
}
}
freeze();
}
public String getDisplayName() {
......@@ -137,7 +133,8 @@ public final class TestResult extends MetaTabulatedResult {
/**
* Builds up the transient part of the data structure.
*/
void freeze() {
void freeze(TestResultAction parent) {
this.parent = parent;
suitesByName = new HashMap<String,SuiteResult>();
totalTests = 0;
failedTests = new ArrayList<CaseResult>();
......
......@@ -35,22 +35,22 @@ public class TestResultAction extends AbstractTestResultAction<TestResultAction>
private Integer totalCount;
TestResultAction(Build owner, DirectoryScanner results, BuildListener listener) {
TestResultAction(Build owner, TestResult result, BuildListener listener) {
super(owner);
TestResult r = new TestResult(this,results,listener);
result.freeze(this);
totalCount = r.getTotalCount();
failCount = r.getFailCount();
totalCount = result.getTotalCount();
failCount = result.getFailCount();
// persist the data
try {
getDataFile().write(r);
getDataFile().write(result);
} catch (IOException e) {
e.printStackTrace(listener.fatalError("Failed to save the JUnit test result"));
}
this.result = new WeakReference<TestResult>(r);
this.result = new WeakReference<TestResult>(result);
}
private XmlFile getDataFile() {
......@@ -100,8 +100,7 @@ public class TestResultAction extends AbstractTestResultAction<TestResultAction>
logger.log(Level.WARNING, "Failed to load "+getDataFile(),e);
r = new TestResult(); // return a dummy
}
r.parent = this;
r.freeze();
r.freeze(this);
return r;
}
......
package hudson.tasks.test;
import hudson.Functions;
import hudson.model.Action;
import hudson.model.Build;
import hudson.model.Project;
import hudson.model.Result;
import hudson.util.ChartUtil;
import hudson.util.ColorPalette;
import hudson.util.DataSetBuilder;
import hudson.util.ShiftedCategoryAxis;
import hudson.util.ColorPalette;
import hudson.Functions;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
......
......@@ -10,7 +10,6 @@ import hudson.model.TaskListener;
import hudson.util.StreamTaskListener;
import org.kohsuke.stapler.StaplerRequest;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
......
......@@ -2,7 +2,6 @@ package hudson.triggers;
import antlr.ANTLRException;
import hudson.model.Descriptor;
import org.kohsuke.stapler.StaplerRequest;
/**
......
package hudson.util;
import org.jfree.chart.JFreeChart;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.jfree.chart.JFreeChart;
import javax.servlet.ServletOutputStream;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import java.awt.Font;
import java.awt.HeadlessException;
import java.awt.image.BufferedImage;
......
package hudson.util;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
/**
* {@link ThreadFactory} that creates daemon threads.
*
* @author Kohsuke Kawaguchi
*/
public class DaemonThreadFactory implements ThreadFactory {
private final ThreadFactory core;
public DaemonThreadFactory() {
this(Executors.defaultThreadFactory());
}
public DaemonThreadFactory(ThreadFactory core) {
this.core = core;
}
public Thread newThread(Runnable r) {
Thread t = core.newThread(r);
t.setDaemon(true);
return t;
}
}
package hudson.util;
import org.apache.commons.beanutils.Converter;
/**
* {@link Converter} for enums. Used for form binding.
* @author Kohsuke Kawaguchi
*/
public class EnumConverter implements Converter {
public Object convert(Class aClass, Object object) {
return Enum.valueOf(aClass,object.toString());
}
}
package hudson.util;
import hudson.Util;
import hudson.model.Hudson;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import java.io.IOException;
import java.io.File;
import hudson.model.Hudson;
import hudson.Util;
import java.io.IOException;
/**
* @author Kohsuke Kawaguchi
......
......@@ -3,8 +3,8 @@ package hudson.util;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import net.sf.retrotranslator.runtime.java.lang.Enum_;
/**
......
package hudson.util;
import com.thoughtworks.xstream.converters.collections.CollectionConverter;
import com.thoughtworks.xstream.alias.CannotResolveClassException;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.mapper.Mapper;
import com.thoughtworks.xstream.converters.collections.CollectionConverter;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.alias.CannotResolveClassException;
import com.thoughtworks.xstream.mapper.Mapper;
import java.util.Collection;
......
此差异已折叠。
<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">
<f:entry title="launch command">
<f:textbox name="slave.command" value="${slave.command}"/>
</f:entry>
</j:jelly>
\ No newline at end of file
......@@ -16,7 +16,7 @@
<tr>
<th class="pane" colspan="3">
<a href="${rootURL}/computer/${c.displayName}">${c.displayName}</a>
<j:if test="${c.temporarilyOffline}">(offline)</j:if>
<j:if test="${c.offline}">(offline)</j:if>
</th>
</tr>
</j:otherwise>
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册