From 19860fa0706bfae87fbf7fadb9411ad90bc7ee5c Mon Sep 17 00:00:00 2001 From: kohsuke Date: Fri, 29 Dec 2006 19:15:53 +0000 Subject: [PATCH] merging remoting-integration branch git-svn-id: https://hudson.dev.java.net/svn/hudson/trunk/hudson/main@1523 71c3de6d-444a-0410-be80-ed276b4c234a --- core/pom.xml | 15 +- core/src/main/java/hudson/FilePath.java | 574 ++++++++++++++++-- core/src/main/java/hudson/Functions.java | 16 +- core/src/main/java/hudson/Launcher.java | 93 ++- core/src/main/java/hudson/Main.java | 2 +- core/src/main/java/hudson/Plugin.java | 11 +- core/src/main/java/hudson/PluginManager.java | 2 +- core/src/main/java/hudson/PluginWrapper.java | 12 +- core/src/main/java/hudson/Proc.java | 225 ++++--- core/src/main/java/hudson/Util.java | 69 ++- core/src/main/java/hudson/WebAppMain.java | 4 +- core/src/main/java/hudson/XmlFile.java | 2 +- core/src/main/java/hudson/model/Build.java | 38 +- core/src/main/java/hudson/model/Computer.java | 88 ++- .../main/java/hudson/model/Descriptor.java | 1 + .../java/hudson/model/DirectoryHolder.java | 135 ++-- core/src/main/java/hudson/model/Executor.java | 4 +- .../main/java/hudson/model/ExternalRun.java | 2 +- core/src/main/java/hudson/model/Hudson.java | 90 ++- core/src/main/java/hudson/model/Job.java | 4 +- .../main/java/hudson/model/JobCollection.java | 17 +- .../src/main/java/hudson/model/LargeText.java | 43 ++ core/src/main/java/hudson/model/Node.java | 8 + core/src/main/java/hudson/model/Project.java | 48 +- core/src/main/java/hudson/model/Queue.java | 16 +- core/src/main/java/hudson/model/RSS.java | 5 - core/src/main/java/hudson/model/Run.java | 66 +- core/src/main/java/hudson/model/RunMap.java | 12 +- core/src/main/java/hudson/model/Slave.java | 357 ++++++++--- .../hudson/model/StreamBuildListener.java | 42 +- .../main/java/hudson/model/TaskListener.java | 2 +- core/src/main/java/hudson/model/User.java | 2 +- .../main/java/hudson/model/UserProperty.java | 2 +- .../hudson/model/WorkspaceCleanupThread.java | 45 +- .../hudson/model/listeners/JobListener.java | 2 +- .../ant/taskdefs/cvslib/ChangeLogTask.java | 18 +- .../main/java/hudson/scm/CVSChangeLogSet.java | 16 +- core/src/main/java/hudson/scm/CVSSCM.java | 316 ++++++---- core/src/main/java/hudson/scm/NullSCM.java | 3 +- core/src/main/java/hudson/scm/SCM.java | 16 +- core/src/main/java/hudson/scm/SCMS.java | 2 +- .../hudson/scm/SubversionChangeLogParser.java | 9 +- .../hudson/scm/SubversionChangeLogSet.java | 2 +- .../main/java/hudson/scm/SubversionSCM.java | 29 +- core/src/main/java/hudson/tasks/Ant.java | 1 - .../java/hudson/tasks/AntBasedPublisher.java | 23 - .../java/hudson/tasks/ArtifactArchiver.java | 40 +- .../src/main/java/hudson/tasks/BatchFile.java | 48 +- .../src/main/java/hudson/tasks/BuildStep.java | 17 +- .../main/java/hudson/tasks/BuildWrapper.java | 2 +- core/src/main/java/hudson/tasks/Builder.java | 6 +- .../java/hudson/tasks/CommandInterpreter.java | 71 +++ .../main/java/hudson/tasks/Fingerprinter.java | 170 ++++-- .../java/hudson/tasks/JavadocArchiver.java | 43 +- .../main/java/hudson/tasks/LogRotator.java | 3 +- core/src/main/java/hudson/tasks/Mailer.java | 11 +- core/src/main/java/hudson/tasks/Maven.java | 1 - .../src/main/java/hudson/tasks/Publisher.java | 6 +- core/src/main/java/hudson/tasks/Shell.java | 50 +- .../hudson/tasks/junit/AbortException.java | 14 + .../tasks/junit/JUnitResultArchiver.java | 66 +- .../java/hudson/tasks/junit/SuiteResult.java | 3 +- .../java/hudson/tasks/junit/TestObject.java | 4 +- .../java/hudson/tasks/junit/TestResult.java | 19 +- .../hudson/tasks/junit/TestResultAction.java | 15 +- .../tasks/test/AbstractTestResultAction.java | 4 +- .../main/java/hudson/triggers/SCMTrigger.java | 1 - .../java/hudson/triggers/TimerTrigger.java | 1 - core/src/main/java/hudson/util/ChartUtil.java | 4 +- .../java/hudson/util/DaemonThreadFactory.java | 27 + .../main/java/hudson/util/EnumConverter.java | 13 + .../java/hudson/util/FormFieldValidator.java | 7 +- .../util/RetrotranslatorEnumConverter.java | 2 +- .../util/RobustCollectionConverter.java | 6 +- .../java/hudson/util/StreamCopyThread.java | 32 + .../java/hudson/util/StreamTaskListener.java | 23 +- .../hudson/model/AgentSlave/config.jelly | 5 + .../hudson/model/Computer/index.jelly | 14 +- .../hudson/model/Hudson/configure.jelly | 28 +- .../hudson/model/Hudson/systemInfo.jelly | 25 +- .../hudson/model/LegacySlave/config.jelly | 11 + .../model/Project/configure-entries.jelly | 9 +- .../hudson/model/Slave/ComputerImpl/log.jelly | 12 + .../model/Slave/ComputerImpl/sidepanel.jelly | 14 + .../model/Slave/ComputerImpl/systemInfo.jelly | 19 + .../main/resources/lib/hudson/executors.jelly | 2 +- .../resources/lib/hudson/propertyTable.jelly | 19 + remoting/pom.xml | 18 +- .../main/java/hudson/remoting/Channel.java | 82 ++- .../hudson/remoting/DelegatingCallable.java | 17 + .../remoting/ImportedClassLoaderTable.java | 4 + .../java/hudson/remoting/LocalChannel.java | 14 +- .../hudson/remoting/ObjectInputStreamEx.java | 36 ++ .../src/main/java/hudson/remoting/Pipe.java | 140 +---- .../hudson/remoting/ProxyInputStream.java | 140 +++++ .../hudson/remoting/ProxyOutputStream.java | 160 +++++ .../java/hudson/remoting/ProxyWriter.java | 166 +++++ .../hudson/remoting/RemoteClassLoader.java | 7 + .../hudson/remoting/RemoteInputStream.java | 74 +++ .../remoting/RemoteInvocationHandler.java | 37 +- .../hudson/remoting/RemoteOutputStream.java | 81 +++ .../java/hudson/remoting/RemoteWriter.java | 101 +++ .../java/hudson/remoting/UnexportCommand.java | 19 + .../java/hudson/remoting/UserRequest.java | 40 +- .../java/hudson/remoting/VirtualChannel.java | 24 +- war/pom.xml | 30 +- .../system-config/master-slave/command.html | 35 +- .../system-config/master-slave/localFS.html | 11 - .../system-config/master-slave/remoteFS.html | 13 +- war/resources/images/24x24/computer.gif | Bin 0 -> 1166 bytes 110 files changed, 3350 insertions(+), 1255 deletions(-) delete mode 100644 core/src/main/java/hudson/tasks/AntBasedPublisher.java create mode 100644 core/src/main/java/hudson/tasks/CommandInterpreter.java create mode 100644 core/src/main/java/hudson/tasks/junit/AbortException.java create mode 100644 core/src/main/java/hudson/util/DaemonThreadFactory.java create mode 100644 core/src/main/java/hudson/util/EnumConverter.java create mode 100644 core/src/main/java/hudson/util/StreamCopyThread.java create mode 100644 core/src/main/resources/hudson/model/AgentSlave/config.jelly create mode 100644 core/src/main/resources/hudson/model/LegacySlave/config.jelly create mode 100644 core/src/main/resources/hudson/model/Slave/ComputerImpl/log.jelly create mode 100644 core/src/main/resources/hudson/model/Slave/ComputerImpl/sidepanel.jelly create mode 100644 core/src/main/resources/hudson/model/Slave/ComputerImpl/systemInfo.jelly create mode 100644 core/src/main/resources/lib/hudson/propertyTable.jelly create mode 100644 remoting/src/main/java/hudson/remoting/DelegatingCallable.java create mode 100644 remoting/src/main/java/hudson/remoting/ProxyInputStream.java create mode 100644 remoting/src/main/java/hudson/remoting/ProxyOutputStream.java create mode 100644 remoting/src/main/java/hudson/remoting/ProxyWriter.java create mode 100644 remoting/src/main/java/hudson/remoting/RemoteInputStream.java create mode 100644 remoting/src/main/java/hudson/remoting/RemoteOutputStream.java create mode 100644 remoting/src/main/java/hudson/remoting/RemoteWriter.java create mode 100644 remoting/src/main/java/hudson/remoting/UnexportCommand.java delete mode 100644 war/resources/help/system-config/master-slave/localFS.html create mode 100644 war/resources/images/24x24/computer.gif diff --git a/core/pom.xml b/core/pom.xml index 17a8b3a032..3e384d8d3a 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -13,6 +13,17 @@ + + org.kohsuke.stapler + maven-stapler-plugin + + + + stapler + + + + maven-antlr-plugin @@ -43,7 +54,7 @@ - + @@ -197,7 +208,7 @@ org.kohsuke.stapler stapler - 1.13 + 1.14 antlr diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index 80a1ffb42f..32c5fdf7b1 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -1,44 +1,129 @@ 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. * *

- * 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. + * + *

Using {@link FilePath} smartly

+ *

+ * 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. + * + *

+ * 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. + * + *

+ * {@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: + * + *

+ * FilePath file = ...;
+ *
+ * // make 'file' a fresh empty directory.
+ * file.act(new FileCallable<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();
+ *   }
+ * });
+ * 
* *

- * 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. + * + *

+ * {@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 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 act(final FileCallable callable) throws IOException, InterruptedException { + if(channel!=null) { + // run this on a remote system + try { + return channel.call(new DelegatingCallable() { + 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 act(Callable 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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 list(final FileFilter filter) throws IOException, InterruptedException { + return act(new FileCallable>() { + public List invoke(File f, VirtualChannel channel) throws IOException { + File[] children = f.listFiles(filter); + if(children ==null) return null; + + ArrayList r = new ArrayList(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() { + 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() { + 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() { + 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() { + 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() { + 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() { + // 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() { + 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; } diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index 12a53246c1..91bd485381 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -1,24 +1,24 @@ 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()); } diff --git a/core/src/main/java/hudson/Launcher.java b/core/src/main/java/hudson/Launcher.java index 9e1bb47914..2511a9645c 100644 --- a/core/src/main/java/hudson/Launcher.java +++ b/core/src/main/java/hudson/Launcher.java @@ -1,6 +1,9 @@ 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 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 inherit(String[] env) { - Map m = new HashMap(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 inherit(String[] env) { + Map m = new HashMap(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; + } + } } diff --git a/core/src/main/java/hudson/Main.java b/core/src/main/java/hudson/Main.java index 13fc1773fc..e634add6e3 100644 --- a/core/src/main/java/hudson/Main.java +++ b/core/src/main/java/hudson/Main.java @@ -124,7 +124,7 @@ public class Main { List cmd = new ArrayList(); for( int i=1; i 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 env,InputStream in, OutputStream out) throws IOException { - this(cmd,Util.mapToEnv(env),in,out); - } + /** + * Waits for the completion of the process. + * + *

+ * 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 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 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 process; + + public RemoteProc(Future 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(); } } diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java index afe98b2c29..6c2ffe87d7 100644 --- a/core/src/main/java/hudson/Util.java +++ b/core/src/main/java/hudson/Util.java @@ -1,7 +1,12 @@ 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 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 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 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 steps) { + private boolean build(BuildListener listener, Map steps) throws IOException, InterruptedException { for( Builder bs : steps.values() ) if(!bs.perform(Build.this, launcher, listener)) return false; diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java index 28d1a711e6..cc0a57ae0a 100644 --- a/core/src/main/java/hudson/model/Computer.java +++ b/core/src/main/java/hudson/model/Computer.java @@ -1,7 +1,10 @@ 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 executors = new ArrayList(); 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. + * + *

+ * 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 getSystemProperties() throws IOException, InterruptedException { + return getChannel().call(new GetSystemProperties()); + } + + private static final class GetSystemProperties implements Callable,RuntimeException> { + public Map call() { + return new TreeMap(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 getEnvVars() throws IOException, InterruptedException { + return getChannel().call(new GetEnvVars()); + } + + private static final class GetEnvVars implements Callable,RuntimeException> { + public Map call() { + return new TreeMap(EnvVars.masterEnvVars); + } + private static final long serialVersionUID = 1L; + } + + + protected static final ExecutorService threadPoolForRemoting = Executors.newCachedThreadPool(new DaemonThreadFactory()); + // // // UI diff --git a/core/src/main/java/hudson/model/Descriptor.java b/core/src/main/java/hudson/model/Descriptor.java index 7e4042c5a1..eeb4895033 100644 --- a/core/src/main/java/hudson/model/Descriptor.java +++ b/core/src/main/java/hudson/model/Descriptor.java @@ -1,6 +1,7 @@ package hudson.model; import hudson.XmlFile; +import hudson.scm.CVSSCM; import org.kohsuke.stapler.StaplerRequest; import javax.servlet.http.HttpServletRequest; diff --git a/core/src/main/java/hudson/model/DirectoryHolder.java b/core/src/main/java/hudson/model/DirectoryHolder.java index f9d8aaa922..49fba4f7c2 100644 --- a/core/src/main/java/hudson/model/DirectoryHolder.java +++ b/core/src/main/java/hudson/model/DirectoryHolder.java @@ -1,14 +1,18 @@ 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 { + 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> buildChildPathList(File cur) { - List> r = new ArrayList>(); - - 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 l = new ArrayList(); - 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 FILE_SORTER = new Comparator() { + private static final class FileComparator implements Comparator { 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>> { + public List> invoke(File cur, VirtualChannel channel) throws IOException { + List> r = new ArrayList>(); + + 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 l = new ArrayList(); + 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; + } } diff --git a/core/src/main/java/hudson/model/Executor.java b/core/src/main/java/hudson/model/Executor.java index 8af955004a..c757460743 100644 --- a/core/src/main/java/hudson/model/Executor.java +++ b/core/src/main/java/hudson/model/Executor.java @@ -1,14 +1,12 @@ 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. diff --git a/core/src/main/java/hudson/model/ExternalRun.java b/core/src/main/java/hudson/model/ExternalRun.java index 14485acc07..f70f1fd95d 100644 --- a/core/src/main/java/hudson/model/ExternalRun.java +++ b/core/src/main/java/hudson/model/ExternalRun.java @@ -38,7 +38,7 @@ public class ExternalRun extends Run { 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; } diff --git a/core/src/main/java/hudson/model/Hudson.java b/core/src/main/java/hudson/model/Hudson.java index e51112c9f7..02223ccfde 100644 --- a/core/src/main/java/hudson/model/Hudson.java +++ b/core/src/main/java/hudson/model/Hudson.java @@ -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 byName = new HashMap(); for (Computer c : computers.values()) { @@ -313,11 +312,11 @@ public final class Hudson extends JobCollection implements Node { private void updateComputer(Node n, Map byNameMap, Set 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 newSlaves = new ArrayList(); - 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 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; diff --git a/core/src/main/java/hudson/model/Job.java b/core/src/main/java/hudson/model/Job.java index 25357d142d..0aa6a2d98e 100644 --- a/core/src/main/java/hudson/model/Job.java +++ b/core/src/main/java/hudson/model/Job.java @@ -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. diff --git a/core/src/main/java/hudson/model/JobCollection.java b/core/src/main/java/hudson/model/JobCollection.java index 61f3521b42..783316b6f7 100644 --- a/core/src/main/java/hudson/model/JobCollection.java +++ b/core/src/main/java/hudson/model/JobCollection.java @@ -1,23 +1,22 @@ 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. diff --git a/core/src/main/java/hudson/model/LargeText.java b/core/src/main/java/hudson/model/LargeText.java index f946c91c7c..33c0e5476b 100644 --- a/core/src/main/java/hudson/model/LargeText.java +++ b/core/src/main/java/hudson/model/LargeText.java @@ -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. * + *

+ * 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. */ diff --git a/core/src/main/java/hudson/model/Node.java b/core/src/main/java/hudson/model/Node.java index b1150326f6..49cfeb5327 100644 --- a/core/src/main/java/hudson/model/Node.java +++ b/core/src/main/java/hudson/model/Node.java @@ -1,6 +1,8 @@ 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); + } } } diff --git a/core/src/main/java/hudson/model/Project.java b/core/src/main/java/hudson/model/Project.java index 9892576989..87740fb00f 100644 --- a/core/src/main/java/hudson/model/Project.java +++ b/core/src/main/java/hudson/model/Project.java @@ -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 { 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 { 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 { /** * 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); } } diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index 92ec52c484..068024795a 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -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() { diff --git a/core/src/main/java/hudson/model/RSS.java b/core/src/main/java/hudson/model/RSS.java index 32b3e05567..4716fc0d24 100644 --- a/core/src/main/java/hudson/model/RSS.java +++ b/core/src/main/java/hudson/model/RSS.java @@ -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. diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java index 8a19fdf208..045c17ab89 100644 --- a/core/src/main/java/hudson/model/Run.java +++ b/core/src/main/java/hudson/model/Run.java @@ -1,13 +1,15 @@ 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 ,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run() { + 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 { + 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; + } } diff --git a/core/src/main/java/hudson/model/StreamBuildListener.java b/core/src/main/java/hudson/model/StreamBuildListener.java index d79c7c15d7..71d5453608 100644 --- a/core/src/main/java/hudson/model/StreamBuildListener.java +++ b/core/src/main/java/hudson/model/StreamBuildListener.java @@ -1,29 +1,37 @@ 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; } diff --git a/core/src/main/java/hudson/model/TaskListener.java b/core/src/main/java/hudson/model/TaskListener.java index d83b57a3a4..8b3ac410a4 100644 --- a/core/src/main/java/hudson/model/TaskListener.java +++ b/core/src/main/java/hudson/model/TaskListener.java @@ -1,7 +1,7 @@ package hudson.model; -import hudson.util.StreamTaskListener; import hudson.util.NullStream; +import hudson.util.StreamTaskListener; import java.io.PrintStream; import java.io.PrintWriter; diff --git a/core/src/main/java/hudson/model/User.java b/core/src/main/java/hudson/model/User.java index f6fbf8053f..7b4e267b25 100644 --- a/core/src/main/java/hudson/model/User.java +++ b/core/src/main/java/hudson/model/User.java @@ -1,9 +1,9 @@ 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; diff --git a/core/src/main/java/hudson/model/UserProperty.java b/core/src/main/java/hudson/model/UserProperty.java index bcf99f58da..55cde785e1 100644 --- a/core/src/main/java/hudson/model/UserProperty.java +++ b/core/src/main/java/hudson/model/UserProperty.java @@ -1,7 +1,7 @@ package hudson.model; -import hudson.Plugin; import hudson.ExtensionPoint; +import hudson.Plugin; /** * Extensible property of {@link User}. diff --git a/core/src/main/java/hudson/model/WorkspaceCleanupThread.java b/core/src/main/java/hudson/model/WorkspaceCleanupThread.java index 20dc8ce52c..20d31120ca 100644 --- a/core/src/main/java/hudson/model/WorkspaceCleanupThread.java +++ b/core/src/main/java/hudson/model/WorkspaceCleanupThread.java @@ -1,6 +1,6 @@ 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 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; } diff --git a/core/src/main/java/hudson/model/listeners/JobListener.java b/core/src/main/java/hudson/model/listeners/JobListener.java index cea4c5a173..103abeb135 100644 --- a/core/src/main/java/hudson/model/listeners/JobListener.java +++ b/core/src/main/java/hudson/model/listeners/JobListener.java @@ -1,7 +1,7 @@ package hudson.model.listeners; -import hudson.model.Job; import hudson.model.Hudson; +import hudson.model.Job; /** * Receives notifications about jobs. diff --git a/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/ChangeLogTask.java b/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/ChangeLogTask.java index b27cba27f5..ca682ed7f5 100644 --- a/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/ChangeLogTask.java +++ b/core/src/main/java/hudson/org/apache/tools/ant/taskdefs/cvslib/ChangeLogTask.java @@ -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")); diff --git a/core/src/main/java/hudson/scm/CVSChangeLogSet.java b/core/src/main/java/hudson/scm/CVSChangeLogSet.java index 192bc94d8c..5575acaafe 100644 --- a/core/src/main/java/hudson/scm/CVSChangeLogSet.java +++ b/core/src/main/java/hudson/scm/CVSChangeLogSet.java @@ -1,16 +1,16 @@ 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 { 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--) { diff --git a/core/src/main/java/hudson/scm/CVSSCM.java b/core/src/main/java/hudson/scm/CVSSCM.java index dbebde9de5..26c1de0aeb 100644 --- a/core/src/main/java/hudson/scm/CVSSCM.java +++ b/core/src/main/java/hudson/scm/CVSSCM.java @@ -1,6 +1,7 @@ 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. * + *

+ * 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 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 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() { + 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 update(boolean dryRun, Launcher launcher, FilePath workspace, TaskListener listener, Date date) throws IOException { + private List update(boolean dryRun, Launcher launcher, FilePath workspace, TaskListener listener, Date date) throws IOException, InterruptedException { List changedFileNames = new ArrayList(); // 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 moduleNames = new TreeSet(Collections.list(new StringTokenizer(module))); + final Set 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>() { + public Set 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() { + 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 changedFiles, File changelogFile, final BuildListener listener) { + private boolean calcChangeLog(Build build, final List 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() { + 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 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 implements ModelObject { + static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); + /** * Path to .cvspass. 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 cvs log to dump a lot of messages. */ public static boolean debugLogging = false; + + private static final long serialVersionUID = 1L; } diff --git a/core/src/main/java/hudson/scm/NullSCM.java b/core/src/main/java/hudson/scm/NullSCM.java index b6aea4c1af..746de25e4f 100644 --- a/core/src/main/java/hudson/scm/NullSCM.java +++ b/core/src/main/java/hudson/scm/NullSCM.java @@ -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}. * diff --git a/core/src/main/java/hudson/scm/SCM.java b/core/src/main/java/hudson/scm/SCM.java index 33095c5082..b3d875fbc5 100644 --- a/core/src/main/java/hudson/scm/SCM.java +++ b/core/src/main/java/hudson/scm/SCM.java @@ -1,8 +1,8 @@ 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, 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, 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. diff --git a/core/src/main/java/hudson/scm/SCMS.java b/core/src/main/java/hudson/scm/SCMS.java index faa2e8fea5..ff74a14d14 100644 --- a/core/src/main/java/hudson/scm/SCMS.java +++ b/core/src/main/java/hudson/scm/SCMS.java @@ -13,5 +13,5 @@ public class SCMS { */ @SuppressWarnings("unchecked") // generic array creation public static final List> SCMS = - Descriptor.toList(NullSCM.DESCRIPTOR,CVSSCM.DESCRIPTOR,SubversionSCM.DESCRIPTOR); + Descriptor.toList(NullSCM.DESCRIPTOR,CVSSCM.DescriptorImpl.DESCRIPTOR,SubversionSCM.DESCRIPTOR); } diff --git a/core/src/main/java/hudson/scm/SubversionChangeLogParser.java b/core/src/main/java/hudson/scm/SubversionChangeLogParser.java index 3fd3d8639c..db28810f67 100644 --- a/core/src/main/java/hudson/scm/SubversionChangeLogParser.java +++ b/core/src/main/java/hudson/scm/SubversionChangeLogParser.java @@ -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); } diff --git a/core/src/main/java/hudson/scm/SubversionChangeLogSet.java b/core/src/main/java/hudson/scm/SubversionChangeLogSet.java index 3858d5775c..474e7f6171 100644 --- a/core/src/main/java/hudson/scm/SubversionChangeLogSet.java +++ b/core/src/main/java/hudson/scm/SubversionChangeLogSet.java @@ -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. diff --git a/core/src/main/java/hudson/scm/SubversionSCM.java b/core/src/main/java/hudson/scm/SubversionSCM.java index 42bdb55b36..cee294ac39 100644 --- a/core/src/main/java/hudson/scm/SubversionSCM.java +++ b/core/src/main/java/hudson/scm/SubversionSCM.java @@ -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 revMap = buildRevisionMap(workspace,listener); + Map revMap = buildRevisionMap(workspace,launcher,listener); for (Entry 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 buildRevisionMap(FilePath workspace, TaskListener listener) throws IOException { + private Map buildRevisionMap(FilePath workspace, Launcher launcher, TaskListener listener) throws IOException { PrintStream logger = listener.getLogger(); Map revisions = new HashMap(); @@ -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 wsRev = buildRevisionMap(workspace,listener); + Map 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; diff --git a/core/src/main/java/hudson/tasks/Ant.java b/core/src/main/java/hudson/tasks/Ant.java index 88cf46b768..a6be1d6613 100644 --- a/core/src/main/java/hudson/tasks/Ant.java +++ b/core/src/main/java/hudson/tasks/Ant.java @@ -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; diff --git a/core/src/main/java/hudson/tasks/AntBasedPublisher.java b/core/src/main/java/hudson/tasks/AntBasedPublisher.java deleted file mode 100644 index 2d5dc2df43..0000000000 --- a/core/src/main/java/hudson/tasks/AntBasedPublisher.java +++ /dev/null @@ -1,23 +0,0 @@ -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())); - } - } -} diff --git a/core/src/main/java/hudson/tasks/ArtifactArchiver.java b/core/src/main/java/hudson/tasks/ArtifactArchiver.java index fbf919fd42..411f2eaa5c 100644 --- a/core/src/main/java/hudson/tasks/ArtifactArchiver.java +++ b/core/src/main/java/hudson/tasks/ArtifactArchiver.java @@ -1,15 +1,12 @@ 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(); diff --git a/core/src/main/java/hudson/tasks/BatchFile.java b/core/src/main/java/hudson/tasks/BatchFile.java index 2df903ef85..f8b733c8d0 100644 --- a/core/src/main/java/hudson/tasks/BatchFile.java +++ b/core/src/main/java/hudson/tasks/BatchFile.java @@ -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 getDescriptor() { diff --git a/core/src/main/java/hudson/tasks/BuildStep.java b/core/src/main/java/hudson/tasks/BuildStep.java index 43e6bd30a6..a443ea8227 100644 --- a/core/src/main/java/hudson/tasks/BuildStep.java +++ b/core/src/main/java/hudson/tasks/BuildStep.java @@ -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 ); diff --git a/core/src/main/java/hudson/tasks/BuildWrapper.java b/core/src/main/java/hudson/tasks/BuildWrapper.java index d279e817c9..0238d9960d 100644 --- a/core/src/main/java/hudson/tasks/BuildWrapper.java +++ b/core/src/main/java/hudson/tasks/BuildWrapper.java @@ -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. diff --git a/core/src/main/java/hudson/tasks/Builder.java b/core/src/main/java/hudson/tasks/Builder.java index 169be88132..10f5eeacc5 100644 --- a/core/src/main/java/hudson/tasks/Builder.java +++ b/core/src/main/java/hudson/tasks/Builder.java @@ -1,11 +1,11 @@ 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. diff --git a/core/src/main/java/hudson/tasks/CommandInterpreter.java b/core/src/main/java/hudson/tasks/CommandInterpreter.java new file mode 100644 index 0000000000..c70ddd8a75 --- /dev/null +++ b/core/src/main/java/hudson/tasks/CommandInterpreter.java @@ -0,0 +1,71 @@ +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(); +} diff --git a/core/src/main/java/hudson/tasks/Fingerprinter.java b/core/src/main/java/hudson/tasks/Fingerprinter.java index 108a3b210a..ae3e470031 100644 --- a/core/src/main/java/hudson/tasks/Fingerprinter.java +++ b/core/src/main/java/hudson/tasks/Fingerprinter.java @@ -1,15 +1,19 @@ 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 record = new HashMap(); + Map record = new HashMap(); - 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 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 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 records = p.getWorkspace().act(new FileCallable>() { + public List invoke(File baseDir, VirtualChannel channel) throws IOException { + List results = new ArrayList(); + + 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 DESCRIPTOR = new Descriptor(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; } diff --git a/core/src/main/java/hudson/tasks/JavadocArchiver.java b/core/src/main/java/hudson/tasks/JavadocArchiver.java index 3d4df3e8d9..71a8a8fc5d 100644 --- a/core/src/main/java/hudson/tasks/JavadocArchiver.java +++ b/core/src/main/java/hudson/tasks/JavadocArchiver.java @@ -1,6 +1,8 @@ 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); } } } diff --git a/core/src/main/java/hudson/tasks/LogRotator.java b/core/src/main/java/hudson/tasks/LogRotator.java index b1b3e378db..e8e3b82a11 100644 --- a/core/src/main/java/hudson/tasks/LogRotator.java +++ b/core/src/main/java/hudson/tasks/LogRotator.java @@ -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. * diff --git a/core/src/main/java/hudson/tasks/Mailer.java b/core/src/main/java/hudson/tasks/Mailer.java index 648203d9bc..794f848ee9 100644 --- a/core/src/main/java/hudson/tasks/Mailer.java +++ b/core/src/main/java/hudson/tasks/Mailer.java @@ -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]; diff --git a/core/src/main/java/hudson/tasks/Maven.java b/core/src/main/java/hudson/tasks/Maven.java index f85c9705e1..3603fad492 100644 --- a/core/src/main/java/hudson/tasks/Maven.java +++ b/core/src/main/java/hudson/tasks/Maven.java @@ -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; diff --git a/core/src/main/java/hudson/tasks/Publisher.java b/core/src/main/java/hudson/tasks/Publisher.java index 3279e1bd25..e32f941f45 100644 --- a/core/src/main/java/hudson/tasks/Publisher.java +++ b/core/src/main/java/hudson/tasks/Publisher.java @@ -1,11 +1,11 @@ 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. diff --git a/core/src/main/java/hudson/tasks/Shell.java b/core/src/main/java/hudson/tasks/Shell.java index 1d979d2e1b..707b82411a 100644 --- a/core/src/main/java/hudson/tasks/Shell.java +++ b/core/src/main/java/hudson/tasks/Shell.java @@ -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 getDescriptor() { diff --git a/core/src/main/java/hudson/tasks/junit/AbortException.java b/core/src/main/java/hudson/tasks/junit/AbortException.java new file mode 100644 index 0000000000..af8870b8bc --- /dev/null +++ b/core/src/main/java/hudson/tasks/junit/AbortException.java @@ -0,0 +1,14 @@ +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; +} diff --git a/core/src/main/java/hudson/tasks/junit/JUnitResultArchiver.java b/core/src/main/java/hudson/tasks/junit/JUnitResultArchiver.java index 677621c3f5..ea24735adb 100644 --- a/core/src/main/java/hudson/tasks/junit/JUnitResultArchiver.java +++ b/core/src/main/java/hudson/tasks/junit/JUnitResultArchiver.java @@ -1,24 +1,28 @@ 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() { + 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 getDescriptor() { - return DESCRIPTOR; + return DescriptorImpl.DESCRIPTOR; } - public static final Descriptor DESCRIPTOR = new Descriptor(JUnitResultArchiver.class) { + private static final long serialVersionUID = 1L; + + public static class DescriptorImpl extends Descriptor { + public static final Descriptor 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")); } - }; + } } diff --git a/core/src/main/java/hudson/tasks/junit/SuiteResult.java b/core/src/main/java/hudson/tasks/junit/SuiteResult.java index 3858821663..1e21282706 100644 --- a/core/src/main/java/hudson/tasks/junit/SuiteResult.java +++ b/core/src/main/java/hudson/tasks/junit/SuiteResult.java @@ -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; diff --git a/core/src/main/java/hudson/tasks/junit/TestObject.java b/core/src/main/java/hudson/tasks/junit/TestObject.java index f9e42661b6..bae2820243 100644 --- a/core/src/main/java/hudson/tasks/junit/TestObject.java +++ b/core/src/main/java/hudson/tasks/junit/TestObject.java @@ -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(); /** diff --git a/core/src/main/java/hudson/tasks/junit/TestResult.java b/core/src/main/java/hudson/tasks/junit/TestResult.java index cbe935167a..7554c3f149 100644 --- a/core/src/main/java/hudson/tasks/junit/TestResult.java +++ b/core/src/main/java/hudson/tasks/junit/TestResult.java @@ -1,13 +1,14 @@ 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 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(); totalTests = 0; failedTests = new ArrayList(); diff --git a/core/src/main/java/hudson/tasks/junit/TestResultAction.java b/core/src/main/java/hudson/tasks/junit/TestResultAction.java index bb1960bd25..1d813ef3a3 100644 --- a/core/src/main/java/hudson/tasks/junit/TestResultAction.java +++ b/core/src/main/java/hudson/tasks/junit/TestResultAction.java @@ -35,22 +35,22 @@ public class TestResultAction extends AbstractTestResultAction 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(r); + this.result = new WeakReference(result); } private XmlFile getDataFile() { @@ -100,8 +100,7 @@ public class TestResultAction extends AbstractTestResultAction 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; } diff --git a/core/src/main/java/hudson/tasks/test/AbstractTestResultAction.java b/core/src/main/java/hudson/tasks/test/AbstractTestResultAction.java index 1dc14be525..9be8c37f6c 100644 --- a/core/src/main/java/hudson/tasks/test/AbstractTestResultAction.java +++ b/core/src/main/java/hudson/tasks/test/AbstractTestResultAction.java @@ -1,14 +1,14 @@ 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; diff --git a/core/src/main/java/hudson/triggers/SCMTrigger.java b/core/src/main/java/hudson/triggers/SCMTrigger.java index 00d2486217..119c1f8e91 100644 --- a/core/src/main/java/hudson/triggers/SCMTrigger.java +++ b/core/src/main/java/hudson/triggers/SCMTrigger.java @@ -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; diff --git a/core/src/main/java/hudson/triggers/TimerTrigger.java b/core/src/main/java/hudson/triggers/TimerTrigger.java index e3cb45439c..5f70abfb2c 100644 --- a/core/src/main/java/hudson/triggers/TimerTrigger.java +++ b/core/src/main/java/hudson/triggers/TimerTrigger.java @@ -2,7 +2,6 @@ package hudson.triggers; import antlr.ANTLRException; import hudson.model.Descriptor; - import org.kohsuke.stapler.StaplerRequest; /** diff --git a/core/src/main/java/hudson/util/ChartUtil.java b/core/src/main/java/hudson/util/ChartUtil.java index 54d63d9667..54a321fd1b 100644 --- a/core/src/main/java/hudson/util/ChartUtil.java +++ b/core/src/main/java/hudson/util/ChartUtil.java @@ -1,11 +1,11 @@ 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; diff --git a/core/src/main/java/hudson/util/DaemonThreadFactory.java b/core/src/main/java/hudson/util/DaemonThreadFactory.java new file mode 100644 index 0000000000..264ebce3d2 --- /dev/null +++ b/core/src/main/java/hudson/util/DaemonThreadFactory.java @@ -0,0 +1,27 @@ +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; + } +} diff --git a/core/src/main/java/hudson/util/EnumConverter.java b/core/src/main/java/hudson/util/EnumConverter.java new file mode 100644 index 0000000000..bc51066fb9 --- /dev/null +++ b/core/src/main/java/hudson/util/EnumConverter.java @@ -0,0 +1,13 @@ +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()); + } +} diff --git a/core/src/main/java/hudson/util/FormFieldValidator.java b/core/src/main/java/hudson/util/FormFieldValidator.java index d8edeb3bf7..b4bf06d13e 100644 --- a/core/src/main/java/hudson/util/FormFieldValidator.java +++ b/core/src/main/java/hudson/util/FormFieldValidator.java @@ -1,14 +1,13 @@ 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 diff --git a/core/src/main/java/hudson/util/RetrotranslatorEnumConverter.java b/core/src/main/java/hudson/util/RetrotranslatorEnumConverter.java index 5f6c114221..a0cfd47cba 100644 --- a/core/src/main/java/hudson/util/RetrotranslatorEnumConverter.java +++ b/core/src/main/java/hudson/util/RetrotranslatorEnumConverter.java @@ -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_; /** diff --git a/core/src/main/java/hudson/util/RobustCollectionConverter.java b/core/src/main/java/hudson/util/RobustCollectionConverter.java index 2eefa6a7e8..d30f098c2b 100644 --- a/core/src/main/java/hudson/util/RobustCollectionConverter.java +++ b/core/src/main/java/hudson/util/RobustCollectionConverter.java @@ -1,10 +1,10 @@ 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; diff --git a/core/src/main/java/hudson/util/StreamCopyThread.java b/core/src/main/java/hudson/util/StreamCopyThread.java new file mode 100644 index 0000000000..99641b54c5 --- /dev/null +++ b/core/src/main/java/hudson/util/StreamCopyThread.java @@ -0,0 +1,32 @@ +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? + } + } +} diff --git a/core/src/main/java/hudson/util/StreamTaskListener.java b/core/src/main/java/hudson/util/StreamTaskListener.java index 9d82785349..80321b8d37 100644 --- a/core/src/main/java/hudson/util/StreamTaskListener.java +++ b/core/src/main/java/hudson/util/StreamTaskListener.java @@ -1,20 +1,29 @@ 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. * + *

+ * 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; } diff --git a/core/src/main/resources/hudson/model/AgentSlave/config.jelly b/core/src/main/resources/hudson/model/AgentSlave/config.jelly new file mode 100644 index 0000000000..0d9fe999d8 --- /dev/null +++ b/core/src/main/resources/hudson/model/AgentSlave/config.jelly @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Computer/index.jelly b/core/src/main/resources/hudson/model/Computer/index.jelly index ad9fb7af99..5f9f40c297 100644 --- a/core/src/main/resources/hudson/model/Computer/index.jelly +++ b/core/src/main/resources/hudson/model/Computer/index.jelly @@ -18,7 +18,19 @@ Slave ${it.displayName} - + + +

+ This node is offline because Hudson failed to launch the slave agent on it. + See log for more details +

+ +
+ +
+
+ +

Projects tied on ${it.displayName}

diff --git a/core/src/main/resources/hudson/model/Hudson/configure.jelly b/core/src/main/resources/hudson/model/Hudson/configure.jelly index c5fc0db8f8..da7c822cb3 100644 --- a/core/src/main/resources/hudson/model/Hudson/configure.jelly +++ b/core/src/main/resources/hudson/model/Hudson/configure.jelly @@ -35,38 +35,28 @@ - + + + + + - + - - - - - - - - - - + - ${m.description} diff --git a/core/src/main/resources/hudson/model/Hudson/systemInfo.jelly b/core/src/main/resources/hudson/model/Hudson/systemInfo.jelly index 710a1ae28d..8467e1c625 100644 --- a/core/src/main/resources/hudson/model/Hudson/systemInfo.jelly +++ b/core/src/main/resources/hudson/model/Hudson/systemInfo.jelly @@ -2,33 +2,14 @@ Various system information for diagnostics --> - + - - - - -
- - - - - - - - - - -
NameValue
- - -

System Properties

- +

Environment Variables

- +
diff --git a/core/src/main/resources/hudson/model/LegacySlave/config.jelly b/core/src/main/resources/hudson/model/LegacySlave/config.jelly new file mode 100644 index 0000000000..a9f8ed33d2 --- /dev/null +++ b/core/src/main/resources/hudson/model/LegacySlave/config.jelly @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Project/configure-entries.jelly b/core/src/main/resources/hudson/model/Project/configure-entries.jelly index 3f31254221..b11e03f685 100644 --- a/core/src/main/resources/hudson/model/Project/configure-entries.jelly +++ b/core/src/main/resources/hudson/model/Project/configure-entries.jelly @@ -59,14 +59,7 @@ - - - - - - - - + diff --git a/core/src/main/resources/hudson/model/Slave/ComputerImpl/log.jelly b/core/src/main/resources/hudson/model/Slave/ComputerImpl/log.jelly new file mode 100644 index 0000000000..20cc2a2796 --- /dev/null +++ b/core/src/main/resources/hudson/model/Slave/ComputerImpl/log.jelly @@ -0,0 +1,12 @@ + + + + +

+      
+ +
+ +
+
+
diff --git a/core/src/main/resources/hudson/model/Slave/ComputerImpl/sidepanel.jelly b/core/src/main/resources/hudson/model/Slave/ComputerImpl/sidepanel.jelly new file mode 100644 index 0000000000..755b1ad0d0 --- /dev/null +++ b/core/src/main/resources/hudson/model/Slave/ComputerImpl/sidepanel.jelly @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Slave/ComputerImpl/systemInfo.jelly b/core/src/main/resources/hudson/model/Slave/ComputerImpl/systemInfo.jelly new file mode 100644 index 0000000000..88f2a6d7e1 --- /dev/null +++ b/core/src/main/resources/hudson/model/Slave/ComputerImpl/systemInfo.jelly @@ -0,0 +1,19 @@ + + + + + + + +

System Properties

+ +

Environment Variables

+ +
+
+
+
diff --git a/core/src/main/resources/lib/hudson/executors.jelly b/core/src/main/resources/lib/hudson/executors.jelly index 62f42b046f..2033acb673 100644 --- a/core/src/main/resources/lib/hudson/executors.jelly +++ b/core/src/main/resources/lib/hudson/executors.jelly @@ -16,7 +16,7 @@ ${c.displayName} - (offline) + (offline) diff --git a/core/src/main/resources/lib/hudson/propertyTable.jelly b/core/src/main/resources/lib/hudson/propertyTable.jelly new file mode 100644 index 0000000000..083fcb3fa2 --- /dev/null +++ b/core/src/main/resources/lib/hudson/propertyTable.jelly @@ -0,0 +1,19 @@ + + + + + + + + + + + + + +
NameValue
+
\ No newline at end of file diff --git a/remoting/pom.xml b/remoting/pom.xml index e1794e85a1..f011fd4b52 100644 --- a/remoting/pom.xml +++ b/remoting/pom.xml @@ -117,10 +117,14 @@ - + - + + + + +
@@ -154,6 +158,16 @@
+ + maven-jar-plugin + + + + hudson.remoting.Launcher + + + +
diff --git a/remoting/src/main/java/hudson/remoting/Channel.java b/remoting/src/main/java/hudson/remoting/Channel.java index 884532b4f7..457207e373 100644 --- a/remoting/src/main/java/hudson/remoting/Channel.java +++ b/remoting/src/main/java/hudson/remoting/Channel.java @@ -1,15 +1,15 @@ 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 exportedObjects = new ExportTable(); + /** + * Registered listeners. + */ + private final Vector listeners = new Vector(); + 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 T. This object can be transfered - * to the other {@link Channel}, and calling methods on it will invoke the - * same method on the given instance object. + * {@inheritDoc} */ - /*package*/ synchronized T export(Class type, T instance) { + public T export(Class 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 + public V call(Callable callable) throws IOException, T, InterruptedException { - UserResponse r = new UserRequest(this, callable).call(this); try { + UserResponse r = new UserRequest(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 + public Future callAsync(final Callable callable) throws IOException { - final Future> f = new UserRequest(this, callable).callAsync(this); + final Future> f = new UserRequest(this, callable).callAsync(this); return new FutureAdapter>(f) { protected V adapt(UserResponse 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. diff --git a/remoting/src/main/java/hudson/remoting/DelegatingCallable.java b/remoting/src/main/java/hudson/remoting/DelegatingCallable.java new file mode 100644 index 0000000000..191348ff3b --- /dev/null +++ b/remoting/src/main/java/hudson/remoting/DelegatingCallable.java @@ -0,0 +1,17 @@ +package hudson.remoting; + +/** + * {@link Callable} that nominates another claassloader for serialization. + * + *

+ * 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 extends Callable { + ClassLoader getClassLoader(); +} diff --git a/remoting/src/main/java/hudson/remoting/ImportedClassLoaderTable.java b/remoting/src/main/java/hudson/remoting/ImportedClassLoaderTable.java index a118845b7e..0256365c3e 100644 --- a/remoting/src/main/java/hudson/remoting/ImportedClassLoaderTable.java +++ b/remoting/src/main/java/hudson/remoting/ImportedClassLoaderTable.java @@ -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) { diff --git a/remoting/src/main/java/hudson/remoting/LocalChannel.java b/remoting/src/main/java/hudson/remoting/LocalChannel.java index f9f130a7d8..b52db3c338 100644 --- a/remoting/src/main/java/hudson/remoting/LocalChannel.java +++ b/remoting/src/main/java/hudson/remoting/LocalChannel.java @@ -1,8 +1,10 @@ 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 call(Callable callable) throws T { + public V call(Callable callable) throws T { return callable.call(); } - public Future callAsync(final Callable callable) throws IOException { + public Future callAsync(final Callable callable) throws IOException { final java.util.concurrent.Future f = executor.submit(new java.util.concurrent.Callable() { public V call() throws Exception { try { @@ -61,4 +63,8 @@ public class LocalChannel implements VirtualChannel { public void close() { // noop } + + public T export(Class intf, T instance) { + return instance; + } } diff --git a/remoting/src/main/java/hudson/remoting/ObjectInputStreamEx.java b/remoting/src/main/java/hudson/remoting/ObjectInputStreamEx.java index d412192e93..1f9acf3258 100644 --- a/remoting/src/main/java/hudson/remoting/ObjectInputStreamEx.java +++ b/remoting/src/main/java/hudson/remoting/ObjectInputStreamEx.java @@ -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); + } + } } diff --git a/remoting/src/main/java/hudson/remoting/Pipe.java b/remoting/src/main/java/hudson/remoting/Pipe.java index ffff4eb6cd..452dba8c9f 100644 --- a/remoting/src/main/java/hudson/remoting/Pipe.java +++ b/remoting/src/main/java/hudson/remoting/Pipe.java @@ -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) { diff --git a/remoting/src/main/java/hudson/remoting/ProxyInputStream.java b/remoting/src/main/java/hudson/remoting/ProxyInputStream.java new file mode 100644 index 0000000000..65eee9e70a --- /dev/null +++ b/remoting/src/main/java/hudson/remoting/ProxyInputStream.java @@ -0,0 +1,140 @@ +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. + * + *

+ * 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 { + 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; + } +} diff --git a/remoting/src/main/java/hudson/remoting/ProxyOutputStream.java b/remoting/src/main/java/hudson/remoting/ProxyOutputStream.java new file mode 100644 index 0000000000..9a29588ab7 --- /dev/null +++ b/remoting/src/main/java/hudson/remoting/ProxyOutputStream.java @@ -0,0 +1,160 @@ +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; + } +} diff --git a/remoting/src/main/java/hudson/remoting/ProxyWriter.java b/remoting/src/main/java/hudson/remoting/ProxyWriter.java new file mode 100644 index 0000000000..9adac32ba7 --- /dev/null +++ b/remoting/src/main/java/hudson/remoting/ProxyWriter.java @@ -0,0 +1,166 @@ +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; + } +} diff --git a/remoting/src/main/java/hudson/remoting/RemoteClassLoader.java b/remoting/src/main/java/hudson/remoting/RemoteClassLoader.java index 74b8bea94a..d3ff97eddd 100644 --- a/remoting/src/main/java/hudson/remoting/RemoteClassLoader.java +++ b/remoting/src/main/java/hudson/remoting/RemoteClassLoader.java @@ -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; diff --git a/remoting/src/main/java/hudson/remoting/RemoteInputStream.java b/remoting/src/main/java/hudson/remoting/RemoteInputStream.java new file mode 100644 index 0000000000..046a06d6a4 --- /dev/null +++ b/remoting/src/main/java/hudson/remoting/RemoteInputStream.java @@ -0,0 +1,74 @@ +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(); + } +} diff --git a/remoting/src/main/java/hudson/remoting/RemoteInvocationHandler.java b/remoting/src/main/java/hudson/remoting/RemoteInvocationHandler.java index 723af0f5a6..44a8a388b8 100644 --- a/remoting/src/main/java/hudson/remoting/RemoteInvocationHandler.java +++ b/remoting/src/main/java/hudson/remoting/RemoteInvocationHandler.java @@ -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}. + * + *

+ * 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 wrap(Channel channel, int id, Class 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 { @@ -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(); } diff --git a/remoting/src/main/java/hudson/remoting/RemoteOutputStream.java b/remoting/src/main/java/hudson/remoting/RemoteOutputStream.java new file mode 100644 index 0000000000..e87a952549 --- /dev/null +++ b/remoting/src/main/java/hudson/remoting/RemoteOutputStream.java @@ -0,0 +1,81 @@ +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}. + * + *

Usage

+ *
+ * final OutputStream out = new RemoteOutputStream(os);
+ *
+ * channel.call(new Callable() {
+ *   public Object call() {
+ *     // this will write to 'os'.
+ *     out.write(...);
+ *   }
+ * });
+ * 
+ * + * @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(); + } +} diff --git a/remoting/src/main/java/hudson/remoting/RemoteWriter.java b/remoting/src/main/java/hudson/remoting/RemoteWriter.java new file mode 100644 index 0000000000..3cb8ab2e4a --- /dev/null +++ b/remoting/src/main/java/hudson/remoting/RemoteWriter.java @@ -0,0 +1,101 @@ +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}. + * + *

Usage

+ *
+ * final Writer out = new RemoteWriter(w);
+ *
+ * channel.call(new Callable() {
+ *   public Object call() {
+ *     // this will write to 'w'.
+ *     out.write(...);
+ *   }
+ * });
+ * 
+ * + * @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(); + } +} diff --git a/remoting/src/main/java/hudson/remoting/UnexportCommand.java b/remoting/src/main/java/hudson/remoting/UnexportCommand.java new file mode 100644 index 0000000000..3c9637789a --- /dev/null +++ b/remoting/src/main/java/hudson/remoting/UnexportCommand.java @@ -0,0 +1,19 @@ +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; +} diff --git a/remoting/src/main/java/hudson/remoting/UserRequest.java b/remoting/src/main/java/hudson/remoting/UserRequest.java index b5c40ab841..a932e2e079 100644 --- a/remoting/src/main/java/hudson/remoting/UserRequest.java +++ b/remoting/src/main/java/hudson/remoting/UserRequest.java @@ -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 extends Request,EXC> { +final class UserRequest extends Request,EXC> { private final byte[] request; private final IClassLoader classLoaderProxy; @@ -27,30 +28,33 @@ final class UserRequest extends public UserRequest(Channel local, Callable 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 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 callable = (Callable)o; + Object o = new ObjectInputStreamEx(new ByteArrayInputStream(request), cl).readObject(); + + Callable callable = (Callable)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(serialize(r,channel)); @@ -69,6 +73,10 @@ final class UserRequest 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 extends } } -final class UserResponse implements Serializable { +final class UserResponse implements Serializable { private final byte[] response; public UserResponse(byte[] response) { diff --git a/remoting/src/main/java/hudson/remoting/VirtualChannel.java b/remoting/src/main/java/hudson/remoting/VirtualChannel.java index 75e0153ec3..6cbc8077f2 100644 --- a/remoting/src/main/java/hudson/remoting/VirtualChannel.java +++ b/remoting/src/main/java/hudson/remoting/VirtualChannel.java @@ -1,6 +1,5 @@ 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 call(Callable callable) throws IOException, T, InterruptedException; /** @@ -35,11 +34,30 @@ public interface VirtualChannel { * @throws IOException * If there's an error during the communication. */ - + Future callAsync(final Callable 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. + * + *

+ * All the parameters and return values must be serializable. + * + * @param type + * Interface to be remoted. + * @return + * the proxy object that implements T. 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 instance object. + */ + T export( Class type, T instance); } diff --git a/war/pom.xml b/war/pom.xml index e7a573595b..137fe98a3f 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -24,10 +24,38 @@ ${basedir}/resources + + ${basedir}/target/generated-resources + - + + + maven-dependency-plugin + 2.0-alpha-1-SNAPSHOT + + + generate-resources + + copy + + + + + org.jvnet.hudson.main + remoting + ${version} + ${hudsonClassifier} + slave.jar + + + ${basedir}/target/generated-resources/WEB-INF + + + + + org.mortbay.jetty maven-jetty-plugin diff --git a/war/resources/help/system-config/master-slave/command.html b/war/resources/help/system-config/master-slave/command.html index 5d4e7b77a9..5b8b34854d 100644 --- a/war/resources/help/system-config/master-slave/command.html +++ b/war/resources/help/system-config/master-slave/command.html @@ -1,12 +1,35 @@

- 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 slave.jar + program on the correct slave machine.

- Normally, you'd want to use SSH and RSH for this, but - it can be any custom program as well. + A copy of slave.jar can be found on WEB-INF/slave.jar inside + hudson.war. + +

+ In a simple case, this could be + something like "ssh hostname 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. + +

+#!/bin/sh
+exec java -jar ~/bin/slave.jar
+
+ +

+ 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. + +

+ In a larger deployment, It is also worth considering to load slave.jar from + a NFS-mounted common location, so that you don't have to update this file every time + you update Hudson.

Setting this to "ssh -v hostname" may be useful for debugging connectivity diff --git a/war/resources/help/system-config/master-slave/localFS.html b/war/resources/help/system-config/master-slave/localFS.html deleted file mode 100644 index 5d5c58a359..0000000000 --- a/war/resources/help/system-config/master-slave/localFS.html +++ /dev/null @@ -1,11 +0,0 @@ -

-

- 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' - -

- Master and slave needs to be able to read/write this directory - under the same user account. -

\ No newline at end of file diff --git a/war/resources/help/system-config/master-slave/remoteFS.html b/war/resources/help/system-config/master-slave/remoteFS.html index c10778fa73..a8ac357c19 100644 --- a/war/resources/help/system-config/master-slave/remoteFS.html +++ b/war/resources/help/system-config/master-slave/remoteFS.html @@ -1,5 +1,12 @@
- 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' +

+ 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'. + +

+ 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.

diff --git a/war/resources/images/24x24/computer.gif b/war/resources/images/24x24/computer.gif new file mode 100644 index 0000000000000000000000000000000000000000..454aa3c0378029b0c9da54f56504997ded4eee1f GIT binary patch literal 1166 zcmdUu`%hDM7>5rsXUP&2k^#wx3j@d0A`4p<$jo3F%4G~;Cadc-z%(EiYi%itQK*#D zmZBM>wA`FPZ?vV{bV|8Ydx3H2#ZgdbX=%^t4Hm*k!w3=t_GSOYo;>;F`StzbP0Ki& zm{br3g+ZGT^hNrun%e5ri=4Fl`)Tlk+m{rKlD&l4ranBX=vP*c` z#r*6N{<#uwGt@pc{d+jt$b8^x2x=KC#kBdtV(d5 zE+El`*XcsiJrVhy@W%d%Z`6p$HDJXQ1_%j-AtBdFC_ot4O=WhM*NSPil5)7426tD0 zWp;3#MuMMb^hW_@23N6LWd zYz|y^pV`>-3$K~W=kr?H_-zkce(Pv&=^7CT1VT}lP$UqE1tO79Bo>M#LWx8olSyR& zxlAsXDHJ`uy^8*U-hsjXXG2e(4gEf(92!v#jj4vmo)3?yMpdd&wdT26{X#qbLaWp0 z#?`uU?IZx1m_+_SkjW|Iq~0)PFz5|N#AwtTO$L+6IBlMunVp_}WuBdze*OB@JiszH zZ=JJP=B-wXb-}V=v)XLdg++_aZnH1i><*jVZg)EE4u`|(1UeT{n-g_7F$W4nohatS zFemB;jiMNe0o^F(!d!051#r9EKo92jVlEE|mj~!|d4WE+$K&z(Jl-Xs*SF;H0hWBe zW$*Ix^2*A}>ffttYioYLe`8}~b8~ZRYioOZduL~7cXxMhZx8yP6Seiue;45QcmF3K z+%8nT8k927sbK{4y(PXm37tqj5}n!f<~XEdHcKE2G8j#d<=}q25`l)&KS;PT@JX(g zaVR+QbBKJt$fJcivK>l|Ht=wBw!mY=Yh@{jP~WJHl}|NL~2S3}sYbUG%Rr z0uST|C_*a-W1#5d3bGeN>9#;<=0aTiIudg9bYfz8l373xfk))}ZbOIrO_%!pp_%mtjB|)le=EF=vh^YI7DlGlsOg5*m3!zk*pl=g%i5kgd!nj6_RTFjM{Zmo6I3hdN#s2vY#SlMY|GplNuhQ!4JIWph#WWzxnQ_DagdTyu{1<{B BEkXbQ literal 0 HcmV?d00001 -- GitLab