提交 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.remoting.Callable;
import hudson.remoting.Channel;
import hudson.remoting.Pipe;
import hudson.remoting.RemoteOutputStream;
import hudson.remoting.VirtualChannel;
import hudson.remoting.DelegatingCallable;
import hudson.util.IOException2;
import hudson.model.Hudson;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.types.FileSet;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import java.net.URI;
/**
* {@link File} like path-manipulation object.
* {@link File} like object with remoting support.
*
* <p>
* In general, because programs could be executed remotely,
* we need two path strings to identify the same directory.
* One from a point of view of the master (local), the other
* from a point of view of the slave (remote).
* Unlike {@link File}, which always implies a file path on the current computer,
* {@link FilePath} represents a file path on a specific slave or the master.
*
* Despite that, {@link FilePath} can be used much like {@link File}. It exposes
* a bunch of operations (and we should add more operations as long as they are
* generally useful), and when invoked against a file on a remote node, {@link FilePath}
* executes the necessary code remotely, thereby providing semi-transparent file
* operations.
*
* <h2>Using {@link FilePath} smartly</h2>
* <p>
* The transparency makes it easy to write plugins without worrying too much about
* remoting, by making it works like NFS, where remoting happens at the file-system
* later.
*
* <p>
* But one should note that such use of remoting may not be optional. Sometimes,
* it makes more sense to move some computation closer to the data, as opposed to
* move the data to the computation. For example, if you are just computing a MD5
* digest of a file, then it would make sense to do the digest on the host where
* the file is located, as opposed to send the whole data to the master and do MD5
* digesting there.
*
* <p>
* {@link FilePath} supports this "code migration" by in the
* {@link #act(FileCallable)} method. One can pass in a custom implementation
* of {@link FileCallable}, to be executed on the node where the data is located.
* The following code shows the example:
*
* <pre>
* FilePath file = ...;
*
* // make 'file' a fresh empty directory.
* file.act(new FileCallable&lt;Void>() {
* // if 'file' is on a different node, this FileCallable will
* // be transfered to that node and executed there.
* public Void invoke(File f,VirtualChannel channel) {
* // f and file represents the same thing
* f.deleteContents();
* f.mkdirs();
* }
* });
* </pre>
*
* <p>
* This class allows path manipulation to be done
* and allow the local/remote versions to be obtained
* after the computation.
* When {@link FileCallable} is transfered to a remote node, it will be done so
* by using the same Java serializaiton scheme that the remoting module uses.
* See {@link Channel} for more about this.
*
* <p>
* {@link FilePath} itself can be sent over to a remote node as a part of {@link Callable}
* serialization. For example, sending a {@link FilePath} of a remote node to that
* node causes {@link FilePath} to become "local". Similarly, sending a
* {@link FilePath} that represents the local computer causes it to become "remote."
*
* @author Kohsuke Kawaguchi
*/
public final class FilePath {
private final File local;
public final class FilePath implements Serializable {
/**
* When this {@link FilePath} represents the remote path,
* this field is always non-null on master (the field represents
* the channel to the remote slave.) When transferred to a slave via remoting,
* this field reverts back to null, since it's transient.
*
* When this {@link FilePath} represents a path on the master,
* this field is null on master. When transferred to a slave via remoting,
* this field becomes non-null, representing the {@link Channel}
* back to the master.
*
* This is used to determine whether we are running on the master or the slave.
*/
private transient VirtualChannel channel;
// since the platform of the slave might be different, can't use java.io.File
private final String remote;
public FilePath(File local, String remote) {
this.local = local;
public FilePath(VirtualChannel channel, String remote) {
this.channel = channel;
this.remote = remote;
}
/**
* Useful when there's no remote path.
* To create {@link FilePath} on the master computer.
*/
public FilePath(File local) {
this(local,local.getPath());
public FilePath(File localPath) {
this.channel = null;
this.remote = localPath.getPath();
}
public FilePath(FilePath base, String rel) {
this.local = new File(base.local,rel);
this.channel = base.channel;
if(base.isUnix()) {
this.remote = base.remote+'/'+rel;
} else {
......@@ -55,28 +140,116 @@ public final class FilePath {
return remote.indexOf("\\")==-1;
}
public File getLocal() {
return local;
}
public String getRemote() {
return remote;
}
/**
* Code that gets executed on the machine where the {@link FilePath} is local.
* Used to act on {@link FilePath}.
*
* @see FilePath#act(FileCallable)
*/
public static interface FileCallable<T> extends Serializable {
/**
* Performs the computational task on the node where the data is located.
*
* @param f
* {@link File} that represents the local file that {@link FilePath} has represented.
* @param channel
* The "back pointer" of the {@link Channel} that represents the communication
* with the node from where the code was sent.
*/
T invoke(File f, VirtualChannel channel) throws IOException;
}
/**
* Executes some program on the machine that this {@link FilePath} exists,
* so that one can perform local file operations.
*/
public <T> T act(final FileCallable<T> callable) throws IOException, InterruptedException {
if(channel!=null) {
// run this on a remote system
try {
return channel.call(new DelegatingCallable<T,IOException>() {
public T call() throws IOException {
return callable.invoke(new File(remote), Channel.current());
}
public ClassLoader getClassLoader() {
return callable.getClass().getClassLoader();
}
});
} catch (IOException e) {
// wrap it into a new IOException so that we get the caller's stack trace as well.
throw new IOException2("remote file operation failed",e);
}
} else {
// the file is on the local machine.
return callable.invoke(new File(remote), Hudson.MasterComputer.localChannel);
}
}
/**
* Executes some program on the machine that this {@link FilePath} exists,
* so that one can perform local file operations.
*/
public <V,E extends Throwable> V act(Callable<V,E> callable) throws IOException, InterruptedException, E {
if(channel!=null) {
// run this on a remote system
return channel.call(callable);
} else {
// the file is on the local machine
return callable.call();
}
}
/**
* Converts this file to the URI, relative to the machine
* on which this file is available.
*/
public URI toURI() throws IOException, InterruptedException {
return act(new FileCallable<URI>() {
public URI invoke(File f, VirtualChannel channel) {
return f.toURI();
}
});
}
/**
* Creates this directory.
*/
public void mkdirs() throws IOException {
if(!local.mkdirs() && !local.exists())
throw new IOException("Failed to mkdirs: "+local);
public void mkdirs() throws IOException, InterruptedException {
if(act(new FileCallable<Boolean>() {
public Boolean invoke(File f, VirtualChannel channel) throws IOException {
return !f.mkdirs() && !f.exists();
}
}))
throw new IOException("Failed to mkdirs: "+remote);
}
/**
* Deletes this directory, including all its contents recursively.
*/
public void deleteRecursive() throws IOException, InterruptedException {
act(new FileCallable<Void>() {
public Void invoke(File f, VirtualChannel channel) throws IOException {
Util.deleteRecursive(f);
return null;
}
});
}
/**
* Deletes all the contents of this directory, but not the directory itself
*/
public void deleteContents() throws IOException {
// TODO: consider doing this remotely if possible
Util.deleteContentsRecursive(getLocal());
public void deleteContents() throws IOException, InterruptedException {
act(new FileCallable<Void>() {
public Void invoke(File f, VirtualChannel channel) throws IOException {
Util.deleteContentsRecursive(f);
return null;
}
});
}
/**
......@@ -85,7 +258,15 @@ public final class FilePath {
* This method assumes that the file name is the same between local and remote.
*/
public String getName() {
return local.getName();
int len = remote.length()-1;
while(len>=0) {
char ch = remote.charAt(len);
if(ch=='\\' || ch=='/')
break;
len--;
}
return remote.substring(len+1);
}
/**
......@@ -107,47 +288,352 @@ public final class FilePath {
len--;
}
return new FilePath( local.getParentFile(), remote.substring(0,len) );
return new FilePath( channel, remote.substring(0,len) );
}
/**
* Creates a temporary file.
*/
public FilePath createTempFile(String prefix, String suffix) throws IOException {
public FilePath createTempFile(final String prefix, final String suffix) throws IOException, InterruptedException {
try {
return new FilePath(this,act(new FileCallable<String>() {
public String invoke(File dir, VirtualChannel channel) throws IOException {
File f = File.createTempFile(prefix, suffix, dir);
return f.getName();
}
}));
} catch (IOException e) {
throw new IOException2("Failed to create a temp file on "+remote,e);
}
}
/**
* Creates a temporary file in this directory and set the contents by the
* given text (encoded in the platform default encoding)
*/
public FilePath createTextTempFile(final String prefix, final String suffix, final String contents) throws IOException, InterruptedException {
try {
File f = File.createTempFile(prefix, suffix, getLocal());
return new FilePath(this,f.getName());
return new FilePath(this,act(new FileCallable<String>() {
public String invoke(File dir, VirtualChannel channel) throws IOException {
File f = File.createTempFile(prefix, suffix, dir);
Writer w = new FileWriter(f);
w.write(contents);
w.close();
return f.getName();
}
}));
} catch (IOException e) {
throw new IOException2("Failed to create a temp file on "+getLocal(),e);
throw new IOException2("Failed to create a temp file on "+remote,e);
}
}
/**
* Deletes this file.
*/
public boolean delete() {
return local.delete();
public boolean delete() throws IOException, InterruptedException {
return act(new FileCallable<Boolean>() {
public Boolean invoke(File f, VirtualChannel channel) throws IOException {
return f.delete();
}
});
}
/**
* Checks if the file exists.
*/
public boolean exists() throws IOException, InterruptedException {
return act(new FileCallable<Boolean>() {
public Boolean invoke(File f, VirtualChannel channel) throws IOException {
return f.exists();
}
});
}
public boolean exists() {
return local.exists();
/**
* Gets the last modified time stamp of this file, by using the clock
* of the machine where this file actually resides.
*
* @see File#lastModified()
*/
public long lastModified() throws IOException, InterruptedException {
return act(new FileCallable<Long>() {
public Long invoke(File f, VirtualChannel channel) throws IOException {
return f.lastModified();
}
});
}
public boolean isDirectory() {
return local.isDirectory();
/**
* Checks if the file is a directory.
*/
public boolean isDirectory() throws IOException, InterruptedException {
return act(new FileCallable<Boolean>() {
public Boolean invoke(File f, VirtualChannel channel) throws IOException {
return f.isDirectory();
}
});
}
/**
* Always use {@link #getLocal()} or {@link #getRemote()}
* List up files in this directory.
*
* @param filter
* The optional filter used to narrow down the result.
* If non-null, must be {@link Serializable}.
* If this {@link FilePath} represents a remote path,
* the filter object will be executed on the remote machine.
*/
public List<FilePath> list(final FileFilter filter) throws IOException, InterruptedException {
return act(new FileCallable<List<FilePath>>() {
public List<FilePath> invoke(File f, VirtualChannel channel) throws IOException {
File[] children = f.listFiles(filter);
if(children ==null) return null;
ArrayList<FilePath> r = new ArrayList<FilePath>(children.length);
for (File child : children)
r.add(new FilePath(child));
return r;
}
});
}
/**
* Reads this file.
*/
public InputStream read() throws IOException {
if(channel==null)
return new FileInputStream(new File(remote));
final Pipe p = Pipe.createRemoteToLocal();
channel.callAsync(new Callable<Void,IOException>() {
public Void call() throws IOException {
FileInputStream fis = new FileInputStream(new File(remote));
Util.copyStream(fis,p.getOut());
fis.close();
p.getOut().close();
return null;
}
});
return p.getIn();
}
/**
* Writes to this file.
* If this file already exists, it will be overwritten.
*/
public OutputStream write() throws IOException {
if(channel==null)
return new FileOutputStream(new File(remote));
final Pipe p = Pipe.createLocalToRemote();
channel.callAsync(new Callable<Void,IOException>() {
public Void call() throws IOException {
FileOutputStream fos = new FileOutputStream(new File(remote));
Util.copyStream(p.getIn(),fos);
fos.close();
p.getIn().close();
return null;
}
});
return p.getOut();
}
/**
* Computes the MD5 digest of the file in hex string.
*/
public String digest() throws IOException, InterruptedException {
return act(new FileCallable<String>() {
public String invoke(File f, VirtualChannel channel) throws IOException {
return Util.getDigestOf(new FileInputStream(f));
}
});
}
/**
* Copies this file to the specified target.
*/
public void copyTo(FilePath target) throws IOException, InterruptedException {
OutputStream out = target.write();
try {
copyTo(out);
} finally {
out.close();
}
}
/**
* Sends the contents of this file into the given {@link OutputStream}.
*/
public void copyTo(OutputStream os) throws IOException, InterruptedException {
final OutputStream out = new RemoteOutputStream(os);
act(new FileCallable<Void>() {
public Void invoke(File f, VirtualChannel channel) throws IOException {
FileInputStream fis = new FileInputStream(f);
Util.copyStream(fis,out);
fis.close();
out.close();
return null;
}
});
}
/**
* Remoting interface used for {@link FilePath#copyRecursiveTo(String, FilePath)}.
*
* TODO: this might not be the most efficient way to do the copy.
*/
interface RemoteCopier {
void open(String fileName) throws IOException;
void write(byte[] buf, int len) throws IOException;
void close() throws IOException;
}
/**
* Copies the files that match the given file mask to the specified target node.
*
* @return
* the number of files copied.
*/
public int copyRecursiveTo(final String fileMask, final FilePath target) throws IOException, InterruptedException {
if(this.channel==target.channel) {
// local to local copy.
return act(new FileCallable<Integer>() {
public Integer invoke(File base, VirtualChannel channel) throws IOException {
assert target.channel==null;
try {
class CopyImpl extends Copy {
private int copySize;
public CopyImpl() {
setProject(new org.apache.tools.ant.Project());
}
protected void doFileOperations() {
copySize = super.fileCopyMap.size();
super.doFileOperations();
}
public int getNumCopied() {
return copySize;
}
}
CopyImpl copyTask = new CopyImpl();
copyTask.setTodir(new File(target.remote));
FileSet src = new FileSet();
src.setDir(base);
src.setIncludes(fileMask);
copyTask.addFileset(src);
copyTask.execute();
return copyTask.getNumCopied();
} catch (BuildException e) {
throw new IOException2("Failed to copy "+base+"/"+fileMask+" to "+target,e);
}
}
});
} else {
// remote copy
final FilePath src = this;
return target.act(new FileCallable<Integer>() {
// this code is executed on the node that receives files.
public Integer invoke(final File dest, VirtualChannel channel) throws IOException {
final RemoteCopier copier = src.getChannel().export(
RemoteCopier.class,
new RemoteCopier() {
private OutputStream os;
public void open(String fileName) throws IOException {
File file = new File(dest, fileName);
file.getParentFile().mkdirs();
os = new FileOutputStream(file);
}
public void write(byte[] buf, int len) throws IOException {
os.write(buf,0,len);
}
public void close() throws IOException {
os.close();
os = null;
}
});
try {
return src.act(new FileCallable<Integer>() {
public Integer invoke(File base, VirtualChannel channel) throws IOException {
// copy to a remote node
FileSet fs = new FileSet();
fs.setDir(base);
fs.setIncludes(fileMask);
byte[] buf = new byte[8192];
DirectoryScanner ds = fs.getDirectoryScanner(new org.apache.tools.ant.Project());
String[] files = ds.getIncludedFiles();
for( String f : files) {
File file = new File(base, f);
copier.open(f);
FileInputStream in = new FileInputStream(file);
int len;
while((len=in.read(buf))>=0)
copier.write(buf,len);
in.close();
copier.close();
}
return files.length;
}
});
} catch (InterruptedException e) {
throw new IOException2("Copy operation interrupted",e);
}
}
});
}
}
@Deprecated
public String toString() {
// to make writing JSPs easily, return local
return local.toString();
return remote;
}
/**
* {@link FilePath} constant that can be used if the directory is not important.
*/
public static final FilePath RANDOM = new FilePath(new File("."));
public VirtualChannel getChannel() {
if(channel!=null) return channel;
else return Hudson.MasterComputer.localChannel;
}
private void writeObject(ObjectOutputStream oos) throws IOException {
Channel target = Channel.current();
if(channel!=null && channel!=target)
throw new IllegalStateException("Can't send a remote FilePath to a different remote channel");
oos.defaultWriteObject();
oos.writeBoolean(channel==null);
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
Channel channel = Channel.current();
assert channel!=null;
ois.defaultReadObject();
if(ois.readBoolean()) {
this.channel = channel;
} else {
this.channel = null;
}
}
private static final long serialVersionUID = 1L;
}
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--) {
......
package hudson.scm;
import hudson.FilePath;
import hudson.FilePath.FileCallable;
import hudson.Launcher;
import hudson.Proc;
import hudson.Util;
......@@ -12,45 +13,52 @@ import hudson.model.Descriptor;
import hudson.model.Hudson;
import hudson.model.ModelObject;
import hudson.model.Project;
import hudson.model.Result;
import hudson.model.StreamBuildListener;
import hudson.model.TaskListener;
import hudson.model.Result;
import hudson.org.apache.tools.ant.taskdefs.cvslib.ChangeLogTask;
import hudson.remoting.RemoteOutputStream;
import hudson.remoting.VirtualChannel;
import hudson.util.ArgumentListBuilder;
import hudson.util.ForkOutputStream;
import hudson.util.FormFieldValidator;
import java.util.Collections;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.Expand;
import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipOutputStream;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringWriter;
import java.io.PrintWriter;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.Set;
import java.util.TreeSet;
import java.util.HashSet;
import java.util.HashMap;
import java.util.Locale;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
......@@ -61,9 +69,13 @@ import java.util.regex.Pattern;
* I couldn't call this class "CVS" because that would cause the view folder name
* to collide with CVS control files.
*
* <p>
* This object gets shipped to the remote machine to perform some of the work,
* so it implements {@link Serializable}.
*
* @author Kohsuke Kawaguchi
*/
public class CVSSCM extends AbstractCVSFamilySCM {
public class CVSSCM extends AbstractCVSFamilySCM implements Serializable {
/**
* CVSSCM connection string.
*/
......@@ -143,7 +155,7 @@ public class CVSSCM extends AbstractCVSFamilySCM {
return flatten;
}
public boolean pollChanges(Project project, Launcher launcher, FilePath dir, TaskListener listener) throws IOException {
public boolean pollChanges(Project project, Launcher launcher, FilePath dir, TaskListener listener) throws IOException, InterruptedException {
List<String> changedFiles = update(true, launcher, dir, listener, new Date());
return changedFiles!=null && !changedFiles.isEmpty();
......@@ -155,10 +167,10 @@ public class CVSSCM extends AbstractCVSFamilySCM {
cmd.add("-D", df.format(date));
}
public boolean checkout(Build build, Launcher launcher, FilePath dir, BuildListener listener, File changelogFile) throws IOException {
public boolean checkout(Build build, Launcher launcher, FilePath dir, BuildListener listener, File changelogFile) throws IOException, InterruptedException {
List<String> changedFiles = null; // files that were affected by update. null this is a check out
if(canUseUpdate && isUpdatable(dir.getLocal())) {
if(canUseUpdate && isUpdatable(dir)) {
changedFiles = update(false, launcher, dir, listener, build.getTimestamp().getTime());
if(changedFiles==null)
return false; // failed
......@@ -166,6 +178,7 @@ public class CVSSCM extends AbstractCVSFamilySCM {
dir.deleteContents();
ArgumentListBuilder cmd = new ArgumentListBuilder();
// TODO: debug option to make it verbose
cmd.add("cvs","-Q","-z9","-d",cvsroot,"co");
if(branch!=null)
cmd.add("-r",branch);
......@@ -179,27 +192,34 @@ public class CVSSCM extends AbstractCVSFamilySCM {
}
// archive the workspace to support later tagging
// TODO: doing this partially remotely would be faster
File archiveFile = getArchiveFile(build);
ZipOutputStream zos = new ZipOutputStream(archiveFile);
if(flatten) {
archive(build.getProject().getWorkspace().getLocal(), module, zos);
} else {
StringTokenizer tokens = new StringTokenizer(module);
while(tokens.hasMoreTokens()) {
String m = tokens.nextToken();
File mf = new File(build.getProject().getWorkspace().getLocal(), m);
if(!mf.isDirectory()) {
// this module is just a file, say "foo/bar.txt".
// to record "foo/CVS/*", we need to start by archiving "foo".
m = m.substring(0,m.lastIndexOf('/'));
mf = mf.getParentFile();
final OutputStream os = new RemoteOutputStream(new FileOutputStream(archiveFile));
build.getProject().getWorkspace().act(new FileCallable<Void>() {
public Void invoke(File ws, VirtualChannel channel) throws IOException {
ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(os));
if(flatten) {
archive(ws, module, zos);
} else {
StringTokenizer tokens = new StringTokenizer(module);
while(tokens.hasMoreTokens()) {
String m = tokens.nextToken();
File mf = new File(ws, m);
if(!mf.isDirectory()) {
// this module is just a file, say "foo/bar.txt".
// to record "foo/CVS/*", we need to start by archiving "foo".
m = m.substring(0,m.lastIndexOf('/'));
mf = mf.getParentFile();
}
archive(mf,m,zos);
}
}
archive(mf,m,zos);
zos.close();
return null;
}
}
zos.close();
});
// contribute the tag action
build.getActions().add(new TagAction(build));
......@@ -279,7 +299,7 @@ public class CVSSCM extends AbstractCVSFamilySCM {
* List of affected file names, relative to the workspace directory.
* Null if the operation failed.
*/
private List<String> update(boolean dryRun, Launcher launcher, FilePath workspace, TaskListener listener, Date date) throws IOException {
private List<String> update(boolean dryRun, Launcher launcher, FilePath workspace, TaskListener listener, Date date) throws IOException, InterruptedException {
List<String> changedFileNames = new ArrayList<String>(); // file names relative to the workspace
......@@ -303,25 +323,32 @@ public class CVSSCM extends AbstractCVSFamilySCM {
parseUpdateOutput("",baos, changedFileNames);
} else {
@SuppressWarnings("unchecked") // StringTokenizer oddly has the wrong type
Set<String> moduleNames = new TreeSet(Collections.list(new StringTokenizer(module)));
final Set<String> moduleNames = new TreeSet(Collections.list(new StringTokenizer(module)));
// Add in any existing CVS dirs, in case project checked out its own.
File[] subdirs = workspace.getLocal().listFiles();
if (subdirs != null) {
SUBDIR: for (File s : subdirs) {
if (new File(s, "CVS").isDirectory()) {
String top = s.getName();
for (String mod : moduleNames) {
if (mod.startsWith(top + "/")) {
// #190: user asked to check out foo/bar foo/baz quux
// Our top-level dirs are "foo" and "quux".
// Do not add "foo" to checkout or we will check out foo/*!
continue SUBDIR;
moduleNames.addAll(workspace.act(new FileCallable<Set<String>>() {
public Set<String> invoke(File ws, VirtualChannel channel) throws IOException {
File[] subdirs = ws.listFiles();
if (subdirs != null) {
SUBDIR: for (File s : subdirs) {
if (new File(s, "CVS").isDirectory()) {
String top = s.getName();
for (String mod : moduleNames) {
if (mod.startsWith(top + "/")) {
// #190: user asked to check out foo/bar foo/baz quux
// Our top-level dirs are "foo" and "quux".
// Do not add "foo" to checkout or we will check out foo/*!
continue SUBDIR;
}
}
moduleNames.add(top);
}
}
moduleNames.add(top);
}
return moduleNames;
}
}
}));
for (String moduleName : moduleNames) {
// capture the output during update
ByteArrayOutputStream baos = new ByteArrayOutputStream();
......@@ -398,18 +425,22 @@ public class CVSSCM extends AbstractCVSFamilySCM {
/**
* Returns true if we can use "cvs update" instead of "cvs checkout"
*/
private boolean isUpdatable(File dir) {
if(flatten) {
return isUpdatableModule(dir);
} else {
StringTokenizer tokens = new StringTokenizer(module);
while(tokens.hasMoreTokens()) {
File module = new File(dir,tokens.nextToken());
if(!isUpdatableModule(module))
return false;
private boolean isUpdatable(FilePath dir) throws IOException, InterruptedException {
return dir.act(new FileCallable<Boolean>() {
public Boolean invoke(File dir, VirtualChannel channel) throws IOException {
if(flatten) {
return isUpdatableModule(dir);
} else {
StringTokenizer tokens = new StringTokenizer(module);
while(tokens.hasMoreTokens()) {
File module = new File(dir,tokens.nextToken());
if(!isUpdatableModule(module))
return false;
}
return true;
}
}
return true;
}
});
}
private boolean isUpdatableModule(File module) {
......@@ -434,10 +465,7 @@ public class CVSSCM extends AbstractCVSFamilySCM {
Reader r = new FileReader(tag);
try {
String s = new BufferedReader(r).readLine();
if (s == null) {
return false;
}
return s.startsWith("D");
return s != null && s.startsWith("D");
} finally {
r.close();
}
......@@ -470,6 +498,36 @@ public class CVSSCM extends AbstractCVSFamilySCM {
}
}
/**
* Used to communicate the result of the detection in {@link CVSSCM#calcChangeLog(Build, List, File, BuildListener)}
*/
class ChangeLogResult implements Serializable {
boolean hadError;
String errorOutput;
public ChangeLogResult(boolean hadError, String errorOutput) {
this.hadError = hadError;
if(hadError)
this.errorOutput = errorOutput;
}
private static final long serialVersionUID = 1L;
}
/**
* Used to propagate {@link BuildException} and error log at the same time.
*/
class BuildExceptionWithLog extends RuntimeException {
final String errorOutput;
public BuildExceptionWithLog(BuildException cause, String errorOutput) {
super(cause);
this.errorOutput = errorOutput;
}
private static final long serialVersionUID = 1L;
}
/**
* Computes the changelog into an XML file.
*
......@@ -483,7 +541,7 @@ public class CVSSCM extends AbstractCVSFamilySCM {
* This is provided if the previous operation is update, otherwise null,
* which means we have to fall back to the default slow computation.
*/
private boolean calcChangeLog(Build build, List<String> changedFiles, File changelogFile, final BuildListener listener) {
private boolean calcChangeLog(Build build, final List<String> changedFiles, File changelogFile, final BuildListener listener) throws InterruptedException {
if(build.getPreviousBuild()==null || (changedFiles!=null && changedFiles.isEmpty())) {
// nothing to compare against, or no changes
// (note that changedFiles==null means fallback, so we have to run cvs log.
......@@ -493,87 +551,109 @@ public class CVSSCM extends AbstractCVSFamilySCM {
listener.getLogger().println("$ computing changelog");
final StringWriter errorOutput = new StringWriter();
final boolean[] hadError = new boolean[1];
ChangeLogTask task = new ChangeLogTask() {
public void log(String msg, int msgLevel) {
// send error to listener. This seems like the route in which the changelog task
// sends output
if(msgLevel==org.apache.tools.ant.Project.MSG_ERR) {
hadError[0] = true;
errorOutput.write(msg);
errorOutput.write('\n');
return;
}
if(debugLogging) {
listener.getLogger().println(msg);
}
}
};
task.setProject(new org.apache.tools.ant.Project());
File baseDir = build.getProject().getWorkspace().getLocal();
task.setDir(baseDir);
if(DESCRIPTOR.getCvspassFile().length()!=0)
task.setPassfile(new File(DESCRIPTOR.getCvspassFile()));
task.setCvsRoot(cvsroot);
task.setCvsRsh(cvsRsh);
task.setFailOnError(true);
task.setDestfile(changelogFile);
task.setBranch(branch);
task.setStart(build.getPreviousBuild().getTimestamp().getTime());
task.setEnd(build.getTimestamp().getTime());
if(changedFiles!=null) {
// if the directory doesn't exist, cvs changelog will die, so filter them out.
// this means we'll lose the log of those changes
for (String filePath : changedFiles) {
if(new File(baseDir,filePath).getParentFile().exists())
task.addFile(filePath);
}
} else {
// fallback
if(!flatten)
task.setPackage(module);
}
FilePath baseDir = build.getProject().getWorkspace();
final String cvspassFile = getDescriptor().getCvspassFile();
try {
task.execute();
if(hadError[0]) {
// range of time for detecting changes
final Date startTime = build.getPreviousBuild().getTimestamp().getTime();
final Date endTime = build.getTimestamp().getTime();
final OutputStream out = new RemoteOutputStream(new FileOutputStream(changelogFile));
ChangeLogResult result = baseDir.act(new FileCallable<ChangeLogResult>() {
public ChangeLogResult invoke(File ws, VirtualChannel channel) throws IOException {
final StringWriter errorOutput = new StringWriter();
final boolean[] hadError = new boolean[1];
ChangeLogTask task = new ChangeLogTask() {
public void log(String msg, int msgLevel) {
// send error to listener. This seems like the route in which the changelog task
// sends output
if(msgLevel==org.apache.tools.ant.Project.MSG_ERR) {
hadError[0] = true;
errorOutput.write(msg);
errorOutput.write('\n');
return;
}
if(debugLogging) {
listener.getLogger().println(msg);
}
}
};
task.setProject(new org.apache.tools.ant.Project());
task.setDir(ws);
if(cvspassFile.length()!=0)
task.setPassfile(new File(cvspassFile));
task.setCvsRoot(cvsroot);
task.setCvsRsh(cvsRsh);
task.setFailOnError(true);
task.setDeststream(new BufferedOutputStream(out));
task.setBranch(branch);
task.setStart(startTime);
task.setEnd(endTime);
if(changedFiles!=null) {
// if the directory doesn't exist, cvs changelog will die, so filter them out.
// this means we'll lose the log of those changes
for (String filePath : changedFiles) {
if(new File(ws,filePath).getParentFile().exists())
task.addFile(filePath);
}
} else {
// fallback
if(!flatten)
task.setPackage(module);
}
try {
task.execute();
} catch (BuildException e) {
throw new BuildExceptionWithLog(e,errorOutput.toString());
}
return new ChangeLogResult(hadError[0],errorOutput.toString());
}
});
if(result.hadError) {
// non-fatal error must have occurred, such as cvs changelog parsing error.s
listener.getLogger().print(errorOutput);
listener.getLogger().print(result.errorOutput);
}
return true;
} catch( BuildException e ) {
} catch( BuildExceptionWithLog e ) {
// capture output from the task for diagnosis
listener.getLogger().print(errorOutput);
listener.getLogger().print(e.errorOutput);
// then report an error
PrintWriter w = listener.error(e.getMessage());
BuildException x = (BuildException) e.getCause();
PrintWriter w = listener.error(x.getMessage());
w.println("Working directory is "+baseDir);
e.printStackTrace(w);
x.printStackTrace(w);
return false;
} catch( RuntimeException e ) {
// an user reported a NPE inside the changeLog task.
// we don't want a bug in Ant to prevent a build.
e.printStackTrace(listener.error(e.getMessage()));
return true; // so record the message but continue
} catch( IOException e ) {
e.printStackTrace(listener.error("Failed to detect changlog"));
return true;
}
}
public DescriptorImpl getDescriptor() {
return DESCRIPTOR;
return DescriptorImpl.DESCRIPTOR;
}
public void buildEnvVars(Map<String,String> env) {
if(cvsRsh!=null)
env.put("CVS_RSH",cvsRsh);
String cvspass = DESCRIPTOR.getCvspassFile();
String cvspass = getDescriptor().getCvspassFile();
if(cvspass.length()!=0)
env.put("CVS_PASSFILE",cvspass);
}
static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
public static final class DescriptorImpl extends Descriptor<SCM> implements ModelObject {
static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
/**
* Path to <tt>.cvspass</tt>. Null to default.
*/
......@@ -690,7 +770,7 @@ public class CVSSCM extends AbstractCVSFamilySCM {
public void doVersion(StaplerRequest req, StaplerResponse rsp) throws IOException {
rsp.setContentType("text/plain");
Proc proc = Hudson.getInstance().createLauncher(TaskListener.NULL).launch(
new String[]{"cvs", "--version"}, new String[0], rsp.getOutputStream(), FilePath.RANDOM);
new String[]{"cvs", "--version"}, new String[0], rsp.getOutputStream(), null);
proc.join();
}
......@@ -895,7 +975,7 @@ public class CVSSCM extends AbstractCVSFamilySCM {
public final class TagWorkerThread extends Thread {
private final String tagName;
// StringWriter is synchronized
private final StringWriter log = new StringWriter();
private final ByteArrayOutputStream log = new ByteArrayOutputStream();
public TagWorkerThread(String tagName) {
this.tagName = tagName;
......@@ -947,7 +1027,7 @@ public class CVSSCM extends AbstractCVSFamilySCM {
path = path.getParent();
}
if(!CVSSCM.this.run(new Launcher(listener),cmd,listener, path)) {
if(!CVSSCM.this.run(new Launcher.LocalLauncher(listener),cmd,listener, path)) {
listener.getLogger().println("tagging failed");
return;
}
......@@ -984,4 +1064,6 @@ public class CVSSCM extends AbstractCVSFamilySCM {
* Setting this property to true would cause <tt>cvs log</tt> to dump a lot of messages.
*/
public static boolean debugLogging = false;
private static final long serialVersionUID = 1L;
}
......@@ -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;
......
package hudson.util;
import hudson.Util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* {@link Thread} that copies {@link InputStream} to {@link OutputStream}.
*
* @author Kohsuke Kawaguchi
*/
public class StreamCopyThread extends Thread {
private final InputStream in;
private final OutputStream out;
public StreamCopyThread(String threadName, InputStream in, OutputStream out) {
super(threadName);
this.in = in;
this.out = out;
}
public void run() {
try {
Util.copyStream(in,out);
in.close();
} catch (IOException e) {
// TODO: what to do?
}
}
}
package hudson.util;
import hudson.model.TaskListener;
import hudson.remoting.RemoteOutputStream;
import hudson.CloseProofOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.io.Serializable;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
/**
* {@link TaskListener} that generates output into a single stream.
*
* <p>
* This object is remotable.
*
* @author Kohsuke Kawaguchi
*/
public final class StreamTaskListener implements TaskListener {
private final PrintStream out;
public final class StreamTaskListener implements TaskListener, Serializable {
private PrintStream out;
public StreamTaskListener(PrintStream out) {
this.out = out;
......@@ -40,4 +49,14 @@ public final class StreamTaskListener implements TaskListener {
public PrintWriter fatalError(String msg) {
return error(msg);
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeObject(new RemoteOutputStream(new CloseProofOutputStream(this.out)));
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
out = (PrintStream) in.readObject();
}
private static final long serialVersionUID = 1L;
}
<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
......@@ -18,7 +18,19 @@
<img src="${rootURL}/images/48x48/${it.icon}" width="48" height="48" />
Slave ${it.displayName}
</h1>
<j:if test="${it.offline &amp;&amp; !it.temporarilyOffline}">
<p class="error">
This node is offline because Hudson failed to launch the slave agent on it.
<a href="log">See log for more details</a>
</p>
<l:isAdmin>
<form method="get" action="launchSlaveAgent">
<input type="submit" value="Launch slave agent" />
</form>
</l:isAdmin>
</j:if>
<h2>Projects tied on ${it.displayName}</h2>
<j:set var="jobs" value="${it.tiedJobs}" />
<j:choose>
......
......@@ -35,38 +35,28 @@
<s:repeatable var="s" items="${it.slaves}">
<table width="100%">
<s:entry title="name" help="/help/system-config/master-slave/name.html">
<input class="setting-input" name="slave_name"
type="text" value="${s.nodeName}" />
<s:textbox name="slave.name" value="${s.nodeName}" />
</s:entry>
<s:entry title="launch command" help="/help/system-config/master-slave/command.html">
<s:textbox name="slave.command" value="${s.command}"/>
</s:entry>
<s:entry title="description" help="/help/system-config/master-slave/description.html">
<input class="setting-input" name="slave_description"
type="text" value="${s.nodeDescription}" />
<s:textbox name="slave.description" value="${s.nodeDescription}" />
</s:entry>
<s:entry title="# of executors" help="/help/system-config/master-slave/numExecutors.html">
<input class="setting-input number" name="slave_executors"
<input class="setting-input number" name="slave.numExecutors"
type="text" value="${s.numExecutors}" />
</s:entry>
<s:entry title="connect command" help="/help/system-config/master-slave/command.html">
<input class="setting-input" name="slave_command"
type="text" value="${s.command}" />
</s:entry>
<s:entry title="local FS root" help="/help/system-config/master-slave/localFS.html">
<input class="setting-input validated" name="slave_localFS"
checkUrl="'checkLocalFSRoot?value='+this.value"
type="text" value="${s.localFS}" />
</s:entry>
<s:entry title="remote FS root" help="/help/system-config/master-slave/remoteFS.html">
<input class="setting-input" name="slave_remoteFS"
type="text" value="${s.remoteFS}" />
<s:textbox name="slave.remoteFS" value="${s.remoteFS}" />
</s:entry>
<s:entry title="usage" help="/help/system-config/master-slave/usage.html">
<select class="setting-input" name="slave_mode">
<select class="setting-input" name="slave.mode">
<j:forEach var="m" items="${h.getNodeModes()}">
<s:option value="${m.name}" selected="${m==s.mode}">${m.description}</s:option>
</j:forEach>
......
......@@ -2,33 +2,14 @@
Various system information for diagnostics
-->
<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">
<l:layout xmlns:local="local">
<l:layout>
<st:include page="sidepanel.jelly" />
<d:taglib uri="local">
<!-- table to show a map -->
<d:tag name="table">
<table class="pane sortable">
<tr>
<th class="pane-header" initialSortDir="down">Name</th>
<th class="pane-header">Value</th>
</tr>
<j:forEach var="e" items="${items}">
<tr>
<td class="pane"><st:out value="${e.key}"/></td>
<td class="pane"><st:out value="${e.value}"/></td>
</tr>
</j:forEach>
</table>
</d:tag>
</d:taglib>
<l:main-panel>
<l:isAdmin>
<h1>System Properties</h1>
<local:table items="${h.systemProperties}" />
<t:propertyTable items="${h.systemProperties}" />
<h1>Environment Variables</h1>
<local:table items="${h.envVars}" />
<t:propertyTable items="${h.envVars}" />
</l:isAdmin>
</l:main-panel>
</l:layout>
......
<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="connect command" help="/help/system-config/master-slave/command.html">
<f:textbox name="slave.command" value="${slave.command}"/>
</f:entry>
<f:entry title="local FS root" help="/help/system-config/master-slave/localFS.html">
<f:textbox name="slave.localFS"
checkUrl="'checkLocalFSRoot?value='+this.value"
value="${slave.localFS}" />
</f:entry>
</j:jelly>
\ No newline at end of file
......@@ -59,14 +59,7 @@
<j:forEach var="idx" begin="0" end="${size(scms)-1}">
<j:set var="scmd" value="${scms[idx]}" />
<f:radioBlock name="scm" value="${idx}" title="${scmd.displayName}" checked="${it.scm.descriptor==scmd}">
<j:choose>
<j:when test="${it.scm.descriptor==scmd}">
<j:set var="scm" value="${it.scm}"/>
</j:when>
<j:otherwise>
<j:set var="scm" value="${null}"/>
</j:otherwise>
</j:choose>
<j:set var="scm" value="${h.ifThenElse(it.scm.descriptor==scmd, it.scm, null)}"/>
<st:include from="${scmd}" page="${scmd.configPage}"/>
</f:radioBlock>
</j:forEach>
......
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:layout title="${it.displayName} log">
<st:include page="sidepanel.jelly" />
<l:main-panel>
<pre id="out"></pre>
<div id="spinner">
<img src="${rootURL}/images/spinner.gif" />
</div>
<t:progressiveText href="progressiveLog" idref="out" spinner="spinner" />
</l:main-panel>
</l:layout>
</j:jelly>
<!--
Side panel for a slave.
-->
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:header title="${it.displayName}" />
<l:side-panel>
<l:tasks>
<l:task icon="images/24x24/up.gif" href="${rootURL}/" title="Back to Dashboard" />
<l:task icon="images/24x24/search.gif" href="${rootURL}/computer/${it.displayName}/" title="Status" />
<l:task icon="images/24x24/clipboard.gif" href="log" title="Log" />
<l:task icon="images/24x24/computer.gif" href="systemInfo" title="System Information" />
</l:tasks>
</l:side-panel>
</j:jelly>
\ No newline at end of file
<!--
Various system information for diagnostics.
TODO: merge this with Hudson/systemInfo.jelly
-->
<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">
<l:layout title="${it.displayName} System Information">
<st:include page="sidepanel.jelly" />
<l:main-panel>
<l:isAdmin>
<h1>System Properties</h1>
<t:propertyTable items="${it.systemProperties}" />
<h1>Environment Variables</h1>
<t:propertyTable items="${it.envVars}" />
</l:isAdmin>
</l:main-panel>
</l:layout>
</j:jelly>
......@@ -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>
......
<!--
Dispaly sortable table of properties.
items="<a map object>"
-->
<j:jelly xmlns:j="jelly:core" xmlns:x="jelly:xml" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<table class="pane sortable">
<tr>
<th class="pane-header" initialSortDir="down">Name</th>
<th class="pane-header">Value</th>
</tr>
<j:forEach var="e" items="${items}">
<tr>
<td class="pane"><st:out value="${e.key}"/></td>
<td class="pane"><st:out value="${e.value}"/></td>
</tr>
</j:forEach>
</table>
</j:jelly>
\ No newline at end of file
......@@ -117,10 +117,14 @@
<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" />
<jar basedir="target/classes14" destfile="target/${artifactId}-${version}-jdk14.jar">
<manifest>
<attribute name="Main-Class" value="hudson.remoting.Launcher"/>
</manifest>
</jar>
</tasks>
</configuration>
<goals>
......@@ -154,6 +158,16 @@
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>hudson.remoting.Launcher</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
......
package hudson.remoting;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.io.EOFException;
import java.lang.reflect.Proxy;
import java.util.Hashtable;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.logging.Level;
......@@ -89,6 +89,11 @@ public class Channel implements VirtualChannel {
*/
private final ExportTable<Object> exportedObjects = new ExportTable<Object>();
/**
* Registered listeners.
*/
private final Vector<Listener> listeners = new Vector<Listener>();
public Channel(String name, Executor exec, InputStream is, OutputStream os) throws IOException {
this(name,exec,is,os,null);
}
......@@ -147,6 +152,20 @@ public class Channel implements VirtualChannel {
new ReaderThread(name).start();
}
/**
* Callback "interface" for changes in the state of {@link Channel}.
*/
public static abstract class Listener {
/**
* When the channel was closed normally or abnormally due to an error.
*
* @param cause
* if the channel is closed abnormally, this parameter
* represents an exception that has triggered it.
*/
public void onClosed(Channel channel, IOException cause) {}
}
/**
* Sends a command to the remote end and executes it there.
*
......@@ -170,20 +189,13 @@ public class Channel implements VirtualChannel {
}
/**
* Exports an object for remoting to the other {@link Channel}.
*
* @param type
* Interface to be remoted.
* @return
* the proxy object that implements <tt>T</tt>. This object can be transfered
* to the other {@link Channel}, and calling methods on it will invoke the
* same method on the given <tt>instance</tt> object.
* {@inheritDoc}
*/
/*package*/ synchronized <T> T export(Class<T> type, T instance) {
public <T> T export(Class<T> type, T instance) {
if(instance==null)
return null;
// TODO: unexport
// proxy will unexport this instance when it's GC-ed on the remote machine.
final int id = export(instance);
return type.cast(Proxy.newProxyInstance( type.getClassLoader(), new Class[]{type},
new RemoteInvocationHandler(id)));
......@@ -204,14 +216,19 @@ public class Channel implements VirtualChannel {
/**
* {@inheritDoc}
*/
public <V extends Serializable,T extends Throwable>
public <V,T extends Throwable>
V call(Callable<V,T> callable) throws IOException, T, InterruptedException {
UserResponse<V> r = new UserRequest<V,T>(this, callable).call(this);
try {
UserResponse<V> r = new UserRequest<V,T>(this, callable).call(this);
return r.retrieve(this, callable.getClass().getClassLoader());
// re-wrap the exception so that we can capture the stack trace of the caller.
} catch (ClassNotFoundException e) {
// this is unlikely to happen, so this is a lame implementation
IOException x = new IOException();
IOException x = new IOException("Remote call failed");
x.initCause(e);
throw x;
} catch (Error e) {
IOException x = new IOException("Remote call failed");
x.initCause(e);
throw x;
}
......@@ -220,9 +237,9 @@ public class Channel implements VirtualChannel {
/**
* {@inheritDoc}
*/
public <V extends Serializable,T extends Throwable>
public <V,T extends Throwable>
Future<V> callAsync(final Callable<V,T> callable) throws IOException {
final Future<UserResponse<V>> f = new UserRequest<V, T>(this, callable).callAsync(this);
final Future<UserResponse<V>> f = new UserRequest<V,T>(this, callable).callAsync(this);
return new FutureAdapter<V,UserResponse<V>>(f) {
protected V adapt(UserResponse<V> r) throws ExecutionException {
try {
......@@ -236,8 +253,10 @@ public class Channel implements VirtualChannel {
};
}
/**
* Aborts the connection in response to an error.
*/
private synchronized void terminate(IOException e) {
// abort
closed = true;
synchronized(pendingCalls) {
for (Request<?,?> req : pendingCalls.values())
......@@ -245,6 +264,28 @@ public class Channel implements VirtualChannel {
pendingCalls.clear();
}
notify();
for (Listener l : listeners.toArray(new Listener[listeners.size()]))
l.onClosed(this,e);
}
/**
* Registers a new {@link Listener}.
*
* @see #removeListener(Listener)
*/
public void addListener(Listener l) {
listeners.add(l);
}
/**
* Removes a listener.
*
* @return
* false if the given listener has not been registered to begin with.
*/
public boolean removeListener(Listener l) {
return listeners.remove(l);
}
/**
......@@ -334,7 +375,8 @@ public class Channel implements VirtualChannel {
/**
* This method can be invoked during the serialization/deserialization of
* objects, when they are transferred to the remote {@link Channel}.
* objects when they are transferred to the remote {@link Channel},
* as well as during {@link Callable#call()} is invoked.
*
* @return null
* if the calling thread is not performing serialization.
......
package hudson.remoting;
/**
* {@link Callable} that nominates another claassloader for serialization.
*
* <p>
* For various reasons, one {@link Callable} object is serialized by one classloader.
* Normally the classloader that loaded {@link Callable} implementation will be used,
* but when {@link Callable} further delegates to another classloader, that might
* not be suitable. Implementing this interface allows {@link Callable} to
* use designate classloader.
*
* @author Kohsuke Kawaguchi
*/
public interface DelegatingCallable<V,T extends Throwable> extends Callable<V,T> {
ClassLoader getClassLoader();
}
......@@ -16,6 +16,10 @@ final class ImportedClassLoaderTable {
this.channel = channel;
}
public synchronized ClassLoader get(int oid) {
return get(RemoteInvocationHandler.wrap(channel,oid,IClassLoader.class));
}
public synchronized ClassLoader get(IClassLoader classLoaderProxy) {
ClassLoader r = classLoaders.get(classLoaderProxy);
if(r==null) {
......
package hudson.remoting;
import java.util.concurrent.*;
import java.io.Serializable;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* {@link VirtualChannel} that performs computation on the local JVM.
......@@ -16,11 +18,11 @@ public class LocalChannel implements VirtualChannel {
this.executor = executor;
}
public <V extends Serializable, T extends Throwable> V call(Callable<V, T> callable) throws T {
public <V, T extends Throwable> V call(Callable<V,T> callable) throws T {
return callable.call();
}
public <V extends Serializable, T extends Throwable> Future<V> callAsync(final Callable<V,T> callable) throws IOException {
public <V, T extends Throwable> Future<V> callAsync(final Callable<V,T> callable) throws IOException {
final java.util.concurrent.Future<V> f = executor.submit(new java.util.concurrent.Callable<V>() {
public V call() throws Exception {
try {
......@@ -61,4 +63,8 @@ public class LocalChannel implements VirtualChannel {
public void close() {
// noop
}
public <T> T export(Class<T> intf, T instance) {
return instance;
}
}
......@@ -4,6 +4,10 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.HashMap;
/**
* {@link ObjectInputStream} that uses a specific class loader.
......@@ -25,4 +29,36 @@ final class ObjectInputStreamEx extends ObjectInputStream {
return super.resolveClass(desc);
}
}
@Override
protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException {
ClassLoader latestLoader = cl;
ClassLoader nonPublicLoader = null;
boolean hasNonPublicInterface = false;
// define proxy in class loader of non-public interface(s), if any
Class[] classObjs = new Class[interfaces.length];
for (int i = 0; i < interfaces.length; i++) {
Class cl = Class.forName(interfaces[i], false, latestLoader);
if ((cl.getModifiers() & Modifier.PUBLIC) == 0) {
if (hasNonPublicInterface) {
if (nonPublicLoader != cl.getClassLoader()) {
throw new IllegalAccessError(
"conflicting non-public interface class loaders");
}
} else {
nonPublicLoader = cl.getClassLoader();
hasNonPublicInterface = true;
}
}
classObjs[i] = cl;
}
try {
return Proxy.getProxyClass(
hasNonPublicInterface ? nonPublicLoader : latestLoader,
classObjs);
} catch (IllegalArgumentException e) {
throw new ClassNotFoundException(null, e);
}
}
}
......@@ -9,7 +9,6 @@ import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.Serializable;
import java.io.ByteArrayOutputStream;
import java.util.logging.Logger;
/**
......@@ -94,7 +93,7 @@ public final class Pipe implements Serializable {
* Creates a {@link Pipe} that allows local system to write and remote system to read.
*/
public static Pipe createLocalToRemote() {
return new Pipe(null,new RemoteOutputStream());
return new Pipe(null,new ProxyOutputStream());
}
private void writeObject(ObjectOutputStream oos) throws IOException {
......@@ -121,7 +120,7 @@ public final class Pipe implements Serializable {
if(ois.readBoolean()) {
// local will write to remote
in = null;
out = new BufferedOutputStream(new RemoteOutputStream(channel, ois.readInt()));
out = new BufferedOutputStream(new ProxyOutputStream(channel, ois.readInt()));
} else {
// local will read from remote.
// tell the remote system about this local read pipe
......@@ -143,139 +142,6 @@ public final class Pipe implements Serializable {
private static final long serialVersionUID = 1L;
/**
* {@link OutputStream} that sends bits to a remote object.
*/
private static class RemoteOutputStream extends OutputStream {
private Channel channel;
private int oid;
/**
* If bytes are written to this stream before it's connected
* to a remote object, bytes will be stored in this buffer.
*/
private ByteArrayOutputStream tmp;
/**
* Set to true if the stream is closed.
*/
private boolean closed;
public RemoteOutputStream() {
}
public RemoteOutputStream(Channel channel, int oid) throws IOException {
connect(channel,oid);
}
/**
* Connects this stream to the specified remote object.
*/
private synchronized void connect(Channel channel, int oid) throws IOException {
if(this.channel!=null)
throw new IllegalStateException("Cannot connect twice");
this.channel = channel;
this.oid = oid;
// if we already have bytes to write, do so now.
if(tmp!=null) {
write(tmp.toByteArray());
tmp = null;
}
if(closed) // already marked closed?
close();
}
public void write(int b) throws IOException {
write(new byte[]{(byte)b},0,1);
}
public void write(byte b[], int off, int len) throws IOException {
if(closed)
throw new IOException("stream is already closed");
if(off==0 && len==b.length)
write(b);
else {
byte[] buf = new byte[len];
System.arraycopy(b,off,buf,0,len);
write(buf);
}
}
public synchronized void write(byte b[]) throws IOException {
if(closed)
throw new IOException("stream is already closed");
if(channel==null) {
if(tmp==null)
tmp = new ByteArrayOutputStream();
tmp.write(b);
} else {
channel.send(new Chunk(oid,b));
}
}
public synchronized void close() throws IOException {
if(channel==null)
closed = true;
else
channel.send(new EOF(oid));
}
/**
* {@link Command} for sending bytes.
*/
private static final class Chunk extends Command {
private final int oid;
private final byte[] buf;
public Chunk(int oid, byte[] buf) {
this.oid = oid;
this.buf = buf;
}
protected void execute(Channel channel) {
OutputStream os = (OutputStream) channel.getExportedObject(oid);
try {
os.write(buf);
} catch (IOException e) {
// ignore errors
}
}
public String toString() {
return "Pipe.Chunk("+oid+","+buf.length+")";
}
private static final long serialVersionUID = 1L;
}
/**
* {@link Command} for sending EOF.
*/
private static final class EOF extends Command {
private final int oid;
public EOF(int oid) {
this.oid = oid;
}
protected void execute(Channel channel) {
OutputStream os = (OutputStream) channel.getExportedObject(oid);
channel.unexport(oid);
try {
os.close();
} catch (IOException e) {
// ignore errors
}
}
public String toString() {
return "Pipe.EOF("+oid+")";
}
private static final long serialVersionUID = 1L;
}
}
private static final Logger logger = Logger.getLogger(Pipe.class.getName());
private static class ConnectCommand extends Command {
......@@ -289,7 +155,7 @@ public final class Pipe implements Serializable {
protected void execute(Channel channel) {
try {
RemoteOutputStream ros = (RemoteOutputStream) channel.getExportedObject(oidRos);
ProxyOutputStream ros = (ProxyOutputStream) channel.getExportedObject(oidRos);
channel.unexport(oidRos);
ros.connect(channel, oidPos);
} catch (IOException e) {
......
package hudson.remoting;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
/**
* {@link InputStream} that reads bits from an exported
* {@link InputStream} on a remote machine.
*
* <p>
* TODO: pre-fetch bytes in advance
*
* @author Kohsuke Kawaguchi
*/
final class ProxyInputStream extends InputStream {
private Channel channel;
private int oid;
/**
* Creates an already connected {@link ProxyOutputStream}.
*
* @param oid
* The object id of the exported {@link OutputStream}.
*/
public ProxyInputStream(Channel channel, int oid) throws IOException {
this.channel = channel;
this.oid = oid;
}
@Override
public int read() throws IOException {
try {
Buffer buf = new Chunk(oid, 1).call(channel);
if(buf.len==1)
return buf.buf[0];
else
return -1;
} catch (InterruptedException e) {
// pretend EOF
Thread.currentThread().interrupt(); // process interrupt later
close();
return -1;
}
}
@Override
public int read(byte b[], int off, int len) throws IOException {
try {
Buffer buf = new Chunk(oid,len).call(channel);
if(buf.len==-1) return -1;
System.arraycopy(buf.buf,0,b,off,buf.len);
return buf.len;
} catch (InterruptedException e) {
// pretend EOF
Thread.currentThread().interrupt(); // process interrupt later
close();
return -1;
}
}
@Override
public synchronized void close() throws IOException {
if(channel!=null) {
channel.send(new EOF(oid));
channel = null;
oid = -1;
}
}
protected void finalize() throws Throwable {
super.finalize();
close();
}
private static final class Buffer implements Serializable {
byte[] buf;
int len;
public Buffer(int len) {
this.buf = new byte[len];
}
public void read(InputStream in) throws IOException {
len = in.read(buf,0,buf.length);
}
private static final long serialVersionUID = 1L;
}
/**
* Command to fetch bytes.
*/
private static final class Chunk extends Request<Buffer,IOException> {
private final int oid;
private final int len;
public Chunk(int oid, int len) {
this.oid = oid;
this.len = len;
}
protected Buffer perform(Channel channel) throws IOException {
InputStream in = (InputStream) channel.getExportedObject(oid);
Buffer buf = new Buffer(len);
buf.read(in);
return buf;
}
}
/**
* {@link Command} for sending EOF.
*/
private static final class EOF extends Command {
private final int oid;
public EOF(int oid) {
this.oid = oid;
}
protected void execute(Channel channel) {
InputStream in = (InputStream) channel.getExportedObject(oid);
channel.unexport(oid);
try {
in.close();
} catch (IOException e) {
// ignore errors
}
}
public String toString() {
return "EOF("+oid+")";
}
private static final long serialVersionUID = 1L;
}
}
package hudson.remoting;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* {@link OutputStream} that sends bits to an exported
* {@link OutputStream} on a remote machine.
*/
final class ProxyOutputStream extends OutputStream {
private Channel channel;
private int oid;
/**
* If bytes are written to this stream before it's connected
* to a remote object, bytes will be stored in this buffer.
*/
private ByteArrayOutputStream tmp;
/**
* Set to true if the stream is closed.
*/
private boolean closed;
/**
* Creates unconnected {@link ProxyOutputStream}.
* The returned stream accepts data right away, and
* when it's {@link #connect(Channel,int) connected} later,
* the data will be sent at once to the remote stream.
*/
public ProxyOutputStream() {
}
/**
* Creates an already connected {@link ProxyOutputStream}.
*
* @param oid
* The object id of the exported {@link OutputStream}.
*/
public ProxyOutputStream(Channel channel, int oid) throws IOException {
connect(channel,oid);
}
/**
* Connects this stream to the specified remote object.
*/
synchronized void connect(Channel channel, int oid) throws IOException {
if(this.channel!=null)
throw new IllegalStateException("Cannot connect twice");
this.channel = channel;
this.oid = oid;
// if we already have bytes to write, do so now.
if(tmp!=null) {
write(tmp.toByteArray());
tmp = null;
}
if(closed) // already marked closed?
close();
}
public void write(int b) throws IOException {
write(new byte[]{(byte)b},0,1);
}
public void write(byte b[], int off, int len) throws IOException {
if(closed)
throw new IOException("stream is already closed");
if(off==0 && len==b.length)
write(b);
else {
byte[] buf = new byte[len];
System.arraycopy(b,off,buf,0,len);
write(buf);
}
}
public synchronized void write(byte b[]) throws IOException {
if(closed)
throw new IOException("stream is already closed");
if(channel==null) {
if(tmp==null)
tmp = new ByteArrayOutputStream();
tmp.write(b);
} else {
channel.send(new Chunk(oid,b));
}
}
public synchronized void close() throws IOException {
closed = true;
if(channel!=null) {
channel.send(new EOF(oid));
channel = null;
oid = -1;
}
}
protected void finalize() throws Throwable {
super.finalize();
close();
}
/**
* {@link Command} for sending bytes.
*/
private static final class Chunk extends Command {
private final int oid;
private final byte[] buf;
public Chunk(int oid, byte[] buf) {
this.oid = oid;
this.buf = buf;
}
protected void execute(Channel channel) {
OutputStream os = (OutputStream) channel.getExportedObject(oid);
try {
os.write(buf);
} catch (IOException e) {
// ignore errors
}
}
public String toString() {
return "Pipe.Chunk("+oid+","+buf.length+")";
}
private static final long serialVersionUID = 1L;
}
/**
* {@link Command} for sending EOF.
*/
private static final class EOF extends Command {
private final int oid;
public EOF(int oid) {
this.oid = oid;
}
protected void execute(Channel channel) {
OutputStream os = (OutputStream) channel.getExportedObject(oid);
channel.unexport(oid);
try {
os.close();
} catch (IOException e) {
// ignore errors
}
}
public String toString() {
return "Pipe.EOF("+oid+")";
}
private static final long serialVersionUID = 1L;
}
}
package hudson.remoting;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
/**
* {@link Writer} that sends bits to an exported
* {@link Writer} on a remote machine.
*/
final class ProxyWriter extends Writer {
private Channel channel;
private int oid;
/**
* If bytes are written to this stream before it's connected
* to a remote object, bytes will be stored in this buffer.
*/
private CharArrayWriter tmp;
/**
* Set to true if the stream is closed.
*/
private boolean closed;
/**
* Creates unconnected {@link ProxyWriter}.
* The returned stream accepts data right away, and
* when it's {@link #connect(Channel,int) connected} later,
* the data will be sent at once to the remote stream.
*/
public ProxyWriter() {
}
/**
* Creates an already connected {@link ProxyWriter}.
*
* @param oid
* The object id of the exported {@link OutputStream}.
*/
public ProxyWriter(Channel channel, int oid) throws IOException {
connect(channel,oid);
}
/**
* Connects this stream to the specified remote object.
*/
synchronized void connect(Channel channel, int oid) throws IOException {
if(this.channel!=null)
throw new IllegalStateException("Cannot connect twice");
this.channel = channel;
this.oid = oid;
// if we already have bytes to write, do so now.
if(tmp!=null) {
write(tmp.toCharArray());
tmp = null;
}
if(closed) // already marked closed?
close();
}
public void write(int c) throws IOException {
write(new char[]{(char)c},0,1);
}
public void write(char[] cbuf, int off, int len) throws IOException {
if(closed)
throw new IOException("stream is already closed");
if(off==0 && len==cbuf.length)
write(cbuf);
else {
char[] buf = new char[len];
System.arraycopy(cbuf,off,buf,0,len);
write(buf);
}
}
public synchronized void write(char[] cbuf) throws IOException {
if(closed)
throw new IOException("stream is already closed");
if(channel==null) {
if(tmp==null)
tmp = new CharArrayWriter();
tmp.write(cbuf);
} else {
channel.send(new Chunk(oid,cbuf));
}
}
public void flush() throws IOException {
// noop
}
public synchronized void close() throws IOException {
closed = true;
if(channel!=null) {
channel.send(new EOF(oid));
channel = null;
oid = -1;
}
}
protected void finalize() throws Throwable {
super.finalize();
close();
}
/**
* {@link Command} for sending bytes.
*/
private static final class Chunk extends Command {
private final int oid;
private final char[] buf;
public Chunk(int oid, char[] buf) {
this.oid = oid;
this.buf = buf;
}
protected void execute(Channel channel) {
Writer os = (Writer) channel.getExportedObject(oid);
try {
os.write(buf);
} catch (IOException e) {
// ignore errors
}
}
public String toString() {
return "Pipe.Chunk("+oid+","+buf.length+")";
}
private static final long serialVersionUID = 1L;
}
/**
* {@link Command} for sending EOF.
*/
private static final class EOF extends Command {
private final int oid;
public EOF(int oid) {
this.oid = oid;
}
protected void execute(Channel channel) {
OutputStream os = (OutputStream) channel.getExportedObject(oid);
channel.unexport(oid);
try {
os.close();
} catch (IOException e) {
// ignore errors
}
}
public String toString() {
return "Pipe.EOF("+oid+")";
}
private static final long serialVersionUID = 1L;
}
}
......@@ -33,6 +33,13 @@ final class RemoteClassLoader extends ClassLoader {
return local.export(IClassLoader.class, new ClassLoaderProxy(cl));
}
/**
* Exports and just returns the object ID, instead of obtaining the proxy.
*/
static int exportId(ClassLoader cl, Channel local) {
return local.export(new ClassLoaderProxy(cl));
}
/*package*/ static final class ClassLoaderProxy implements IClassLoader {
private final ClassLoader cl;
......
package hudson.remoting;
import java.io.InputStream;
import java.io.Serializable;
import java.io.ObjectOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
/**
* @author Kohsuke Kawaguchi
*/
public class RemoteInputStream extends InputStream implements Serializable {
private transient InputStream core;
public RemoteInputStream(InputStream core) {
this.core = core;
}
private void writeObject(ObjectOutputStream oos) throws IOException {
int id = Channel.current().export(core);
oos.writeInt(id);
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
final Channel channel = Channel.current();
assert channel !=null;
this.core = new ProxyInputStream(channel, ois.readInt());
}
private static final long serialVersionUID = 1L;
//
//
// delegation to core
//
//
public int read() throws IOException {
return core.read();
}
public int read(byte[] b) throws IOException {
return core.read(b);
}
public int read(byte[] b, int off, int len) throws IOException {
return core.read(b, off, len);
}
public long skip(long n) throws IOException {
return core.skip(n);
}
public int available() throws IOException {
return core.available();
}
public void close() throws IOException {
core.close();
}
public void mark(int readlimit) {
core.mark(readlimit);
}
public void reset() throws IOException {
core.reset();
}
public boolean markSupported() {
return core.markSupported();
}
}
......@@ -6,6 +6,7 @@ import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* Sits behind a proxy object and implements the proxy logic.
......@@ -21,6 +22,11 @@ final class RemoteInvocationHandler implements InvocationHandler, Serializable {
/**
* Represents the connection to the remote {@link Channel}.
*
* <p>
* This field is null when a {@link RemoteInvocationHandler} is just
* created and not working as a remote proxy. Once tranferred to the
* remote system, this field is set to non-null.
*/
private transient Channel channel;
......@@ -28,6 +34,22 @@ final class RemoteInvocationHandler implements InvocationHandler, Serializable {
this.oid = id;
}
/**
* Creates a proxy that wraps an existing OID on the remote.
*/
RemoteInvocationHandler(Channel channel, int id) {
this.channel = channel;
this.oid = id;
}
/**
* Wraps an OID to the typed wrapper.
*/
public static <T> T wrap(Channel channel, int id, Class<T> type) {
return type.cast(Proxy.newProxyInstance( type.getClassLoader(), new Class[]{type},
new RemoteInvocationHandler(channel,id)));
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(channel==null)
throw new IllegalStateException("proxy is not connected to a channel");
......@@ -57,6 +79,9 @@ final class RemoteInvocationHandler implements InvocationHandler, Serializable {
* Two proxies are the same iff they represent the same remote object.
*/
public boolean equals(Object o) {
if(Proxy.isProxyClass(o.getClass()))
o = Proxy.getInvocationHandler(o);
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
......@@ -70,6 +95,14 @@ final class RemoteInvocationHandler implements InvocationHandler, Serializable {
return oid;
}
protected void finalize() throws Throwable {
// unexport the remote object
if(channel!=null)
channel.send(new UnexportCommand(oid));
super.finalize();
}
private static final long serialVersionUID = 1L;
private static final class RPCRequest extends Request<Serializable,Throwable> {
......@@ -106,7 +139,9 @@ final class RemoteInvocationHandler implements InvocationHandler, Serializable {
if(o==null)
throw new IllegalStateException("Unable to call "+methodName+". Invalid object ID "+oid);
try {
return (Serializable)choose(o).invoke(o,arguments);
Method m = choose(o);
m.setAccessible(true); // in case the class is not public
return (Serializable) m.invoke(o,arguments);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
......
package hudson.remoting;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
/**
* {@link OutputStream} that can be sent over to the remote {@link Channel},
* so that the remote {@link Callable} can write to a local {@link OutputStream}.
*
* <h2>Usage</h2>
* <pre>
* final OutputStream out = new RemoteOutputStream(os);
*
* channel.call(new Callable() {
* public Object call() {
* // this will write to 'os'.
* out.write(...);
* }
* });
* </pre>
*
* @see RemoteInputStream
* @author Kohsuke Kawaguchi
*/
public final class RemoteOutputStream extends OutputStream implements Serializable {
/**
* On local machine, this points to the {@link OutputStream} where
* the data will be sent ultimately.
*
* On remote machine, this points to {@link ProxyOutputStream} that
* does the network proxy.
*/
private transient OutputStream core;
public RemoteOutputStream(OutputStream core) {
this.core = core;
}
private void writeObject(ObjectOutputStream oos) throws IOException {
int id = Channel.current().export(core);
oos.writeInt(id);
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
final Channel channel = Channel.current();
assert channel !=null;
this.core = new ProxyOutputStream(channel, ois.readInt());
}
private static final long serialVersionUID = 1L;
//
//
// delegation to core
//
//
public void write(int b) throws IOException {
core.write(b);
}
public void write(byte[] b) throws IOException {
core.write(b);
}
public void write(byte[] b, int off, int len) throws IOException {
core.write(b, off, len);
}
public void flush() throws IOException {
core.flush();
}
public void close() throws IOException {
core.close();
}
}
package hudson.remoting;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.Writer;
/**
* {@link Writer} that can be sent over to the remote {@link Channel},
* so that the remote {@link Callable} can write to a local {@link Writer}.
*
* <h2>Usage</h2>
* <pre>
* final Writer out = new RemoteWriter(w);
*
* channel.call(new Callable() {
* public Object call() {
* // this will write to 'w'.
* out.write(...);
* }
* });
* </pre>
*
* @see RemoteInputStream
* @author Kohsuke Kawaguchi
*/
public final class RemoteWriter extends Writer implements Serializable {
/**
* On local machine, this points to the {@link Writer} where
* the data will be sent ultimately.
*
* On remote machine, this points to {@link ProxyOutputStream} that
* does the network proxy.
*/
private transient Writer core;
public RemoteWriter(Writer core) {
this.core = core;
}
private void writeObject(ObjectOutputStream oos) throws IOException {
int id = Channel.current().export(core);
oos.writeInt(id);
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
final Channel channel = Channel.current();
assert channel !=null;
this.core = new ProxyWriter(channel, ois.readInt());
}
private static final long serialVersionUID = 1L;
//
//
// delegation to core
//
//
public void write(int c) throws IOException {
core.write(c);
}
public void write(char[] cbuf) throws IOException {
core.write(cbuf);
}
public void write(char[] cbuf, int off, int len) throws IOException {
core.write(cbuf, off, len);
}
public void write(String str) throws IOException {
core.write(str);
}
public void write(String str, int off, int len) throws IOException {
core.write(str, off, len);
}
public Writer append(CharSequence csq) throws IOException {
return core.append(csq);
}
public Writer append(CharSequence csq, int start, int end) throws IOException {
return core.append(csq, start, end);
}
public Writer append(char c) throws IOException {
return core.append(c);
}
public void flush() throws IOException {
core.flush();
}
public void close() throws IOException {
core.close();
}
}
package hudson.remoting;
/**
* {@link Command} that unexports an object.
* @author Kohsuke Kawaguchi
*/
public class UnexportCommand extends Command {
private final int oid;
public UnexportCommand(int oid) {
this.oid = oid;
}
protected void execute(Channel channel) {
channel.unexport(oid);
}
private static final long serialVersionUID = 1L;
}
......@@ -7,6 +7,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.NotSerializableException;
/**
* {@link Request} that can take {@link Callable} whose actual implementation
......@@ -18,7 +19,7 @@ import java.io.Serializable;
*
* @author Kohsuke Kawaguchi
*/
final class UserRequest<RSP extends Serializable,EXC extends Throwable> extends Request<UserResponse<RSP>,EXC> {
final class UserRequest<RSP,EXC extends Throwable> extends Request<UserResponse<RSP>,EXC> {
private final byte[] request;
private final IClassLoader classLoaderProxy;
......@@ -27,30 +28,33 @@ final class UserRequest<RSP extends Serializable,EXC extends Throwable> extends
public UserRequest(Channel local, Callable<?,EXC> c) throws IOException {
request = serialize(c,local);
this.toString = c.toString();
classLoaderProxy = RemoteClassLoader.export( c.getClass().getClassLoader(), local );
ClassLoader cl = c.getClass().getClassLoader();
if(c instanceof DelegatingCallable)
cl = ((DelegatingCallable)c).getClassLoader();
classLoaderProxy = RemoteClassLoader.export(cl,local);
}
protected UserResponse<RSP> perform(Channel channel) throws EXC {
try {
ClassLoader cl = channel.importedClassLoaders.get(classLoaderProxy);
Object o;
RSP r = null;
Channel oldc = Channel.setCurrent(channel);
try {
o = new ObjectInputStreamEx(new ByteArrayInputStream(request), cl).readObject();
} finally {
Channel.setCurrent(oldc);
}
Callable<RSP,EXC> callable = (Callable<RSP,EXC>)o;
Object o = new ObjectInputStreamEx(new ByteArrayInputStream(request), cl).readObject();
Callable<RSP,EXC> callable = (Callable<RSP,EXC>)o;
ClassLoader old = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(cl);
// execute the service
RSP r = null;
try {
r = callable.call();
ClassLoader old = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(cl);
// execute the service
try {
r = callable.call();
} finally {
Thread.currentThread().setContextClassLoader(old);
}
} finally {
Thread.currentThread().setContextClassLoader(old);
Channel.setCurrent(oldc);
}
return new UserResponse<RSP>(serialize(r,channel));
......@@ -69,6 +73,10 @@ final class UserRequest<RSP extends Serializable,EXC extends Throwable> extends
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new ObjectOutputStream(baos).writeObject(o);
return baos.toByteArray();
} catch( NotSerializableException e ) {
IOException x = new IOException("Unable to serialize " + o);
x.initCause(e);
throw x;
} finally {
Channel.setCurrent(old);
}
......@@ -79,7 +87,7 @@ final class UserRequest<RSP extends Serializable,EXC extends Throwable> extends
}
}
final class UserResponse<RSP extends Serializable> implements Serializable {
final class UserResponse<RSP> implements Serializable {
private final byte[] response;
public UserResponse(byte[] response) {
......
package hudson.remoting;
import java.io.Serializable;
import java.io.IOException;
/**
......@@ -20,7 +19,7 @@ public interface VirtualChannel {
* @throws IOException
* If there's any error in the communication between {@link Channel}s.
*/
<V extends Serializable,T extends Throwable>
<V,T extends Throwable>
V call(Callable<V,T> callable) throws IOException, T, InterruptedException;
/**
......@@ -35,11 +34,30 @@ public interface VirtualChannel {
* @throws IOException
* If there's an error during the communication.
*/
<V extends Serializable,T extends Throwable>
<V,T extends Throwable>
Future<V> callAsync(final Callable<V,T> callable) throws IOException;
/**
* Performs an orderly shut down of this channel (and the remote peer.)
*
* @throws IOException
* if the orderly shut-down failed.
*/
void close() throws IOException;
/**
* Exports an object for remoting to the other {@link Channel}
* by creating a remotable proxy.
*
* <p>
* All the parameters and return values must be serializable.
*
* @param type
* Interface to be remoted.
* @return
* the proxy object that implements <tt>T</tt>. This object can be transfered
* to the other {@link Channel}, and calling methods on it from the remote side
* will invoke the same method on the given local <tt>instance</tt> object.
*/
<T> T export( Class<T> type, T instance);
}
......@@ -24,10 +24,38 @@
<resource>
<directory>${basedir}/resources</directory>
</resource>
<resource>
<directory>${basedir}/target/generated-resources</directory>
</resource>
</webResources>
</configuration>
</plugin>
<plugin>
<plugin>
<!-- for copying remoting jar to $WAR/WEB-INF/slave.jar -->
<artifactId>maven-dependency-plugin</artifactId>
<version>2.0-alpha-1-SNAPSHOT</version><!-- this seems to be the only available version -->
<executions>
<execution>
<phase>generate-resources</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.jvnet.hudson.main</groupId>
<artifactId>remoting</artifactId>
<version>${version}</version>
<classifier>${hudsonClassifier}</classifier>
<destFileName>slave.jar</destFileName>
</artifactItem>
</artifactItems>
<outputDirectory>${basedir}/target/generated-resources/WEB-INF</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<configuration>
......
<div>
Command to be used to execute a program on this slave, such as
'ssh slave1' or 'rsh slave2'. Hudson appends the actual command it wants to run
after this and then execute it locally, and then expects the command you supplied
to do the remote job submission.
Command to be used to launch a slave agent program, which controls the slave
computer and communicates with the master. This command is executed on the
master, and Hudson assumes that the executed program launches <tt>slave.jar</tt>
program on the correct slave machine.
<p>
Normally, you'd want to use SSH and RSH for this, but
it can be any custom program as well.
A copy of <tt>slave.jar</tt> can be found on <tt>WEB-INF/slave.jar</tt> inside
<tt>hudson.war</tt>.
<p>
In a simple case, this could be
something like "ssh <i>hostname</i> java -jar ~/bin/slave.jar".
However, it is often a good idea to write a small shell script like the following on a slave,
so that you can control the location of Java and/or slave.jar, as well as set up any
environment variables specific to this slave node, such as PATH.
<pre>
#!/bin/sh
exec java -jar ~/bin/slave.jar
</pre>
<p>
You can use any command to run a process on the slave machine, such as RSH,
as long as stdin/stdout of this process will be connected to
"java -jar ~/bin/slave.jar" eventually.
<p>
In a larger deployment, It is also worth considering to load <tt>slave.jar</tt> from
a NFS-mounted common location, so that you don't have to update this file every time
you update Hudson.
<p>
Setting this to "ssh -v <i>hostname</i>" may be useful for debugging connectivity
......
<div>
<p>
A slave needs to have a directory dedicated for Hudson, and
it needs to be visible from the master Hudson. Specify
the path (from the viewpoint of the master Hudson) to this
work directory, such as '/net/slave1/var/hudson'
<p>
Master and slave needs to be able to read/write this directory
under the same user account.
</div>
\ No newline at end of file
<div>
Specify the same path you specified above in 'local FS root',
but this time from the viewpoint of the slave node, such
as '/var/hudson'
<p>
A slave needs to have a directory dedicated for Hudson. Specify
the absolute path of this work directory on the slave, such as
'/export/home/hudson'.
<p>
Slaves do not maintain important data (other than active workspaces
of projects last built on it), so you can possibly set slave
workspace to a temporary directory. The only downside of doing this
is that you may lose up-to-date workspace if the slave is turned off.
</div>
此差异由.gitattributes 抑制。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册