diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..d9916aad3663165c4736240353e19734c3a034f9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+target
+work
+*.iml
+*.iws
+*.ipr
diff --git a/cli/pom.xml b/cli/pom.xml
index 181de7233545960006533e671340a366e4379efb..5f72b126a47edd652bf2f66e16a2def0491143a6 100644
--- a/cli/pom.xml
+++ b/cli/pom.xml
@@ -4,7 +4,7 @@
pomorg.jvnet.hudson.main
- 1.306-SNAPSHOT
+ 1.386-SNAPSHOTcliHudson CLI
@@ -33,7 +33,7 @@
org.jvnet.localizermaven-localizer-plugin
- 1.9
+ 1.10
@@ -64,7 +64,7 @@
org.jvnet.localizerlocalizer
- 1.9
+ 1.10
diff --git a/cli/src/main/java/hudson/cli/CLI.java b/cli/src/main/java/hudson/cli/CLI.java
index 402f56b0221ef196a2178515067fdf690d844b63..fa62fa552067df9c6cca85bed98ae29ba5de250b 100644
--- a/cli/src/main/java/hudson/cli/CLI.java
+++ b/cli/src/main/java/hudson/cli/CLI.java
@@ -27,14 +27,25 @@ import hudson.remoting.Channel;
import hudson.remoting.RemoteInputStream;
import hudson.remoting.RemoteOutputStream;
import hudson.remoting.PingThread;
+import hudson.remoting.SocketInputStream;
+import hudson.remoting.SocketOutputStream;
import java.net.URL;
+import java.net.URLConnection;
+import java.net.Socket;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.ArrayList;
+import java.util.logging.Logger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.DataOutputStream;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
/**
* CLI entry point to Hudson.
@@ -42,6 +53,102 @@ import java.util.concurrent.Executors;
* @author Kohsuke Kawaguchi
*/
public class CLI {
+ private final ExecutorService pool;
+ private final Channel channel;
+ private final CliEntryPoint entryPoint;
+ private final boolean ownsPool;
+
+ public CLI(URL hudson) throws IOException, InterruptedException {
+ this(hudson,null);
+ }
+
+ public CLI(URL hudson, ExecutorService exec) throws IOException, InterruptedException {
+ String url = hudson.toExternalForm();
+ if(!url.endsWith("/")) url+='/';
+
+ ownsPool = exec==null;
+ pool = exec!=null ? exec : Executors.newCachedThreadPool();
+
+ int clip = getCliTcpPort(url);
+ if(clip>=0) {
+ // connect via CLI port
+ String host = new URL(url).getHost();
+ LOGGER.fine("Trying to connect directly via TCP/IP to port "+clip+" of "+host);
+ Socket s = new Socket(host,clip);
+ DataOutputStream dos = new DataOutputStream(s.getOutputStream());
+ dos.writeUTF("Protocol:CLI-connect");
+
+ channel = new Channel("CLI connection to "+hudson, pool,
+ new BufferedInputStream(new SocketInputStream(s)),
+ new BufferedOutputStream(new SocketOutputStream(s)));
+ } else {
+ // connect via HTTP
+ LOGGER.fine("Trying to connect to "+url+" via HTTP");
+ url+="cli";
+ hudson = new URL(url);
+
+ FullDuplexHttpStream con = new FullDuplexHttpStream(hudson);
+ channel = new Channel("Chunked connection to "+hudson,
+ pool,con.getInputStream(),con.getOutputStream());
+ new PingThread(channel,30*1000) {
+ protected void onDead() {
+ // noop. the point of ping is to keep the connection alive
+ // as most HTTP servers have a rather short read time out
+ }
+ }.start();
+ }
+
+ // execute the command
+ entryPoint = (CliEntryPoint)channel.waitForRemoteProperty(CliEntryPoint.class.getName());
+
+ if(entryPoint.protocolVersion()!=CliEntryPoint.VERSION)
+ throw new IOException(Messages.CLI_VersionMismatch());
+ }
+
+ /**
+ * If the server advertises CLI port, returns it.
+ */
+ private int getCliTcpPort(String url) throws IOException {
+ URLConnection head = new URL(url).openConnection();
+ try {
+ head.connect();
+ } catch (IOException e) {
+ throw (IOException)new IOException("Failed to connect to "+url).initCause(e);
+ }
+ String p = head.getHeaderField("X-Hudson-CLI-Port");
+ if(p==null) return -1;
+ return Integer.parseInt(p);
+ }
+
+ public void close() throws IOException, InterruptedException {
+ channel.close();
+ channel.join();
+ if(ownsPool)
+ pool.shutdown();
+ }
+
+ public int execute(List args, InputStream stdin, OutputStream stdout, OutputStream stderr) {
+ return entryPoint.main(args,Locale.getDefault(),
+ new RemoteInputStream(stdin),
+ new RemoteOutputStream(stdout),
+ new RemoteOutputStream(stderr));
+ }
+
+ public int execute(List args) {
+ return execute(args,System.in,System.out,System.err);
+ }
+
+ public int execute(String... args) {
+ return execute(Arrays.asList(args));
+ }
+
+ /**
+ * Returns true if the named command exists.
+ */
+ public boolean hasCommand(String name) {
+ return entryPoint.hasCommand(name);
+ }
+
public static void main(final String[] _args) throws Exception {
List args = Arrays.asList(_args);
@@ -57,43 +164,23 @@ public class CLI {
break;
}
- if(url==null)
+ if(url==null) {
printUsageAndExit(Messages.CLI_NoURL());
- if(!url.endsWith("/")) url+='/';
- url+="cli";
+ return;
+ }
if(args.isEmpty())
args = Arrays.asList("help"); // default to help
- FullDuplexHttpStream con = new FullDuplexHttpStream(new URL(url));
- ExecutorService pool = Executors.newCachedThreadPool();
- Channel channel = new Channel("Chunked connection to "+url,
- pool,con.getInputStream(),con.getOutputStream());
- new PingThread(channel,30*1000) {
- protected void onDead() {
- // noop. the point of ping is to keep the connection alive
- // as most HTTP servers have a rather short read time out
- }
- }.start();
-
- // execute the command
- int r=-1;
+ CLI cli = new CLI(new URL(url));
try {
- CliEntryPoint cli = (CliEntryPoint)channel.waitForRemoteProperty(CliEntryPoint.class.getName());
- if(cli.protocolVersion()!=CliEntryPoint.VERSION) {
- System.err.println(Messages.CLI_VersionMismatch());
- } else {
- // Arrays.asList is not serializable --- see 6835580
- args = new ArrayList(args);
- r = cli.main(args, Locale.getDefault(), new RemoteInputStream(System.in),
- new RemoteOutputStream(System.out), new RemoteOutputStream(System.err));
- }
+ // execute the command
+ // Arrays.asList is not serializable --- see 6835580
+ args = new ArrayList(args);
+ System.exit(cli.execute(args, System.in, System.out, System.err));
} finally {
- channel.close();
- pool.shutdown();
+ cli.close();
}
-
- System.exit(r);
}
private static void printUsageAndExit(String msg) {
@@ -101,4 +188,6 @@ public class CLI {
System.err.println(Messages.CLI_Usage());
System.exit(-1);
}
+
+ private static final Logger LOGGER = Logger.getLogger(CLI.class.getName());
}
diff --git a/cli/src/main/java/hudson/cli/CliEntryPoint.java b/cli/src/main/java/hudson/cli/CliEntryPoint.java
index 79a1c7ff488853b81812fa8fcd2e3f89c95814f2..1f5478d191495bf86e20a3bc2d32ce78fab02912 100644
--- a/cli/src/main/java/hudson/cli/CliEntryPoint.java
+++ b/cli/src/main/java/hudson/cli/CliEntryPoint.java
@@ -42,6 +42,11 @@ public interface CliEntryPoint {
*/
int main(List args, Locale locale, InputStream stdin, OutputStream stdout, OutputStream stderr);
+ /**
+ * Does the named command exist?
+ */
+ boolean hasCommand(String name);
+
/**
* Returns {@link #VERSION}, so that the client and the server can detect version incompatibility
* gracefully.
diff --git a/cli/src/main/java/hudson/cli/FullDuplexHttpStream.java b/cli/src/main/java/hudson/cli/FullDuplexHttpStream.java
index f7f73e69aea7e639a33aeb0af592a1c121cebe1d..369448ec53fd9e7ce72d415596b8c43d4cc1524f 100644
--- a/cli/src/main/java/hudson/cli/FullDuplexHttpStream.java
+++ b/cli/src/main/java/hudson/cli/FullDuplexHttpStream.java
@@ -1,11 +1,17 @@
package hudson.cli;
+import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
+import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.UUID;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import sun.misc.BASE64Encoder;
/**
* Creates a capacity-unlimited bi-directional {@link InputStream}/{@link OutputStream} pair over
@@ -15,10 +21,6 @@ import java.util.UUID;
*/
public class FullDuplexHttpStream {
private final URL target;
- /**
- * Uniquely identifies this connection, so that the server can bundle separate HTTP requests together.
- */
- private final UUID uuid = UUID.randomUUID();
private final OutputStream output;
private final InputStream input;
@@ -34,12 +36,27 @@ public class FullDuplexHttpStream {
public FullDuplexHttpStream(URL target) throws IOException {
this.target = target;
+ String authorization = null;
+ if (target.getUserInfo() != null) {
+ authorization = new BASE64Encoder().encode(target.getUserInfo().getBytes());
+ }
+
+ CrumbData crumbData = new CrumbData();
+
+ UUID uuid = UUID.randomUUID(); // so that the server can correlate those two connections
+
// server->client
HttpURLConnection con = (HttpURLConnection) target.openConnection();
con.setDoOutput(true); // request POST to avoid caching
con.setRequestMethod("POST");
- con.addRequestProperty("Session",uuid.toString());
+ con.addRequestProperty("Session", uuid.toString());
con.addRequestProperty("Side","download");
+ if (authorization != null) {
+ con.addRequestProperty("Authorization", "Basic " + authorization);
+ }
+ if(crumbData.isValid) {
+ con.addRequestProperty(crumbData.crumbName, crumbData.crumb);
+ }
con.getOutputStream().close();
input = con.getInputStream();
// make sure we hit the right URL
@@ -51,10 +68,61 @@ public class FullDuplexHttpStream {
con.setDoOutput(true); // request POST
con.setRequestMethod("POST");
con.setChunkedStreamingMode(0);
- con.addRequestProperty("Session",uuid.toString());
+ con.setRequestProperty("Content-type","application/octet-stream");
+ con.addRequestProperty("Session", uuid.toString());
con.addRequestProperty("Side","upload");
+ if (authorization != null) {
+ con.addRequestProperty ("Authorization", "Basic " + authorization);
+ }
+
+ if(crumbData.isValid) {
+ con.addRequestProperty(crumbData.crumbName, crumbData.crumb);
+ }
output = con.getOutputStream();
}
static final int BLOCK_SIZE = 1024;
+ static final Logger LOGGER = Logger.getLogger(FullDuplexHttpStream.class.getName());
+
+ private final class CrumbData {
+ String crumbName;
+ String crumb;
+ boolean isValid;
+
+ private CrumbData() {
+ this.crumbName = "";
+ this.crumb = "";
+ this.isValid = false;
+ getData();
+ }
+
+ private void getData() {
+ try {
+ String base = createCrumbUrlBase();
+ crumbName = readData(base+"?xpath=/*/crumbRequestField/text()");
+ crumb = readData(base+"?xpath=/*/crumb/text()");
+ isValid = true;
+ LOGGER.fine("Crumb data: "+crumbName+"="+crumb);
+ } catch (IOException e) {
+ // presumably this Hudson doesn't use crumb
+ LOGGER.log(Level.FINE,"Failed to get crumb data",e);
+ }
+ }
+
+ private String createCrumbUrlBase() {
+ String url = target.toExternalForm();
+ return new StringBuilder(url.substring(0, url.lastIndexOf("/cli"))).append("/crumbIssuer/api/xml/").toString();
+ }
+
+ private String readData(String dest) throws IOException {
+ HttpURLConnection con = (HttpURLConnection) new URL(dest).openConnection();
+ try {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream()));
+ return reader.readLine();
+ }
+ finally {
+ con.disconnect();
+ }
+ }
+ }
}
diff --git a/cli/src/main/resources/hudson/cli/Messages_da.properties b/cli/src/main/resources/hudson/cli/Messages_da.properties
new file mode 100644
index 0000000000000000000000000000000000000000..264c9c146a1c8aa78b987fb6463d3d808fa5374d
--- /dev/null
+++ b/cli/src/main/resources/hudson/cli/Messages_da.properties
@@ -0,0 +1,28 @@
+# The MIT License
+#
+# Copyright (c) 2004-2010, Sun Microsystems, Inc. Kohsuke Kawaguchi. Knud Poulsen.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+CLI.VersionMismatch=Versionskonflikt. CLI''en fungerer ikke med denne Hudson server
+CLI.Usage=Hudson CLI\n\
+Brug: java -jar hudson-cli.jar [-s URL] command [opts...] args...\n\
+Tilvalg:\n\
+De tilg\u00e6ngelige kommandoer afh\u00e6nger af serveren. K\u00f8r 'help' kommandoen for at se listen.
+CLI.NoURL=Hverken -s eller HUDSON_URL milj\u00f8variablen er defineret
diff --git a/cli/src/main/resources/hudson/cli/Messages_de.properties b/cli/src/main/resources/hudson/cli/Messages_de.properties
new file mode 100644
index 0000000000000000000000000000000000000000..4b290fa05f21bcb813dd59cceaec77955319f966
--- /dev/null
+++ b/cli/src/main/resources/hudson/cli/Messages_de.properties
@@ -0,0 +1,9 @@
+CLI.Usage=Hudson Kommandozeilenschnittstelle (Hudson CLI)\n\
+ Verwendung: java -jar hudson-cli.jar [-s URL] command [opts...] args...\n\
+ Optionen:\n\
+ \ -s URL : URL des Hudson-Servers (Wert der Umgebungsvariable HUDSON_URL ist der Vorgabewert)\n\
+ \n\
+ Die verfügbaren Kommandos hängen vom kontaktierten Server ab. Verwenden Sie das Kommando \
+ 'help', um eine Liste aller verfügbaren Kommandos anzuzeigen.
+CLI.NoURL=Weder die Option -s noch eine Umgebungsvariable HUDSON_URL wurde spezifiziert.
+CLI.VersionMismatch=Versionskonflikt: Diese Version von Hudson CLI ist nicht mit dem Hudson-Server kompatibel.
diff --git a/cli/src/main/resources/hudson/cli/Messages_es.properties b/cli/src/main/resources/hudson/cli/Messages_es.properties
new file mode 100644
index 0000000000000000000000000000000000000000..5da93a62e2a6bd45e06d7c9e0972af522e962f38
--- /dev/null
+++ b/cli/src/main/resources/hudson/cli/Messages_es.properties
@@ -0,0 +1,9 @@
+CLI.Usage=Hudson CLI\n\
+ Usar: java -jar hudson-cli.jar [-s URL] command [opts...] args...\n\
+ Options:\n\
+ \ -s URL : dirección web (por defecto se usa la variable HUDSON_URL)\n\
+ \n\
+ La lista completa de comandos disponibles depende del servidor. Ejecuta\n\
+ el comando ''help'' para ver la lista.
+CLI.NoURL=No se ha especificado el parámetro -s ni la variable HUDSON_URL
+CLI.VersionMismatch=La versión no coincide. Esta CLI no se puede usar en este Hudson
diff --git a/cli/src/main/resources/hudson/cli/Messages_ja.properties b/cli/src/main/resources/hudson/cli/Messages_ja.properties
new file mode 100644
index 0000000000000000000000000000000000000000..3be9f9dda28d8f00893093a973bea3c6ae88f704
--- /dev/null
+++ b/cli/src/main/resources/hudson/cli/Messages_ja.properties
@@ -0,0 +1,39 @@
+# The MIT License
+#
+# Copyright (c) 2004-2010, Sun Microsystems, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+# Neither -s nor the HUDSON_URL env var is specified.
+CLI.NoURL=-s\u3082\u74B0\u5883\u5909\u6570HUDSON_URL\u3082\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002
+# Version mismatch. This CLI cannot work with this Hudson server
+CLI.VersionMismatch=\u30D0\u30FC\u30B8\u30E7\u30F3\u30DF\u30B9\u30DE\u30C3\u30C1\u3067\u3059\u3002\u3053\u306ECLI\u306FHudson\u30B5\u30FC\u30D0\u3067\u306F\u52D5\u304D\u307E\u305B\u3093\u3002
+# Hudson CLI\n\
+# Usage: java -jar hudson-cli.jar [-s URL] command [opts...] args...\n\
+# Options:\n\
+# \ -s URL : specify the server URL (defaults to the HUDSON_URL env var)\n\
+# \n\
+# The available commands depend on the server. Run the 'help' command to\n\
+# see the list.
+CLI.Usage=Hudson CLI\n\
+ \u4F7F\u7528\u65B9\u6CD5: java -jar hudson-cli.jar [-s URL] command [opts...] args...\n\
+ \u30AA\u30D7\u30B7\u30E7\u30F3:\n\
+ \ -s URL : \u30B5\u30FC\u30D0\u306EURL\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306F\u74B0\u5883\u5909\u6570HUDSON_URL\u3067\u3059\uFF09\u3002\n\
+ \n\
+ \u5229\u7528\u53EF\u80FD\u306A\u30B3\u30DE\u30F3\u30C9\u306F\u30B5\u30FC\u30D0\u306B\u4F9D\u5B58\u3057\u307E\u3059\u3002\u305D\u306E\u30EA\u30B9\u30C8\u3092\u307F\u308B\u306B\u306F'help'\u30B3\u30DE\u30F3\u30C9\u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002
diff --git a/cli/src/main/resources/hudson/cli/Messages_pt_BR.properties b/cli/src/main/resources/hudson/cli/Messages_pt_BR.properties
new file mode 100644
index 0000000000000000000000000000000000000000..7152ef0f18a6c112cf6bcaf174183c748f704cd0
--- /dev/null
+++ b/cli/src/main/resources/hudson/cli/Messages_pt_BR.properties
@@ -0,0 +1,41 @@
+# The MIT License
+#
+# Copyright (c) 2004-2010, Sun Microsystems, Inc., Reginaldo L. Russinholi
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+# Version mismatch. This CLI cannot work with this Hudson server
+CLI.VersionMismatch=A versão não coincide. Esta CLI não pode funcionar com este servidor Hudson
+# Hudson CLI\n\
+# Usage: java -jar hudson-cli.jar [-s URL] command [opts...] args...\n\
+# Options:\n\
+# \ -s URL : specify the server URL (defaults to the HUDSON_URL env var)\n\
+# \n\
+# The available commands depend on the server. Run the 'help' command to\n\
+# see the list.
+CLI.Usage=Hudson CLI\n\
+ Uso: java -jar hudson-cli.jar [-s URL] comando [opções...] parâmetros...\n\
+ Opções:\n\
+ \ -s URL : a URL do servidor (por padrão a variável de ambiente HUDSON_URL é usada)\n\
+ \n\
+ Os comandos disponíveis dependem do servidor. Execute o comando 'help' para\n\
+ ver a lista.
+
+# Neither -s nor the HUDSON_URL env var is specified.
+CLI.NoURL=Não foi especificado nem '-s' e nem a variável de ambiente HUDSON_URL
diff --git a/core/pom.xml b/core/pom.xml
index 8ee9bcc0dc081524c7ffe74130a103b77f24f199..2e8c845ce1dbd47c706c05b14148d017a87e7d6a 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -1,7 +1,8 @@
+ 128m
+
org.jvnet.localizermaven-localizer-plugin
- 1.9
+ 1.10
@@ -83,16 +103,41 @@ THE SOFTWARE.
- maven-antlr-plugin
-
- ${basedir}/src/main/grammar
- crontab.g
-
+ org.kohsuke
+ access-modifier-checker
+ 1.0
+
+
+
+ enforce
+
+
+
+
+
+ org.codehaus.mojo
+ antlr-maven-plugin
+ 2.1
+ crongenerate
+
+ ${basedir}/src/main/grammar
+ crontab.g
+
+
+
+ labelExpr
+
+ generate
+
+
+ ${basedir}/src/main/grammar
+ labelExpr.g
+
@@ -105,9 +150,17 @@ THE SOFTWARE.
-
+
-
+
+
+
+
+
+
+
+
+
@@ -158,7 +211,7 @@ THE SOFTWARE.
org.jvnet.hudson.toolsextension-point-lister
- 1.2
+ 1.7com.sun
@@ -186,7 +239,7 @@ THE SOFTWARE.
release
- ${version}
+ ${project.version}
@@ -227,7 +280,7 @@ THE SOFTWARE.
-
+
findbugs
@@ -281,18 +334,6 @@ THE SOFTWARE.
-
- sorcerer
-
-
-
- org.jvnet.sorcerer
- maven-sorcerer-plugin
- 0.7-SNAPSHOT
-
-
-
-
@@ -306,6 +347,11 @@ THE SOFTWARE.
cli${project.version}
+
+ org.jvnet.hudson
+ crypto-util
+ 1.0
+ org.jvnet.hudsonjtidy
@@ -319,24 +365,25 @@ THE SOFTWARE.
- org.jvnet.hudson.svnkit
- svnkit
- 1.2.2-hudson-4
+ org.jruby.ext.posix
+ jna-posix
+ 1.0.3org.kohsuketrilead-putty-extension1.0
+
+ org.jvnet.hudson
+ trilead-ssh2
+ build212-hudson-5
+ org.kohsuke.staplerstapler-jelly
- 1.104
+ 1.154
-
- dom4j
- dom4j
- commons-jellycommons-jelly
@@ -347,20 +394,53 @@ THE SOFTWARE.
+
+ org.kohsuke.stapler
+ stapler-adjunct-timeline
+ 1.2
+
+
+ org.kohsuke.stapler
+ stapler-adjunct-timeline
+ 1.0
+ tests
+ test
+
+
+
+ com.infradna.tool
+ bridge-method-annotation
+ 1.2
+
+
+
+ org.kohsuke.stapler
+ json-lib
+ 2.1-rev6
+ args4jargs4j
- 2.0.13
+ 2.0.16
+
+
+ org.jvnet.hudson
+ annotation-indexer
+ 1.2
+
+
+ org.jvnet.hudson
+ task-reactor
+ 1.2org.jvnet.localizerlocalizer
- 1.9
+ 1.10org.kohsukegraph-layouter
- jdk141.0
@@ -371,22 +451,17 @@ THE SOFTWARE.
org.jvnet.hudsonxstream
- 1.3.1-hudson-2
+ 1.3.1-hudson-8jfreejfreechart1.0.9
-
- org.apache.ant
- ant-junit
- 1.7.0
- org.apache.antant
- 1.7.0
+ 1.8.0javax.servlet
@@ -397,7 +472,7 @@ THE SOFTWARE.
commons-iocommons-io
- 1.3.1
+ 1.4commons-lang
@@ -428,23 +503,18 @@ THE SOFTWARE.
javax.mailmail1.4
-
-
- javax.activation
- activation
- 1.1
-
-
- org.jvnet.hudson.dom4j
- dom4j
- 1.6.1-hudson-1
-
- xml-apis
- xml-apis
+
+ javax.activation
+ activation
+
+ org.jvnet.hudson
+ activation
+ 1.1.1-hudson-1
+ jaxenjaxen
@@ -532,11 +602,6 @@ THE SOFTWARE.
commons-jexl1.1-hudson-20090508
-
- org.jvnet.hudson
- commons-jelly
- 1.1-hudson-20090227
- org.acegisecurityacegi-security
@@ -571,10 +636,15 @@ THE SOFTWARE.
spring-core2.5
+
+ org.springframework
+ spring-aop
+ 2.5
+ xpp3xpp3
- 1.1.3.3
+ 1.1.4cjunit
@@ -604,12 +674,12 @@ THE SOFTWARE.
org.jvnet.winpwinp
- 1.10
+ 1.14org.jvnet.hudsonmemory-monitor
- 1.1
+ 1.3com.octo.captcha
@@ -659,10 +729,15 @@ THE SOFTWARE.
wstx-asl3.2.7
+
+ org.jvnet.hudson
+ jmdns
+ 3.1.6-hudson-2
+ com.sun.winswwinsw
- 1.5
+ 1.8binexeprovided
@@ -670,22 +745,22 @@ THE SOFTWARE.
net.java.dev.jnajna
- 3.0.9
+ 3.2.4com.sun.akumaakuma
- 1.1
+ 1.2org.jvnet.libpam4jlibpam4j
- 1.1
+ 1.2org.jvnet.libzfslibzfs
- 0.4
+ 0.5com.sun.solaris
@@ -695,7 +770,7 @@ THE SOFTWARE.
net.java.sezpozsezpoz
- 1.4
+ 1.7org.jvnet.hudson
@@ -704,8 +779,38 @@ THE SOFTWARE.
org.jvnet.hudson
- htmlunit
- 2.2-hudson-9
+ windows-remote-command
+ 1.0
+
+
+ org.kohsuke.metainf-services
+ metainf-services
+ 1.1
+ true
+
+
+ org.jvnet.robust-http-client
+ robust-http-client
+ 1.1
+
+
+
+ commons-codec
+ commons-codec
+ 1.4
+
+
+
+
+ asm
+ asm-commons
+ 2.2.3
+
+
+
+ org.kohsuke
+ access-modifier-annotation
+ 1.0
@@ -723,6 +828,7 @@ THE SOFTWARE.
org.kohsuke.staplermaven-stapler-plugin
+ 1.15/lib/.*
@@ -731,6 +837,7 @@ THE SOFTWARE.
maven-project-info-reports-plugin
+ 2.1false
diff --git a/core/src/build-script/Cobertura.groovy b/core/src/build-script/Cobertura.groovy
index 349566dd2fef2ef8c9ef92744dec29bdc6048114..073f6291b77c10c07ad948a47a264997e1c7069f 100644
--- a/core/src/build-script/Cobertura.groovy
+++ b/core/src/build-script/Cobertura.groovy
@@ -49,12 +49,14 @@ public class Cobertura {
junitClasspath()
}
batchtest(todir:dir("target/surefire-reports")) {
- fileset(dir:"src/test/java") {
- include(name:"**/*Test.java")
+ fileset(dir:"target/test-classes") {
+ include(name:"**/*Test.class")
}
formatter(type:"xml")
}
sysproperty(key:"net.sourceforge.cobertura.datafile",value:ser)
+ sysproperty(key:"hudson.ClassicPluginStrategy.useAntClassLoader",value:"true")
+ jvmarg(value:"-XX:MaxPermSize=128m")
}
}
diff --git a/core/src/main/grammar/labelExpr.g b/core/src/main/grammar/labelExpr.g
new file mode 100644
index 0000000000000000000000000000000000000000..bbfa77784e843b7bfb7f82b1121b3f05ac5a0e57
--- /dev/null
+++ b/core/src/main/grammar/labelExpr.g
@@ -0,0 +1,115 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2010, InfraDNA, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+header {
+ package hudson.model.labels;
+ import hudson.model.Label;
+}
+
+class LabelExpressionParser extends Parser;
+options {
+ defaultErrorHandler=false;
+}
+
+// order of precedence is as per http://en.wikipedia.org/wiki/Logical_connective#Order_of_precedence
+
+expr
+returns [Label l]
+ : l=term1 EOF
+ ;
+
+term1
+returns [Label l]
+{ Label r; }
+ : l=term2( IFF r=term2 {l=l.iff(r);} )?
+ ;
+
+term2
+returns [Label l]
+{ Label r; }
+ : l=term3( IMPLIES r=term3 {l=l.implies(r);} )?
+ ;
+
+term3
+returns [Label l]
+{ Label r; }
+ : l=term4 ( OR r=term4 {l=l.or(r);} )?
+ ;
+
+term4
+returns [Label l]
+{ Label r; }
+ : l=term5 ( AND r=term5 {l=l.and(r);} )?
+ ;
+
+term5
+returns [Label l]
+{ Label x; }
+ : l=term6
+ | NOT x=term6
+ { l=x.not(); }
+ ;
+
+term6
+returns [Label l]
+options { generateAmbigWarnings=false; }
+ : LPAREN l=term1 RPAREN
+ { l=l.paren(); }
+ | a:ATOM
+ { l=LabelAtom.get(a.getText()); }
+ | s:STRINGLITERAL
+ { l=LabelAtom.get(hudson.util.QuotedStringTokenizer.unquote(s.getText())); }
+ ;
+
+class LabelExpressionLexer extends Lexer;
+
+AND: "&&";
+OR: "||";
+NOT: "!";
+IMPLIES:"->";
+IFF: "<->";
+LPAREN: "(";
+RPAREN: ")";
+
+protected
+IDENTIFIER_PART
+ : ~( '&' | '|' | '!' | '<' | '>' | '(' | ')' | ' ' | '\t' | '\"' | '\'' )
+ ;
+
+ATOM
+/* the real check of valid identifier happens in LabelAtom.get() */
+ : (IDENTIFIER_PART)+
+ ;
+
+WS
+ : (' '|'\t')+
+ { $setType(Token.SKIP); }
+ ;
+
+STRINGLITERAL
+ : '"'
+ ( '\\' ( 'b' | 't' | 'n' | 'f' | 'r' | '\"' | '\'' | '\\' ) /* escape */
+ | ~( '\\' | '"' | '\r' | '\n' )
+ )*
+ '"'
+ ;
diff --git a/core/src/main/java/hudson/AbortException.java b/core/src/main/java/hudson/AbortException.java
index 3a1e50cd389a8b1989388cf4bbcdec444d11baf8..41a10739eb16075a27a26a6704923fa45432d131 100644
--- a/core/src/main/java/hudson/AbortException.java
+++ b/core/src/main/java/hudson/AbortException.java
@@ -27,7 +27,7 @@ import java.io.IOException;
/**
* Signals a failure where the error was anticipated and diagnosed.
- * When this exception is caughted,
+ * When this exception is caught,
* the stack trace will not be printed, and the build will be marked as a failure.
*
* @author Kohsuke Kawaguchi
diff --git a/core/src/main/java/hudson/AbstractMarkupText.java b/core/src/main/java/hudson/AbstractMarkupText.java
index 41e4304389321ade0a62bb91920bdee3f51dadca..4f1062615b0f3377cbce34268ec83a2f6e4abed4 100644
--- a/core/src/main/java/hudson/AbstractMarkupText.java
+++ b/core/src/main/java/hudson/AbstractMarkupText.java
@@ -45,10 +45,14 @@ public abstract class AbstractMarkupText {
/**
* Returns the plain text portion of this {@link MarkupText} without
- * any markup.
+ * any markup, nor any escape.
*/
public abstract String getText();
+ public char charAt(int idx) {
+ return getText().charAt(idx);
+ }
+
/**
* Length of the plain text.
*/
@@ -56,6 +60,14 @@ public abstract class AbstractMarkupText {
return getText().length();
}
+ /**
+ * Returns a subtext.
+ *
+ * @param end
+ * If negative, -N means "trim the last N-1 chars". That is, (s,-1) is the same as (s,length)
+ */
+ public abstract MarkupText.SubText subText(int start, int end);
+
/**
* Adds a start tag and end tag at the specified position.
*
@@ -65,6 +77,15 @@ public abstract class AbstractMarkupText {
*/
public abstract void addMarkup( int startPos, int endPos, String startTag, String endTag );
+ /**
+ * Inserts an A tag that surrounds the given position.
+ *
+ * @since 1.349
+ */
+ public void addHyperlink( int startPos, int endPos, String url ) {
+ addMarkup(startPos,endPos,"","");
+ }
+
/**
* Adds a start tag and end tag around the entire text
*/
@@ -72,6 +93,21 @@ public abstract class AbstractMarkupText {
addMarkup(0,length(),startTag,endTag);
}
+ /**
+ * Find the first occurrence of the given pattern in this text, or null.
+ *
+ * @since 1.349
+ */
+ public MarkupText.SubText findToken(Pattern pattern) {
+ String text = getText();
+ Matcher m = pattern.matcher(text);
+
+ if(m.find())
+ return createSubText(m);
+
+ return null;
+ }
+
/**
* Find all "tokens" that match the given pattern in this text.
*
diff --git a/core/src/main/java/hudson/BulkChange.java b/core/src/main/java/hudson/BulkChange.java
index 9c9047a9de5d2f66843228603a6bf176ba9040c1..9625452c95119991019f79ea2f3112cf84e996ed 100644
--- a/core/src/main/java/hudson/BulkChange.java
+++ b/core/src/main/java/hudson/BulkChange.java
@@ -157,8 +157,18 @@ public class BulkChange {
*/
public static boolean contains(Saveable s) {
for(BulkChange b=current(); b!=null; b=b.parent)
- if(b.saveable== s)
+ if(b.saveable==s || b.saveable==ALL)
return true;
return false;
}
+
+ /**
+ * Magic {@link Saveable} instance that can make {@link BulkChange} veto
+ * all the save operations by making the {@link #contains(Saveable)} method return
+ * true for everything.
+ */
+ public static final Saveable ALL = new Saveable() {
+ public void save() {
+ }
+ };
}
diff --git a/core/src/main/java/hudson/ClassicPluginStrategy.java b/core/src/main/java/hudson/ClassicPluginStrategy.java
index a6aee7a3eb5e4603fa024e8a309a3ea02ede4ffe..57d7cff079fde195fd694a02e56b2a8dcbcae828 100644
--- a/core/src/main/java/hudson/ClassicPluginStrategy.java
+++ b/core/src/main/java/hudson/ClassicPluginStrategy.java
@@ -25,7 +25,9 @@ package hudson;
import hudson.PluginWrapper.Dependency;
import hudson.util.IOException2;
-import hudson.model.Hudson;
+import hudson.util.MaskingClassLoader;
+import hudson.util.VersionNumber;
+import hudson.Plugin.DummyImpl;
import java.io.BufferedReader;
import java.io.File;
@@ -33,21 +35,29 @@ import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
+import java.io.Closeable;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
import java.util.List;
import java.util.jar.Manifest;
+import java.util.jar.Attributes;
import java.util.logging.Logger;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
+import org.apache.tools.ant.AntClassLoader;
import org.apache.tools.ant.taskdefs.Expand;
import org.apache.tools.ant.types.FileSet;
public class ClassicPluginStrategy implements PluginStrategy {
-
- private static final Logger LOGGER = Logger.getLogger(ClassicPluginStrategy.class.getName());
+
+ private static final Logger LOGGER = Logger.getLogger(ClassicPluginStrategy.class.getName());
/**
* Filter for jar files.
@@ -59,133 +69,194 @@ public class ClassicPluginStrategy implements PluginStrategy {
};
private PluginManager pluginManager;
-
- public ClassicPluginStrategy(PluginManager pluginManager) {
- this.pluginManager = pluginManager;
- }
-
- public PluginWrapper createPluginWrapper(File archive) throws IOException {
- LOGGER.info("Loading plugin: " + archive);
-
- Manifest manifest;
- URL baseResourceURL;
-
- boolean isLinked = archive.getName().endsWith(".hpl");
-
- File expandDir = null;
- // if .hpi, this is the directory where war is expanded
-
- if (isLinked) {
- // resolve the .hpl file to the location of the manifest file
- String firstLine = new BufferedReader(new FileReader(archive))
- .readLine();
- if (firstLine.startsWith("Manifest-Version:")) {
- // this is the manifest already
- } else {
- // indirection
- archive = resolve(archive, firstLine);
- }
- // then parse manifest
- FileInputStream in = new FileInputStream(archive);
- try {
- manifest = new Manifest(in);
- } catch (IOException e) {
- throw new IOException2("Failed to load " + archive, e);
- } finally {
- in.close();
- }
- } else {
- expandDir = new File(archive.getParentFile(), PluginWrapper.getBaseName(archive));
- explode(archive, expandDir);
-
- File manifestFile = new File(expandDir, "META-INF/MANIFEST.MF");
- if (!manifestFile.exists()) {
- throw new IOException(
- "Plugin installation failed. No manifest at "
- + manifestFile);
- }
- FileInputStream fin = new FileInputStream(manifestFile);
- try {
- manifest = new Manifest(fin);
- } finally {
- fin.close();
- }
- }
-
- // TODO: define a mechanism to hide classes
- // String export = manifest.getMainAttributes().getValue("Export");
-
- List paths = new ArrayList();
- if (isLinked) {
- parseClassPath(manifest, archive, paths, "Libraries", ",");
- parseClassPath(manifest, archive, paths, "Class-Path", " +"); // backward
- // compatibility
-
- baseResourceURL = resolve(archive,
- manifest.getMainAttributes().getValue("Resource-Path"))
- .toURL();
- } else {
- File classes = new File(expandDir, "WEB-INF/classes");
- if (classes.exists())
- paths.add(classes.toURL());
- File lib = new File(expandDir, "WEB-INF/lib");
- File[] libs = lib.listFiles(JAR_FILTER);
- if (libs != null) {
- for (File jar : libs)
- paths.add(jar.toURL());
- }
-
- baseResourceURL = expandDir.toURL();
- }
- File disableFile = new File(archive.getPath() + ".disabled");
- if (disableFile.exists()) {
- LOGGER.info("Plugin is disabled");
- }
-
- // compute dependencies
- List dependencies = new ArrayList();
- List optionalDependencies = new ArrayList();
- String v = manifest.getMainAttributes().getValue("Plugin-Dependencies");
- if (v != null) {
- for (String s : v.split(",")) {
- PluginWrapper.Dependency d = new PluginWrapper.Dependency(s);
- if (d.optional) {
- optionalDependencies.add(d);
- } else {
- dependencies.add(d);
- }
- }
- }
-
- // native m2 support moved to a plugin starting 1.296, so plugins built before that
- // needs to have an implicit dependency to the maven-plugin, or NoClassDefError will ensue.
- String hudsonVersion = manifest.getMainAttributes().getValue("Hudson-Version");
- String shortName = manifest.getMainAttributes().getValue("Short-Name");
- if (!"maven-plugin".equals(shortName) &&
- // some earlier versions of maven-hpi-plugin apparently puts "null" as a literal here. Watch out for those.
- (hudsonVersion == null || hudsonVersion.equals("null") || hudsonVersion.compareTo("1.296") <= 0)) {
- optionalDependencies.add(new PluginWrapper.Dependency("maven-plugin:" + Hudson.VERSION));
+
+ public ClassicPluginStrategy(PluginManager pluginManager) {
+ this.pluginManager = pluginManager;
+ }
+
+ public PluginWrapper createPluginWrapper(File archive) throws IOException {
+ final Manifest manifest;
+ URL baseResourceURL;
+
+ File expandDir = null;
+ // if .hpi, this is the directory where war is expanded
+
+ boolean isLinked = archive.getName().endsWith(".hpl");
+ if (isLinked) {
+ // resolve the .hpl file to the location of the manifest file
+ String firstLine = new BufferedReader(new FileReader(archive))
+ .readLine();
+ if (firstLine.startsWith("Manifest-Version:")) {
+ // this is the manifest already
+ } else {
+ // indirection
+ archive = resolve(archive, firstLine);
+ }
+ // then parse manifest
+ FileInputStream in = new FileInputStream(archive);
+ try {
+ manifest = new Manifest(in);
+ } catch (IOException e) {
+ throw new IOException2("Failed to load " + archive, e);
+ } finally {
+ in.close();
+ }
+ } else {
+ if (archive.isDirectory()) {// already expanded
+ expandDir = archive;
+ } else {
+ expandDir = new File(archive.getParentFile(), PluginWrapper.getBaseName(archive));
+ explode(archive, expandDir);
+ }
+
+ File manifestFile = new File(expandDir, "META-INF/MANIFEST.MF");
+ if (!manifestFile.exists()) {
+ throw new IOException(
+ "Plugin installation failed. No manifest at "
+ + manifestFile);
+ }
+ FileInputStream fin = new FileInputStream(manifestFile);
+ try {
+ manifest = new Manifest(fin);
+ } finally {
+ fin.close();
+ }
+ }
+
+ final Attributes atts = manifest.getMainAttributes();
+
+ // TODO: define a mechanism to hide classes
+ // String export = manifest.getMainAttributes().getValue("Export");
+
+ List paths = new ArrayList();
+ if (isLinked) {
+ parseClassPath(manifest, archive, paths, "Libraries", ",");
+ parseClassPath(manifest, archive, paths, "Class-Path", " +"); // backward compatibility
+
+ baseResourceURL = resolve(archive,atts.getValue("Resource-Path")).toURI().toURL();
+ } else {
+ File classes = new File(expandDir, "WEB-INF/classes");
+ if (classes.exists())
+ paths.add(classes);
+ File lib = new File(expandDir, "WEB-INF/lib");
+ File[] libs = lib.listFiles(JAR_FILTER);
+ if (libs != null)
+ paths.addAll(Arrays.asList(libs));
+
+ baseResourceURL = expandDir.toURI().toURL();
+ }
+ File disableFile = new File(archive.getPath() + ".disabled");
+ if (disableFile.exists()) {
+ LOGGER.info("Plugin " + archive.getName() + " is disabled");
+ }
+
+ // compute dependencies
+ List dependencies = new ArrayList();
+ List optionalDependencies = new ArrayList();
+ String v = atts.getValue("Plugin-Dependencies");
+ if (v != null) {
+ for (String s : v.split(",")) {
+ PluginWrapper.Dependency d = new PluginWrapper.Dependency(s);
+ if (d.optional) {
+ optionalDependencies.add(d);
+ } else {
+ dependencies.add(d);
+ }
+ }
+ }
+ for (DetachedPlugin detached : DETACHED_LIST)
+ detached.fix(atts,optionalDependencies);
+
+ ClassLoader dependencyLoader = new DependencyClassLoader(getBaseClassLoader(atts), archive, Util.join(dependencies,optionalDependencies));
+
+ return new PluginWrapper(pluginManager, archive, manifest, baseResourceURL,
+ createClassLoader(paths, dependencyLoader, atts), disableFile, dependencies, optionalDependencies);
+ }
+
+ @Deprecated
+ protected ClassLoader createClassLoader(List paths, ClassLoader parent) throws IOException {
+ return createClassLoader( paths, parent, null );
+ }
+
+ /**
+ * Creates the classloader that can load all the specified jar files and delegate to the given parent.
+ */
+ protected ClassLoader createClassLoader(List paths, ClassLoader parent, Attributes atts) throws IOException {
+ if (atts != null) {
+ String usePluginFirstClassLoader = atts.getValue( "PluginFirstClassLoader" );
+ if (Boolean.valueOf( usePluginFirstClassLoader )) {
+ PluginFirstClassLoader classLoader = new PluginFirstClassLoader();
+ classLoader.setParentFirst( false );
+ classLoader.setParent( parent );
+ classLoader.addPathFiles( paths );
+ return classLoader;
+ }
+ }
+ if(useAntClassLoader) {
+ // using AntClassLoader with Closeable so that we can predictably release jar files opened by URLClassLoader
+ AntClassLoader2 classLoader = new AntClassLoader2(parent);
+ classLoader.addPathFiles(paths);
+ return classLoader;
+ } else {
+ // Tom reported that AntClassLoader has a performance issue when Hudson keeps trying to load a class that doesn't exist,
+ // so providing a legacy URLClassLoader support, too
+ List urls = new ArrayList();
+ for (File path : paths)
+ urls.add(path.toURI().toURL());
+ return new URLClassLoader(urls.toArray(new URL[urls.size()]),parent);
+ }
+ }
+
+ /**
+ * Information about plugins that were originally in the core.
+ */
+ private static final class DetachedPlugin {
+ private final String shortName;
+ private final VersionNumber splitWhen;
+ private final String requireVersion;
+
+ private DetachedPlugin(String shortName, String splitWhen, String requireVersion) {
+ this.shortName = shortName;
+ this.splitWhen = new VersionNumber(splitWhen);
+ this.requireVersion = requireVersion;
}
- ClassLoader dependencyLoader = new DependencyClassLoader(getClass()
- .getClassLoader(), Util.join(dependencies,optionalDependencies));
- ClassLoader classLoader = new URLClassLoader(paths.toArray(new URL[paths.size()]),
- dependencyLoader);
+ private void fix(Attributes atts, List optionalDependencies) {
+ // don't fix the dependency for yourself, or else we'll have a cycle
+ String yourName = atts.getValue("Short-Name");
+ if (shortName.equals(yourName)) return;
- return new PluginWrapper(archive, manifest, baseResourceURL,
- classLoader, disableFile, dependencies, optionalDependencies);
- }
+ // some earlier versions of maven-hpi-plugin apparently puts "null" as a literal in Hudson-Version. watch out for them.
+ String hudsonVersion = atts.getValue("Hudson-Version");
+ if (hudsonVersion == null || hudsonVersion.equals("null") || new VersionNumber(hudsonVersion).compareTo(splitWhen) <= 0)
+ optionalDependencies.add(new PluginWrapper.Dependency(shortName+':'+requireVersion));
+ }
+ }
- public void initializeComponents(PluginWrapper plugin) {
- }
+ private static final List DETACHED_LIST = Arrays.asList(
+ new DetachedPlugin("maven-plugin","1.296","1.296"),
+ new DetachedPlugin("subversion","1.310","1.0"),
+ new DetachedPlugin("cvs","1.340","0.1")
+ );
- public void load(PluginWrapper wrapper) throws IOException {
- loadPluginDependencies(wrapper.getDependencies(),
- wrapper.getOptionalDependencies());
+ /**
+ * Computes the classloader that takes the class masking into account.
+ *
+ *
+ * This mechanism allows plugins to have their own verions for libraries that core bundles.
+ */
+ private ClassLoader getBaseClassLoader(Attributes atts) {
+ ClassLoader base = getClass().getClassLoader();
+ String masked = atts.getValue("Mask-Classes");
+ if(masked!=null)
+ base = new MaskingClassLoader(base, masked.trim().split("[ \t\r\n]+"));
+ return base;
+ }
- if (!wrapper.isActive())
- return;
+ public void initializeComponents(PluginWrapper plugin) {
+ }
+ public void load(PluginWrapper wrapper) throws IOException {
// override the context classloader so that XStream activity in plugin.start()
// will be able to resolve classes in this plugin
ClassLoader old = Thread.currentThread().getContextClassLoader();
@@ -194,7 +265,7 @@ public class ClassicPluginStrategy implements PluginStrategy {
String className = wrapper.getPluginClass();
if(className==null) {
// use the default dummy instance
- wrapper.setPlugin(Plugin.NONE);
+ wrapper.setPlugin(new DummyImpl());
} else {
try {
Class clazz = wrapper.classLoader.loadClass(className);
@@ -216,7 +287,7 @@ public class ClassicPluginStrategy implements PluginStrategy {
// initialize plugin
try {
- Plugin plugin = wrapper.getPlugin();
+ Plugin plugin = wrapper.getPlugin();
plugin.setServletContext(pluginManager.context);
startPlugin(wrapper);
} catch(Throwable t) {
@@ -226,11 +297,11 @@ public class ClassicPluginStrategy implements PluginStrategy {
} finally {
Thread.currentThread().setContextClassLoader(old);
}
- }
-
- public void startPlugin(PluginWrapper plugin) throws Exception {
- plugin.getPlugin().start();
- }
+ }
+
+ public void startPlugin(PluginWrapper plugin) throws Exception {
+ plugin.getPlugin().start();
+ }
private static File resolve(File base, String relative) {
File rel = new File(relative);
@@ -240,7 +311,7 @@ public class ClassicPluginStrategy implements PluginStrategy {
return new File(base.getParentFile(),relative);
}
- private static void parseClassPath(Manifest manifest, File archive, List paths, String attributeName, String separator) throws IOException {
+ private static void parseClassPath(Manifest manifest, File archive, List paths, String attributeName, String separator) throws IOException {
String classPath = manifest.getMainAttributes().getValue(attributeName);
if(classPath==null) return; // attribute not found
for (String s : classPath.split(separator)) {
@@ -252,12 +323,12 @@ public class ClassicPluginStrategy implements PluginStrategy {
fs.setDir(dir);
fs.setIncludes(file.getName());
for( String included : fs.getDirectoryScanner(new Project()).getIncludedFiles() ) {
- paths.add(new File(dir,included).toURL());
+ paths.add(new File(dir,included));
}
} else {
if(!file.exists())
throw new IOException("No such file: "+file);
- paths.add(file.toURL());
+ paths.add(file);
}
}
}
@@ -271,11 +342,9 @@ public class ClassicPluginStrategy implements PluginStrategy {
// timestamp check
File explodeTime = new File(destDir,".timestamp");
- if(explodeTime.exists() && explodeTime.lastModified()>archive.lastModified())
+ if(explodeTime.exists() && explodeTime.lastModified()==archive.lastModified())
return; // no need to expand
- LOGGER.info("Extracting "+archive);
-
// delete the contents so that old files won't interfere with new files
Util.deleteContentsRecursive(destDir);
@@ -287,55 +356,34 @@ public class ClassicPluginStrategy implements PluginStrategy {
e.setDest(destDir);
e.execute();
} catch (BuildException x) {
- IOException ioe = new IOException("Failed to expand " + archive);
- ioe.initCause(x);
- throw ioe;
+ throw new IOException2("Failed to expand " + archive,x);
}
- Util.touch(explodeTime);
+ try {
+ new FilePath(explodeTime).touch(archive.lastModified());
+ } catch (InterruptedException e) {
+ throw new AssertionError(e); // impossible
+ }
}
- /**
- * Loads the dependencies to other plugins.
- *
- * @throws IOException
- * thrown if one or several mandatory dependencies doesnt
- * exists.
- */
- private void loadPluginDependencies(List dependencies,
- List optionalDependencies) throws IOException {
- List missingDependencies = new ArrayList();
- // make sure dependencies exist
- for (Dependency d : dependencies) {
- if (pluginManager.getPlugin(d.shortName) == null)
- missingDependencies.add(d.toString());
- }
- if (!missingDependencies.isEmpty()) {
- StringBuilder builder = new StringBuilder();
- builder.append("Dependency ");
- builder.append(Util.join(missingDependencies, ", "));
- builder.append(" doesn't exist");
- throw new IOException(builder.toString());
- }
-
- // add the optional dependencies that exists
- for (Dependency d : optionalDependencies) {
- if (pluginManager.getPlugin(d.shortName) != null)
- dependencies.add(d);
- }
- }
-
/**
* Used to load classes from dependency plugins.
*/
final class DependencyClassLoader extends ClassLoader {
- private List dependencies;
+ /**
+ * This classloader is created for this plugin. Useful during debugging.
+ */
+ private final File _for;
+
+ private List dependencies;
- public DependencyClassLoader(ClassLoader parent, List dependencies) {
+ public DependencyClassLoader(ClassLoader parent, File archive, List dependencies) {
super(parent);
+ this._for = archive;
this.dependencies = dependencies;
}
+ @Override
protected Class> findClass(String name) throws ClassNotFoundException {
for (Dependency dep : dependencies) {
PluginWrapper p = pluginManager.getPlugin(dep.shortName);
@@ -350,6 +398,53 @@ public class ClassicPluginStrategy implements PluginStrategy {
throw new ClassNotFoundException(name);
}
- // TODO: delegate resources? watch out for diamond dependencies
+ @Override
+ protected Enumeration findResources(String name) throws IOException {
+ HashSet result = new HashSet();
+ for (Dependency dep : dependencies) {
+ PluginWrapper p = pluginManager.getPlugin(dep.shortName);
+ if (p!=null) {
+ Enumeration urls = p.classLoader.getResources(name);
+ while (urls != null && urls.hasMoreElements())
+ result.add(urls.nextElement());
+ }
+ }
+
+ return Collections.enumeration(result);
+ }
+
+ @Override
+ protected URL findResource(String name) {
+ for (Dependency dep : dependencies) {
+ PluginWrapper p = pluginManager.getPlugin(dep.shortName);
+ if(p!=null) {
+ URL url = p.classLoader.getResource(name);
+ if (url!=null)
+ return url;
+ }
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * {@link AntClassLoader} with a few methods exposed and {@link Closeable} support.
+ */
+ private static final class AntClassLoader2 extends AntClassLoader implements Closeable {
+ private AntClassLoader2(ClassLoader parent) {
+ super(parent,true);
+ }
+
+ public void addPathFiles(Collection paths) throws IOException {
+ for (File f : paths)
+ addPathFile(f);
+ }
+
+ public void close() throws IOException {
+ cleanup();
+ }
}
+
+ public static boolean useAntClassLoader = Boolean.getBoolean(ClassicPluginStrategy.class.getName()+".useAntClassLoader");
}
diff --git a/core/src/main/java/hudson/CloseProofOutputStream.java b/core/src/main/java/hudson/CloseProofOutputStream.java
index 5af94299f6816959fc4c2c020c77f4e09ffdb226..4256d4ac75bcd70e07ba94a47b602a544f1883d9 100644
--- a/core/src/main/java/hudson/CloseProofOutputStream.java
+++ b/core/src/main/java/hudson/CloseProofOutputStream.java
@@ -36,6 +36,7 @@ public class CloseProofOutputStream extends DelegatingOutputStream {
super(out);
}
+ @Override
public void close() {
}
}
diff --git a/core/src/main/java/hudson/DNSMultiCast.java b/core/src/main/java/hudson/DNSMultiCast.java
new file mode 100644
index 0000000000000000000000000000000000000000..bcc924f7207b6a47e09f138b0b3e7bf5cb52b87e
--- /dev/null
+++ b/core/src/main/java/hudson/DNSMultiCast.java
@@ -0,0 +1,59 @@
+package hudson;
+
+import hudson.model.Hudson;
+
+import javax.jmdns.JmDNS;
+import javax.jmdns.ServiceInfo;
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Registers a DNS multi-cast service-discovery support.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public class DNSMultiCast implements Closeable {
+ private JmDNS jmdns;
+
+ public DNSMultiCast(Hudson hudson) {
+ if (disabled) return; // escape hatch
+
+ try {
+ this.jmdns = JmDNS.create();
+
+ Map props = new HashMap();
+ String rootURL = hudson.getRootUrl();
+ if (rootURL!=null)
+ props.put("url", rootURL);
+ try {
+ props.put("version",String.valueOf(Hudson.getVersion()));
+ } catch (IllegalArgumentException e) {
+ // failed to parse the version number
+ }
+
+ TcpSlaveAgentListener tal = hudson.getTcpSlaveAgentListener();
+ if (tal!=null)
+ props.put("slave-port",String.valueOf(tal.getPort()));
+
+ jmdns.registerService(ServiceInfo.create("_hudson._tcp.local.","hudson",
+ 80,0,0,props));
+ } catch (IOException e) {
+ LOGGER.log(Level.WARNING,"Failed to advertise the service to DNS multi-cast",e);
+ }
+ }
+
+ public void close() {
+ if (jmdns!=null) {
+ jmdns.close();
+ jmdns = null;
+ }
+ }
+
+ private static final Logger LOGGER = Logger.getLogger(DNSMultiCast.class.getName());
+
+ public static boolean disabled = Boolean.getBoolean(DNSMultiCast.class.getName()+".disabled");
+}
diff --git a/core/src/main/java/hudson/DependencyRunner.java b/core/src/main/java/hudson/DependencyRunner.java
index 49b8693551dfd284e2adc71ecdd715a3800ec1dc..3af89079cdd9fab24eeef689cfb95a0bd8952316 100644
--- a/core/src/main/java/hudson/DependencyRunner.java
+++ b/core/src/main/java/hudson/DependencyRunner.java
@@ -1,7 +1,8 @@
/*
* The MIT License
*
- * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Brian Westrich, Jean-Baptiste Quenot
+ * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
+ * Brian Westrich, Jean-Baptiste Quenot
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -25,6 +26,7 @@ package hudson;
import hudson.model.AbstractProject;
import hudson.model.Hudson;
+import hudson.security.ACL;
import java.util.ArrayList;
import java.util.HashSet;
@@ -32,6 +34,8 @@ import java.util.List;
import java.util.Set;
import java.util.Collection;
import java.util.logging.Logger;
+import org.acegisecurity.Authentication;
+import org.acegisecurity.context.SecurityContextHolder;
/**
* Runs a job on all projects in the order of dependencies
@@ -49,22 +53,27 @@ public class DependencyRunner implements Runnable {
}
public void run() {
- Set topLevelProjects = new HashSet();
- // Get all top-level projects
- LOGGER.fine("assembling top level projects");
- for (AbstractProject p : Hudson.getInstance().getAllItems(
- AbstractProject.class))
- if (p.getUpstreamProjects().size() == 0) {
- LOGGER.fine("adding top level project " + p.getName());
- topLevelProjects.add(p);
- } else {
- LOGGER.fine("skipping project since not a top level project: "
- + p.getName());
+ Authentication saveAuth = SecurityContextHolder.getContext().getAuthentication();
+ SecurityContextHolder.getContext().setAuthentication(ACL.SYSTEM);
+
+ try {
+ Set topLevelProjects = new HashSet();
+ // Get all top-level projects
+ LOGGER.fine("assembling top level projects");
+ for (AbstractProject p : Hudson.getInstance().getAllItems(AbstractProject.class))
+ if (p.getUpstreamProjects().size() == 0) {
+ LOGGER.fine("adding top level project " + p.getName());
+ topLevelProjects.add(p);
+ } else {
+ LOGGER.fine("skipping project since not a top level project: " + p.getName());
+ }
+ populate(topLevelProjects);
+ for (AbstractProject p : polledProjects) {
+ LOGGER.fine("running project in correct dependency order: " + p.getName());
+ runnable.run(p);
}
- populate(topLevelProjects);
- for (AbstractProject p : polledProjects) {
- LOGGER.fine("running project in correct dependency order: " + p.getName());
- runnable.run(p);
+ } finally {
+ SecurityContextHolder.getContext().setAuthentication(saveAuth);
}
}
@@ -77,7 +86,7 @@ public class DependencyRunner implements Runnable {
polledProjects.remove(p);
}
- LOGGER.fine("adding project " + p.getName());
+ LOGGER.fine("adding project " + p.getName());
polledProjects.add(p);
// Add all downstream dependencies
diff --git a/core/src/main/java/hudson/DescriptorExtensionList.java b/core/src/main/java/hudson/DescriptorExtensionList.java
index d4bc87db21d53f1b2b2df747860cb996dbcec257..535a58a814d0d026286e66f319674ca76ce01687 100644
--- a/core/src/main/java/hudson/DescriptorExtensionList.java
+++ b/core/src/main/java/hudson/DescriptorExtensionList.java
@@ -28,8 +28,8 @@ import hudson.model.Describable;
import hudson.model.Hudson;
import hudson.model.ViewDescriptor;
import hudson.model.Descriptor.FormException;
+import hudson.util.AdaptedIterator;
import hudson.util.Memoizer;
-import hudson.util.Iterators;
import hudson.util.Iterators.FlattenIterator;
import hudson.slaves.NodeDescriptor;
import hudson.tasks.Publisher;
@@ -67,10 +67,12 @@ public class DescriptorExtensionList, D extends Descrip
/**
* Creates a new instance.
*/
+ @SuppressWarnings({"unchecked", "rawtypes"})
public static ,D extends Descriptor>
- DescriptorExtensionList create(Hudson hudson, Class describableType) {
- if(describableType==(Class)Publisher.class) // javac or IntelliJ compiler complains if I don't have this cast
- return (DescriptorExtensionList)new DescriptorExtensionListImpl(hudson);
+ DescriptorExtensionList createDescriptorList(Hudson hudson, Class describableType) {
+ if (describableType == (Class) Publisher.class) {
+ return (DescriptorExtensionList) new DescriptorExtensionListImpl(hudson);
+ }
return new DescriptorExtensionList(hudson,describableType);
}
@@ -80,7 +82,7 @@ public class DescriptorExtensionList, D extends Descrip
private final Class describableType;
protected DescriptorExtensionList(Hudson hudson, Class describableType) {
- super(hudson, (Class)Descriptor.class, legacyDescriptors.get(describableType));
+ super(hudson, (Class)Descriptor.class, (CopyOnWriteArrayList)getLegacyDescriptors(describableType));
this.describableType = describableType;
}
@@ -94,6 +96,17 @@ public class DescriptorExtensionList, D extends Descrip
return Descriptor.find(this,fqcn);
}
+ /**
+ * Finds the descriptor that describes the given type.
+ * That is, if this method returns d, {@code d.clazz==type}
+ */
+ public D find(Class extends T> type) {
+ for (D d : this)
+ if (d.clazz==type)
+ return d;
+ return null;
+ }
+
/**
* Creates a new instance of a {@link Describable}
* from the structured form submission data posted
@@ -121,21 +134,35 @@ public class DescriptorExtensionList, D extends Descrip
return d;
return null;
}
-
+
+ /**
+ * {@link #load()} in the descriptor is not a real load activity, so locking against "this" is enough.
+ */
+ @Override
+ protected Object getLoadLock() {
+ return this;
+ }
+
+ @Override
+ protected void scoutLoad() {
+ // no-op, since our load() doesn't by itself do any classloading
+ }
+
/**
* Loading the descriptors in this case means filtering the descriptor from the master {@link ExtensionList}.
*/
@Override
- protected List load() {
- List r = new ArrayList();
- for( Descriptor d : hudson.getExtensionList(Descriptor.class) ) {
+ protected List> load() {
+ List> r = new ArrayList>();
+ for( ExtensionComponent c : hudson.getExtensionList(Descriptor.class).getComponents() ) {
+ Descriptor d = c.getInstance();
Type subTyping = Types.getBaseClass(d.getClass(), Descriptor.class);
if (!(subTyping instanceof ParameterizedType)) {
LOGGER.severe(d.getClass()+" doesn't extend Descriptor with a type parameter");
continue; // skip this one
}
if(Types.erasure(Types.getTypeArgument(subTyping,0))==(Class)describableType)
- r.add(d);
+ r.add((ExtensionComponent)c);
}
return r;
}
@@ -143,21 +170,31 @@ public class DescriptorExtensionList, D extends Descrip
/**
* Stores manually registered Descriptor instances. Keyed by the {@link Describable} type.
*/
- private static final Memoizer legacyDescriptors = new Memoizer() {
+ private static final Memoizer>> legacyDescriptors = new Memoizer>>() {
public CopyOnWriteArrayList compute(Class key) {
return new CopyOnWriteArrayList();
}
};
+ private static > CopyOnWriteArrayList>> getLegacyDescriptors(Class type) {
+ return (CopyOnWriteArrayList)legacyDescriptors.get(type);
+ }
+
/**
* List up all the legacy instances currently in use.
*/
public static Iterable listLegacyInstances() {
return new Iterable() {
public Iterator iterator() {
- return new FlattenIterator(legacyDescriptors.values()) {
- protected Iterator expand(CopyOnWriteArrayList v) {
- return v.iterator();
+ return new AdaptedIterator,Descriptor>(
+ new FlattenIterator,CopyOnWriteArrayList>>(legacyDescriptors.values()) {
+ protected Iterator> expand(CopyOnWriteArrayList> v) {
+ return v.iterator();
+ }
+ }) {
+
+ protected Descriptor adapt(ExtensionComponent item) {
+ return item.getInstance();
}
};
}
diff --git a/core/src/main/java/hudson/EnvVars.java b/core/src/main/java/hudson/EnvVars.java
index 9cf6dccaa937b2b44b35e87ca48d50e8ed6cbe76..0070da51e1f12fdb9c6c8b74902b06589c4df3c4 100644
--- a/core/src/main/java/hudson/EnvVars.java
+++ b/core/src/main/java/hudson/EnvVars.java
@@ -32,18 +32,33 @@ import java.io.IOException;
import java.util.Map;
import java.util.TreeMap;
import java.util.Arrays;
+import java.util.UUID;
/**
* Environment variables.
*
*
+ * While all the platforms I tested (Linux 2.6, Solaris, and Windows XP) have the case sensitive
+ * environment variable table, Windows batch script handles environment variable in the case preserving
+ * but case insensitive way (that is, cmd.exe can get both FOO and foo as environment variables
+ * when it's launched, and the "set" command will display it accordingly, but "echo %foo%" results in
+ * echoing the value of "FOO", not "foo" — this is presumably caused by the behavior of the underlying
+ * Win32 API GetEnvironmentVariable acting in case insensitive way.) Windows users are also
+ * used to write environment variable case-insensitively (like %Path% vs %PATH%), and you can see many
+ * documents on the web that claims Windows environment variables are case insensitive.
+ *
+ *
+ * So for a consistent cross platform behavior, it creates the least confusion to make the table
+ * case insensitive but case preserving.
+ *
+ *
* In Hudson, often we need to build up "environment variable overrides"
* on master, then to execute the process on slaves. This causes a problem
* when working with variables like PATH. So to make this work,
* we introduce a special convention PATH+FOO — all entries
* that starts with PATH+ are merged and prepended to the inherited
* PATH variable, on the process where a new process is executed.
- *
+ *
* @author Kohsuke Kawaguchi
*/
public class EnvVars extends TreeMap {
@@ -79,6 +94,9 @@ public class EnvVars extends TreeMap {
this((Map)m);
}
+ /**
+ * Builds an environment variables from an array of the form "key","value","key","value"...
+ */
public EnvVars(String... keyValuePairs) {
this();
if(keyValuePairs.length%2!=0)
@@ -138,6 +156,12 @@ public class EnvVars extends TreeMap {
entry.setValue(Util.replaceMacro(entry.getValue(), env));
}
}
+
+ @Override
+ public String put(String key, String value) {
+ if (value==null) throw new IllegalArgumentException("Null value not allowed as an environment variable: "+key);
+ return super.put(key,value);
+ }
/**
* Takes a string that looks like "a=b" and adds that to this map.
@@ -156,6 +180,14 @@ public class EnvVars extends TreeMap {
return Util.replaceMacro(s, this);
}
+ /**
+ * Creates a magic cookie that can be used as the model environment variable
+ * when we later kill the processes.
+ */
+ public static EnvVars createCookie() {
+ return new EnvVars("HUDSON_COOKIE", UUID.randomUUID().toString());
+ }
+
/**
* Obtains the environment variables of a remote peer.
*
@@ -194,7 +226,7 @@ public class EnvVars extends TreeMap {
private static EnvVars initMaster() {
EnvVars vars = new EnvVars(System.getenv());
vars.platform = Platform.current();
- if(Functions.isUnitTest)
+ if(Main.isUnitTest || Main.isDevelopmentMode)
// if unit test is launched with maven debug switch,
// we need to prevent forked Maven processes from seeing it, or else
// they'll hang
diff --git a/core/src/main/java/hudson/ExpressionFactory2.java b/core/src/main/java/hudson/ExpressionFactory2.java
index 93c4ed3fe644e623d2d7519386a1d30adac6e0c2..be6df295b4e533450e200ba3e9e9b10812a8557f 100644
--- a/core/src/main/java/hudson/ExpressionFactory2.java
+++ b/core/src/main/java/hudson/ExpressionFactory2.java
@@ -54,6 +54,7 @@ final class ExpressionFactory2 implements ExpressionFactory {
this.expression = expression;
}
+ @Override
public String toString() {
return super.toString() + "[" + expression.getExpression() + "]";
}
diff --git a/core/src/main/java/hudson/Extension.java b/core/src/main/java/hudson/Extension.java
index c54e3c04101d4b28f99e592d4feae40a654b6c21..66342a2b6161eb5ac7adfe30a6edd384337e4602 100644
--- a/core/src/main/java/hudson/Extension.java
+++ b/core/src/main/java/hudson/Extension.java
@@ -1,7 +1,7 @@
/*
* The MIT License
*
- * Copyright (c) 2004-2009, Sun Microsystems, Inc.
+ * Copyright (c) 2004-2010, Sun Microsystems, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -26,12 +26,11 @@ package hudson;
import net.java.sezpoz.Indexable;
import java.lang.annotation.Documented;
-import static java.lang.annotation.ElementType.*;
import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
-import hudson.ExtensionFinder.Sezpoz;
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Marks a field, a method, or a class for automatic discovery, so that Hudson can locate
@@ -66,8 +65,23 @@ import hudson.ExtensionFinder.Sezpoz;
* @see ExtensionList
*/
@Indexable
-@Retention(RetentionPolicy.SOURCE)
+@Retention(RUNTIME)
@Target({TYPE, FIELD, METHOD})
@Documented
public @interface Extension {
+ /**
+ * Used for sorting extensions.
+ *
+ * Extensions will be sorted in the descending order of the ordinal.
+ * This is a rather poor approach to the problem, so its use is generally discouraged.
+ *
+ * @since 1.306
+ */
+ double ordinal() default 0;
+
+ /**
+ * If an extension is optional, don't log any class loading errors when reading it.
+ * @since 1.358
+ */
+ boolean optional() default false;
}
diff --git a/core/src/main/java/hudson/ExtensionComponent.java b/core/src/main/java/hudson/ExtensionComponent.java
new file mode 100644
index 0000000000000000000000000000000000000000..2f7eea3e13ae0b15d32a0e0bba019564b6a0f219
--- /dev/null
+++ b/core/src/main/java/hudson/ExtensionComponent.java
@@ -0,0 +1,77 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2010, Kohsuke Kawaguchi
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package hudson;
+
+/**
+ * Discovered {@link Extension} object with a bit of metadata for Hudson.
+ * This is a plain value object.
+ *
+ * @author Kohsuke Kawaguchi
+ * @since 1.356
+ */
+public class ExtensionComponent implements Comparable> {
+ private final T instance;
+ private final double ordinal;
+
+ public ExtensionComponent(T instance, double ordinal) {
+ this.instance = instance;
+ this.ordinal = ordinal;
+ }
+
+ public ExtensionComponent(T instance, Extension annotation) {
+ this(instance,annotation.ordinal());
+ }
+
+ public ExtensionComponent(T instance) {
+ this(instance,0);
+ }
+
+ /**
+ * See {@link Extension#ordinal()}. Used to sort extensions.
+ */
+ public double ordinal() {
+ return ordinal;
+ }
+
+ /**
+ * The instance of the discovered extension.
+ *
+ * @return never null.
+ */
+ public T getInstance() {
+ return instance;
+ }
+
+ /**
+ * Sort {@link ExtensionComponent}s in the descending order of {@link #ordinal()}.
+ */
+ public int compareTo(ExtensionComponent that) {
+ double a = this.ordinal();
+ double b = that.ordinal();
+ if (a>b) return -1;
+ if (a Collection findExtensions(Class type, Hudson hudson) {
+ return Collections.emptyList();
+ }
+
/**
* Discover extensions of the given type.
*
@@ -67,8 +79,51 @@ public abstract class ExtensionFinder implements ExtensionPoint {
* Hudson whose behalf this extension finder is performing lookup.
* @return
* Can be empty but never null.
+ * @since 1.356
+ * Older implementations provide {@link #findExtensions(Class, Hudson)}
*/
- public abstract Collection findExtensions(Class type, Hudson hudson);
+ public abstract Collection> find(Class type, Hudson hudson);
+
+ /**
+ * A pointless function to work around what appears to be a HotSpot problem. See HUDSON-5756 and bug 6933067
+ * on BugParade for more details.
+ */
+ public Collection> _find(Class type, Hudson hudson) {
+ return find(type,hudson);
+ }
+
+ /**
+ * Performs class initializations without creating instances.
+ *
+ * If two threads try to initialize classes in the opposite order, a dead lock will ensue,
+ * and we can get into a similar situation with {@link ExtensionFinder}s.
+ *
+ *
+ * That is, one thread can try to list extensions, which results in {@link ExtensionFinder}
+ * loading and initializing classes. This happens inside a context of a lock, so that
+ * another thread that tries to list the same extensions don't end up creating different
+ * extension instances. So this activity locks extension list first, then class initialization next.
+ *
+ *
+ * In the mean time, another thread can load and initialize a class, and that initialization
+ * can eventually results in listing up extensions, for example through static initializer.
+ * Such activitiy locks class initialization first, then locks extension list.
+ *
+ *
+ * This inconsistent locking order results in a dead lock, you see.
+ *
+ *
+ * So to reduce the likelihood, this method is called in prior to {@link #find(Class, Hudson)} invocation,
+ * but from outside the lock. The implementation is expected to perform all the class initialization activities
+ * from here.
+ *
+ *
+ * See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6459208 for how to force a class initialization.
+ * Also see http://kohsuke.org/2010/09/01/deadlock-that-you-cant-avoid/ for how class initialization
+ * can results in a dead lock.
+ */
+ public void scout(Class extensionType, Hudson hudson) {
+ }
/**
* The default implementation that looks for the {@link Extension} marker.
@@ -78,8 +133,8 @@ public abstract class ExtensionFinder implements ExtensionPoint {
*/
@Extension
public static final class Sezpoz extends ExtensionFinder {
- public Collection findExtensions(Class type, Hudson hudson) {
- List result = new ArrayList();
+ public Collection> find(Class type, Hudson hudson) {
+ List> result = new ArrayList>();
ClassLoader cl = hudson.getPluginManager().uberClassLoader;
for (IndexItem item : Index.load(Extension.class, Object.class, cl)) {
@@ -100,15 +155,52 @@ public abstract class ExtensionFinder implements ExtensionPoint {
if(type.isAssignableFrom(extType)) {
Object instance = item.instance();
if(instance!=null)
- result.add(type.cast(instance));
+ result.add(new ExtensionComponent(type.cast(instance),item.annotation()));
}
+ } catch (LinkageError e) {
+ // sometimes the instantiation fails in an indirect classloading failure,
+ // which results in a LinkageError
+ LOGGER.log(item.annotation().optional() ? Level.FINE : Level.WARNING,
+ "Failed to load "+item.className(), e);
} catch (InstantiationException e) {
- LOGGER.log(Level.WARNING, "Failed to load "+item.className(),e);
+ LOGGER.log(item.annotation().optional() ? Level.FINE : Level.WARNING,
+ "Failed to load "+item.className(), e);
}
}
return result;
}
+
+ @Override
+ public void scout(Class extensionType, Hudson hudson) {
+ ClassLoader cl = hudson.getPluginManager().uberClassLoader;
+ for (IndexItem item : Index.load(Extension.class, Object.class, cl)) {
+ try {
+ AnnotatedElement e = item.element();
+ Class> extType;
+ if (e instanceof Class) {
+ extType = (Class) e;
+ } else
+ if (e instanceof Field) {
+ extType = ((Field)e).getType();
+ } else
+ if (e instanceof Method) {
+ extType = ((Method)e).getReturnType();
+ } else
+ throw new AssertionError();
+ // accroding to http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6459208
+ // this appears to be the only way to force a class initialization
+ Class.forName(extType.getName(),true,extType.getClassLoader());
+ } catch (InstantiationException e) {
+ LOGGER.log(item.annotation().optional() ? Level.FINE : Level.WARNING,
+ "Failed to scout "+item.className(), e);
+ } catch (ClassNotFoundException e) {
+ LOGGER.log(Level.WARNING,"Failed to scout "+item.className(), e);
+ } catch (LinkageError e) {
+ LOGGER.log(Level.WARNING,"Failed to scout "+item.className(), e);
+ }
+ }
+ }
}
private static final Logger LOGGER = Logger.getLogger(ExtensionFinder.class.getName());
diff --git a/core/src/main/java/hudson/ExtensionList.java b/core/src/main/java/hudson/ExtensionList.java
index 65355cf7f749ae6fbfc38e9466edee93d33f0ced..208431b164236c5fcb014da935db6362ef7fa8f9 100644
--- a/core/src/main/java/hudson/ExtensionList.java
+++ b/core/src/main/java/hudson/ExtensionList.java
@@ -23,7 +23,9 @@
*/
package hudson;
+import hudson.init.InitMilestone;
import hudson.model.Hudson;
+import hudson.util.AdaptedIterator;
import hudson.util.DescriptorList;
import hudson.util.Memoizer;
import hudson.util.Iterators;
@@ -31,11 +33,14 @@ import hudson.ExtensionPoint.LegacyInstancesAreScopedToHudson;
import java.util.AbstractList;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.logging.Level;
+import java.util.logging.Logger;
/**
* Retains the known extension instances for the given type 'T'.
@@ -66,16 +71,16 @@ public class ExtensionList extends AbstractList {
* Once discovered, extensions are retained here.
*/
@CopyOnWrite
- private volatile List extensions;
+ private volatile List> extensions;
/**
* Place to store manually registered instances with the per-Hudson scope.
* {@link CopyOnWriteArrayList} is used here to support concurrent iterations and mutation.
*/
- private final CopyOnWriteArrayList legacyInstances;
+ private final CopyOnWriteArrayList> legacyInstances;
protected ExtensionList(Hudson hudson, Class extensionType) {
- this(hudson,extensionType,new CopyOnWriteArrayList());
+ this(hudson,extensionType,new CopyOnWriteArrayList>());
}
/**
@@ -83,9 +88,9 @@ public class ExtensionList extends AbstractList {
* @param legacyStore
* Place to store manually registered instances. The version of the constructor that
* omits this uses a new {@link Vector}, making the storage lifespan tied to the life of {@link ExtensionList}.
- * If the manually registerd instances are scoped to VM level, the caller should pass in a static list.
+ * If the manually registered instances are scoped to VM level, the caller should pass in a static list.
*/
- protected ExtensionList(Hudson hudson, Class extensionType, CopyOnWriteArrayList legacyStore) {
+ protected ExtensionList(Hudson hudson, Class extensionType, CopyOnWriteArrayList> legacyStore) {
this.hudson = hudson;
this.extensionType = extensionType;
this.legacyInstances = legacyStore;
@@ -102,13 +107,25 @@ public class ExtensionList extends AbstractList {
return null;
}
+ @Override
public Iterator iterator() {
// we need to intercept mutation, so for now don't allow Iterator.remove
- return Iterators.readOnly(ensureLoaded().iterator());
+ return new AdaptedIterator,T>(Iterators.readOnly(ensureLoaded().iterator())) {
+ protected T adapt(ExtensionComponent item) {
+ return item.getInstance();
+ }
+ };
+ }
+
+ /**
+ * Gets the same thing as the 'this' list represents, except as {@link ExtensionComponent}s.
+ */
+ public List> getComponents() {
+ return Collections.unmodifiableList(ensureLoaded());
}
public T get(int index) {
- return ensureLoaded().get(index);
+ return ensureLoaded().get(index).getInstance();
}
public int size() {
@@ -117,15 +134,25 @@ public class ExtensionList extends AbstractList {
@Override
public synchronized boolean remove(Object o) {
- legacyInstances.remove(o);
+ removeComponent(legacyInstances,o);
if(extensions!=null) {
- List r = new ArrayList(extensions);
- r.remove(o);
+ List> r = new ArrayList>(extensions);
+ removeComponent(r,o);
extensions = sort(r);
}
return true;
}
+ private void removeComponent(Collection> collection, Object t) {
+ for (Iterator> itr = collection.iterator(); itr.hasNext();) {
+ ExtensionComponent c = itr.next();
+ if (c.getInstance().equals(t)) {
+ collection.remove(c);
+ return;
+ }
+ }
+ }
+
@Override
public synchronized T remove(int index) {
T t = get(index);
@@ -136,16 +163,16 @@ public class ExtensionList extends AbstractList {
/**
* Write access will put the instance into a legacy store.
*
- * @deprecated
+ * @deprecated since 2009-02-23.
* Prefer automatic registration.
*/
@Override
public synchronized boolean add(T t) {
- legacyInstances.add(t);
+ legacyInstances.add(new ExtensionComponent(t));
// if we've already filled extensions, add it
if(extensions!=null) {
- List r = new ArrayList(extensions);
- r.add(t);
+ List> r = new ArrayList>(extensions);
+ r.add(new ExtensionComponent(t));
extensions = sort(r);
}
return true;
@@ -156,6 +183,17 @@ public class ExtensionList extends AbstractList {
add(element);
}
+ /**
+ * Used to bind extension to URLs by their class names.
+ *
+ * @since 1.349
+ */
+ public T getDynamic(String className) {
+ for (T t : this)
+ if (t.getClass().getName().equals(className))
+ return t;
+ return null;
+ }
/**
@@ -165,15 +203,17 @@ public class ExtensionList extends AbstractList {
return hudson.getExtensionList(ExtensionFinder.class);
}
- private List ensureLoaded() {
+ private List> ensureLoaded() {
if(extensions!=null)
return extensions; // already loaded
- if(Hudson.getInstance().getPluginManager()==null)
- return legacyInstances; // can't perform the auto discovery until all plugins are loaded, so just make the legacy instances visisble
+ if(Hudson.getInstance().getInitLevel().compareTo(InitMilestone.PLUGINS_PREPARED)<0)
+ return legacyInstances; // can't perform the auto discovery until all plugins are loaded, so just make the legacy instances visible
+
+ scoutLoad();
- synchronized (this) {
+ synchronized (getLoadLock()) {
if(extensions==null) {
- List r = load();
+ List> r = load();
r.addAll(legacyInstances);
extensions = sort(r);
}
@@ -181,13 +221,48 @@ public class ExtensionList extends AbstractList {
}
}
+ /**
+ * Chooses the object that locks the loading of the extension instances.
+ */
+ protected Object getLoadLock() {
+ return hudson.lookup.setIfNull(Lock.class,new Lock());
+ }
+
+ /**
+ * Loading an {@link ExtensionList} can result in a nested loading of another {@link ExtensionList}.
+ * What that means is that we need a single lock that spans across all the {@link ExtensionList}s,
+ * or else we can end up in a dead lock.
+ */
+ private static final class Lock {}
+
+ /**
+ * See {@link ExtensionFinder#scout(Class, Hudson)} for the dead lock issue and what this does.
+ */
+ protected void scoutLoad() {
+ if (LOGGER.isLoggable(Level.FINER))
+ LOGGER.log(Level.FINER,"Scout-loading ExtensionList: "+extensionType, new Throwable());
+ for (ExtensionFinder finder : finders()) {
+ finder.scout(extensionType, hudson);
+ }
+ }
+
/**
* Loads all the extensions.
*/
- protected List load() {
- List r = new ArrayList();
- for (ExtensionFinder finder : finders())
- r.addAll(finder.findExtensions(extensionType, hudson));
+ protected List> load() {
+ if (LOGGER.isLoggable(Level.FINE))
+ LOGGER.log(Level.FINE,"Loading ExtensionList: "+extensionType, new Throwable());
+
+ List> r = new ArrayList>();
+ for (ExtensionFinder finder : finders()) {
+ try {
+ r.addAll(finder._find(extensionType, hudson));
+ } catch (AbstractMethodError e) {
+ // backward compatibility
+ for (T t : finder.findExtensions(extensionType, hudson))
+ r.add(new ExtensionComponent(t));
+ }
+ }
return r;
}
@@ -198,7 +273,9 @@ public class ExtensionList extends AbstractList {
*
* The implementation should copy a list, do a sort, and return the new instance.
*/
- protected List sort(List r) {
+ protected List> sort(List> r) {
+ r = new ArrayList>(r);
+ Collections.sort(r);
return r;
}
@@ -206,7 +283,7 @@ public class ExtensionList extends AbstractList {
if(type==ExtensionFinder.class)
return new ExtensionList(hudson,type) {
/**
- * If this ExtensionList is searching for ExtensionFinders, calling hudosn.getExtensionList
+ * If this ExtensionList is searching for ExtensionFinders, calling hudson.getExtensionList
* results in infinite recursion.
*/
@Override
@@ -236,4 +313,6 @@ public class ExtensionList extends AbstractList {
public static void clearLegacyInstances() {
staticLegacyInstances.clear();
}
+
+ private static final Logger LOGGER = Logger.getLogger(ExtensionList.class.getName());
}
diff --git a/core/src/main/java/hudson/ExtensionListView.java b/core/src/main/java/hudson/ExtensionListView.java
index 56917dccba4d8b969d9670f9d3be822bcf406850..070aebd237f7e620ab17a85a8b90fd643e8e32db 100644
--- a/core/src/main/java/hudson/ExtensionListView.java
+++ b/core/src/main/java/hudson/ExtensionListView.java
@@ -61,6 +61,7 @@ public class ExtensionListView {
return Hudson.getInstance().getExtensionList(type);
}
+ @Override
public Iterator iterator() {
return storage().iterator();
}
@@ -73,10 +74,12 @@ public class ExtensionListView {
return storage().size();
}
+ @Override
public boolean add(T t) {
return storage().add(t);
}
+ @Override
public void add(int index, T t) {
// index ignored
storage().add(t);
diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java
index f6621065dd50e7dc63fe3a4ef6b77f207299bbe0..c64e61f858e6daa5a635049ebb3713b591bcafa6 100644
--- a/core/src/main/java/hudson/FilePath.java
+++ b/core/src/main/java/hudson/FilePath.java
@@ -1,7 +1,8 @@
/*
* The MIT License
*
- * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Eric Lefevre-Ardant, Erik Ramfelt, Michael B. Donohue
+ * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
+ * Eric Lefevre-Ardant, Erik Ramfelt, Michael B. Donohue, Alan Harder
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -37,25 +38,28 @@ import hudson.remoting.Pipe;
import hudson.remoting.RemoteOutputStream;
import hudson.remoting.VirtualChannel;
import hudson.remoting.RemoteInputStream;
+import hudson.util.DirScanner;
import hudson.util.IOException2;
import hudson.util.HeadBufferingStream;
import hudson.util.FormValidation;
+import hudson.util.IOUtils;
import static hudson.util.jna.GNUCLibrary.LIBC;
import static hudson.Util.fixEmpty;
import static hudson.FilePath.TarCompression.GZIP;
+import hudson.os.PosixAPI;
+import hudson.org.apache.tools.tar.TarInputStream;
+import hudson.util.io.Archiver;
+import hudson.util.io.ArchiverFactory;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.tar.TarEntry;
-import org.apache.tools.tar.TarOutputStream;
-import org.apache.tools.tar.TarInputStream;
-import org.apache.tools.zip.ZipOutputStream;
-import org.apache.tools.zip.ZipEntry;
-import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.input.CountingInputStream;
import org.apache.commons.fileupload.FileItem;
import org.kohsuke.stapler.Stapler;
+import org.jvnet.robust_http_client.RetryableHttpStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@@ -80,6 +84,7 @@ import java.util.List;
import java.util.StringTokenizer;
import java.util.Arrays;
import java.util.Comparator;
+import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@@ -107,7 +112,7 @@ import com.sun.jna.Native;
*
* 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.
+ * layer.
*
*
* But one should note that such use of remoting may not be optional. Sometimes,
@@ -179,7 +184,7 @@ public final class FilePath implements Serializable {
*/
public FilePath(VirtualChannel channel, String remote) {
this.channel = channel;
- this.remote = remote;
+ this.remote = normalize(remote);
}
/**
@@ -191,7 +196,7 @@ public final class FilePath implements Serializable {
*/
public FilePath(File localPath) {
this.channel = null;
- this.remote = localPath.getPath();
+ this.remote = normalize(localPath.getPath());
}
/**
@@ -203,12 +208,12 @@ public final class FilePath implements Serializable {
this.channel = base.channel;
if(isAbsolute(rel)) {
// absolute
- this.remote = rel;
+ this.remote = normalize(rel);
} else
if(base.isUnix()) {
- this.remote = base.remote+'/'+rel;
+ this.remote = normalize(base.remote+'/'+rel);
} else {
- this.remote = base.remote+'\\'+rel;
+ this.remote = normalize(base.remote+'\\'+rel);
}
}
@@ -216,7 +221,67 @@ public final class FilePath implements Serializable {
return rel.startsWith("/") || DRIVE_PATTERN.matcher(rel).matches();
}
- private static final Pattern DRIVE_PATTERN = Pattern.compile("[A-Za-z]:\\\\.+");
+ private static final Pattern DRIVE_PATTERN = Pattern.compile("[A-Za-z]:[\\\\/].*"),
+ ABSOLUTE_PREFIX_PATTERN = Pattern.compile("^(\\\\\\\\|(?:[A-Za-z]:)?[\\\\/])[\\\\/]*");
+
+ /**
+ * {@link File#getParent()} etc cannot handle ".." and "." in the path component very well,
+ * so remove them.
+ */
+ private static String normalize(String path) {
+ StringBuilder buf = new StringBuilder();
+ // Check for prefix designating absolute path
+ Matcher m = ABSOLUTE_PREFIX_PATTERN.matcher(path);
+ if (m.find()) {
+ buf.append(m.group(1));
+ path = path.substring(m.end());
+ }
+ boolean isAbsolute = buf.length() > 0;
+ // Split remaining path into tokens, trimming any duplicate or trailing separators
+ List tokens = new ArrayList();
+ int s = 0, end = path.length();
+ for (int i = 0; i < end; i++) {
+ char c = path.charAt(i);
+ if (c == '/' || c == '\\') {
+ tokens.add(path.substring(s, i));
+ s = i;
+ // Skip any extra separator chars
+ while (++i < end && ((c = path.charAt(i)) == '/' || c == '\\')) { }
+ // Add token for separator unless we reached the end
+ if (i < end) tokens.add(path.substring(s, s+1));
+ s = i;
+ }
+ }
+ if (s < end) tokens.add(path.substring(s));
+ // Look through tokens for "." or ".."
+ for (int i = 0; i < tokens.size();) {
+ String token = tokens.get(i);
+ if (token.equals(".")) {
+ tokens.remove(i);
+ if (tokens.size() > 0)
+ tokens.remove(i > 0 ? i - 1 : i);
+ } else if (token.equals("..")) {
+ if (i == 0) {
+ // If absolute path, just remove: /../something
+ // If relative path, not collapsible so leave as-is
+ tokens.remove(0);
+ if (tokens.size() > 0) token += tokens.remove(0);
+ if (!isAbsolute) buf.append(token);
+ } else {
+ // Normalize: remove something/.. plus separator before/after
+ i -= 2;
+ for (int j = 0; j < 3; j++) tokens.remove(i);
+ if (i > 0) tokens.remove(i-1);
+ else if (tokens.size() > 0) tokens.remove(0);
+ }
+ } else
+ i += 2;
+ }
+ // Recombine tokens
+ for (String token : tokens) buf.append(token);
+ if (buf.length() == 0) buf.append('.');
+ return buf.toString();
+ }
/**
* Checks if the remote path is Unix.
@@ -243,48 +308,31 @@ public final class FilePath implements Serializable {
/**
* Creates a zip file from this directory or a file and sends that to the given output stream.
+ *
+ * @deprecated as of 1.315. Use {@link #zip(OutputStream)} that has more consistent name.
*/
public void createZipArchive(OutputStream os) throws IOException, InterruptedException {
- final OutputStream out = (channel!=null)?new RemoteOutputStream(os):os;
- act(new FileCallable() {
- private transient byte[] buf;
- public Void invoke(File f, VirtualChannel channel) throws IOException {
- buf = new byte[8192];
+ zip(os);
+ }
- ZipOutputStream zip = new ZipOutputStream(out);
- zip.setEncoding(System.getProperty("file.encoding"));
- scan(f,zip,"");
- zip.close();
- return null;
- }
+ /**
+ * Creates a zip file from this directory or a file and sends that to the given output stream.
+ */
+ public void zip(OutputStream os) throws IOException, InterruptedException {
+ zip(os,(FileFilter)null);
+ }
- private void scan(File f, ZipOutputStream zip, String path) throws IOException {
- // Bitmask indicating directories in 'external attributes' of a ZIP archive entry.
- final long BITMASK_IS_DIRECTORY = 1<<4;
-
- if (f.canRead()) {
- if(f.isDirectory()) {
- ZipEntry dirZipEntry = new ZipEntry(path+f.getName()+'/');
- // Setting this bit explicitly is needed by some unzipping applications (see HUDSON-3294).
- dirZipEntry.setExternalAttributes(BITMASK_IS_DIRECTORY);
- zip.putNextEntry(dirZipEntry);
- zip.closeEntry();
- for( File child : f.listFiles() )
- scan(child,zip,path+f.getName()+'/');
- } else {
- zip.putNextEntry(new ZipEntry(path+f.getName()));
- FileInputStream in = new FileInputStream(f);
- int len;
- while((len=in.read(buf))>0)
- zip.write(buf,0,len);
- in.close();
- zip.closeEntry();
- }
- }
- }
-
- private static final long serialVersionUID = 1L;
- });
+ /**
+ * Creates a zip file from this directory by using the specified filter,
+ * and sends the result to the given output stream.
+ *
+ * @param filter
+ * Must be serializable since it may be executed remotely. Can be null to add all files.
+ *
+ * @since 1.315
+ */
+ public void zip(OutputStream os, FileFilter filter) throws IOException, InterruptedException {
+ archive(ArchiverFactory.ZIP,os,filter);
}
/**
@@ -295,41 +343,66 @@ public final class FilePath implements Serializable {
* works like {@link #createZipArchive(OutputStream)}
*
* @since 1.129
+ * @deprecated as of 1.315
+ * Use {@link #zip(OutputStream,String)} that has more consistent name.
*/
public void createZipArchive(OutputStream os, final String glob) throws IOException, InterruptedException {
- if(glob==null || glob.length()==0) {
- createZipArchive(os);
- return;
- }
-
+ archive(ArchiverFactory.ZIP,os,glob);
+ }
+
+ /**
+ * Creates a zip file from this directory by only including the files that match the given glob.
+ *
+ * @param glob
+ * Ant style glob, like "**/*.xml". If empty or null, this method
+ * works like {@link #createZipArchive(OutputStream)}
+ *
+ * @since 1.315
+ */
+ public void zip(OutputStream os, final String glob) throws IOException, InterruptedException {
+ archive(ArchiverFactory.ZIP,os,glob);
+ }
+
+ /**
+ * Uses the given scanner on 'this' directory to list up files and then archive it to a zip stream.
+ */
+ public int zip(OutputStream out, DirScanner scanner) throws IOException, InterruptedException {
+ return archive(ArchiverFactory.ZIP, out, scanner);
+ }
+
+ /**
+ * Archives this directory into the specified archive format, to the given {@link OutputStream}, by using
+ * {@link DirScanner} to choose what files to include.
+ *
+ * @return
+ * number of files/directories archived. This is only really useful to check for a situation where nothing
+ * is archived.
+ */
+ public int archive(final ArchiverFactory factory, OutputStream os, final DirScanner scanner) throws IOException, InterruptedException {
final OutputStream out = (channel!=null)?new RemoteOutputStream(os):os;
- act(new FileCallable() {
- public Void invoke(File dir, VirtualChannel channel) throws IOException {
- byte[] buf = new byte[8192];
-
- ZipOutputStream zip = new ZipOutputStream(out);
- zip.setEncoding(System.getProperty("file.encoding"));
- for( String entry : glob(dir,glob) ) {
- File file = new File(dir,entry);
- if (file.canRead()) {
- zip.putNextEntry(new ZipEntry(dir.getName()+'/'+entry));
- FileInputStream in = new FileInputStream(file);
- int len;
- while((len=in.read(buf))>0)
- zip.write(buf,0,len);
- in.close();
- zip.closeEntry();
- }
+ return act(new FileCallable() {
+ public Integer invoke(File f, VirtualChannel channel) throws IOException {
+ Archiver a = factory.create(out);
+ try {
+ scanner.scan(f,a);
+ } finally {
+ a.close();
}
-
- zip.close();
- return null;
+ return a.countEntries();
}
private static final long serialVersionUID = 1L;
});
}
+ private int archive(final ArchiverFactory factory, OutputStream os, final FileFilter filter) throws IOException, InterruptedException {
+ return archive(factory,os,new DirScanner.Filter(filter));
+ }
+
+ private int archive(final ArchiverFactory factory, OutputStream os, final String glob) throws IOException, InterruptedException {
+ return archive(factory,os,new DirScanner.Glob(glob,null));
+ }
+
/**
* When this {@link FilePath} represents a zip file, extracts that zip file.
*
@@ -400,12 +473,7 @@ public final class FilePath implements Serializable {
} else {
File p = f.getParentFile();
if(p!=null) p.mkdirs();
- FileOutputStream out = new FileOutputStream(f);
- try {
- IOUtils.copy(zip, out);
- } finally {
- out.close();
- }
+ IOUtils.copy(zip, f);
f.setLastModified(e.getTime());
zip.closeEntry();
}
@@ -426,6 +494,23 @@ public final class FilePath implements Serializable {
}));
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ FilePath that = (FilePath) o;
+
+ if (channel != null ? !channel.equals(that.channel) : that.channel != null) return false;
+ return remote.equals(that.remote);
+
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * (channel != null ? channel.hashCode() : 0) + remote.hashCode();
+ }
+
/**
* Supported tar file compression methods.
*/
@@ -509,38 +594,53 @@ public final class FilePath implements Serializable {
* @since 1.299
*/
public boolean installIfNecessaryFrom(URL archive, TaskListener listener, String message) throws IOException, InterruptedException {
- URLConnection con;
try {
- con = archive.openConnection();
- con.connect();
- } catch (IOException x) {
- if (this.exists()) {
- // Cannot connect now, so assume whatever was last unpacked is still OK.
- if (listener != null) {
- listener.getLogger().println("Skipping installation of " + archive + " to " + remote + ": " + x);
+ URLConnection con;
+ try {
+ con = ProxyConfiguration.open(archive);
+ con.connect();
+ } catch (IOException x) {
+ if (this.exists()) {
+ // Cannot connect now, so assume whatever was last unpacked is still OK.
+ if (listener != null) {
+ listener.getLogger().println("Skipping installation of " + archive + " to " + remote + ": " + x);
+ }
+ return false;
+ } else {
+ throw x;
}
- return false;
+ }
+ long sourceTimestamp = con.getLastModified();
+ FilePath timestamp = this.child(".timestamp");
+
+ if(this.exists()) {
+ if(timestamp.exists() && sourceTimestamp ==timestamp.lastModified())
+ return false; // already up to date
+ this.deleteContents();
} else {
- throw x;
+ this.mkdirs();
}
- }
- long sourceTimestamp = con.getLastModified();
- FilePath timestamp = this.child(".timestamp");
- if(this.exists()) {
- if(timestamp.exists() && sourceTimestamp ==timestamp.lastModified())
- return false; // already up to date
- this.deleteContents();
- }
+ if(listener!=null)
+ listener.getLogger().println(message);
- if(listener!=null)
- listener.getLogger().println(message);
- if(archive.toExternalForm().endsWith(".zip"))
- unzipFrom(con.getInputStream());
- else
- untarFrom(con.getInputStream(),GZIP);
- timestamp.touch(sourceTimestamp);
- return true;
+ // for HTTP downloads, enable automatic retry for added resilience
+ InputStream in = archive.getProtocol().equals("http") ? new RetryableHttpStream(archive) : con.getInputStream();
+ CountingInputStream cis = new CountingInputStream(in);
+ try {
+ if(archive.toExternalForm().endsWith(".zip"))
+ unzipFrom(cis);
+ else
+ untarFrom(cis,GZIP);
+ } catch (IOException e) {
+ throw new IOException2(String.format("Failed to unpack %s (%d bytes read of total %d)",
+ archive,cis.getByteCount(),con.getContentLength()),e);
+ }
+ timestamp.touch(sourceTimestamp);
+ return true;
+ } catch (IOException e) {
+ throw new IOException2("Failed to install "+archive+" to "+remote,e);
+ }
}
/**
@@ -572,6 +672,15 @@ public final class FilePath implements Serializable {
}
}
+ /**
+ * Conveniene method to call {@link FilePath#copyTo(FilePath)}.
+ *
+ * @since 1.311
+ */
+ public void copyFrom(FilePath src) throws IOException, InterruptedException {
+ src.copyTo(this);
+ }
+
/**
* Place the data from {@link FileItem} into the file location specified by this {@link FilePath} object.
*/
@@ -606,13 +715,16 @@ public final class FilePath implements Serializable {
/**
* Performs the computational task on the node where the data is located.
*
+ *
+ * All the exceptions are forwarded to the caller.
+ *
* @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;
+ T invoke(File f, VirtualChannel channel) throws IOException, InterruptedException;
}
/**
@@ -620,15 +732,21 @@ public final class FilePath implements Serializable {
* so that one can perform local file operations.
*/
public T act(final FileCallable callable) throws IOException, InterruptedException {
+ return act(callable,callable.getClass().getClassLoader());
+ }
+
+ private T act(final FileCallable callable, ClassLoader cl) throws IOException, InterruptedException {
if(channel!=null) {
// run this on a remote system
try {
- return channel.call(new FileCallableWrapper(callable));
+ return channel.call(new FileCallableWrapper(callable,cl));
+ } catch (TunneledInterruptedException e) {
+ throw (InterruptedException)new InterruptedException().initCause(e);
} catch (AbortException e) {
throw e; // pass through so that the caller can catch it as AbortException
} 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);
+ throw new IOException2("remote file operation failed: "+remote+" at "+channel,e);
}
} else {
// the file is on the local machine.
@@ -681,16 +799,12 @@ public final class FilePath implements Serializable {
*/
public void mkdirs() throws IOException, InterruptedException {
if(!act(new FileCallable() {
- public Boolean invoke(File f, VirtualChannel channel) throws IOException {
+ public Boolean invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
if(f.mkdirs() || f.exists())
return true; // OK
// following Ant task to avoid possible race condition.
- try {
- Thread.sleep(10);
- } catch (InterruptedException e) {
- // ignore
- }
+ Thread.sleep(10);
return f.mkdirs() || f.exists();
}
@@ -722,6 +836,17 @@ public final class FilePath implements Serializable {
});
}
+ /**
+ * Gets the file name portion except the extension.
+ *
+ * For example, "foo" for "foo.txt" and "foo.tar" for "foo.tar.gz".
+ */
+ public String getBaseName() {
+ String n = getName();
+ int idx = n.lastIndexOf('.');
+ if (idx<0) return n;
+ return n.substring(0,idx);
+ }
/**
* Gets just the file name portion.
*
@@ -743,6 +868,20 @@ public final class FilePath implements Serializable {
return r.substring(len+1);
}
+ /**
+ * Short for {@code getParent().child(rel)}. Useful for getting other files in the same directory.
+ */
+ public FilePath sibling(String rel) {
+ return getParent().child(rel);
+ }
+
+ /**
+ * Returns a {@link FilePath} by adding the given suffix to this path name.
+ */
+ public FilePath withSuffix(String suffix) {
+ return new FilePath(channel,remote+suffix);
+ }
+
/**
* The same as {@link FilePath#FilePath(FilePath,String)} but more OO.
* @param rel a relative or absolute path
@@ -754,17 +893,17 @@ public final class FilePath implements Serializable {
/**
* Gets the parent file.
+ * @return parent FilePath or null if there is no parent
*/
public FilePath getParent() {
- int len = remote.length()-1;
- while(len>=0) {
- char ch = remote.charAt(len);
+ int i = remote.length() - 2;
+ for (; i >= 0; i--) {
+ char ch = remote.charAt(i);
if(ch=='\\' || ch=='/')
break;
- len--;
}
- return new FilePath( channel, remote.substring(0,len) );
+ return i >= 0 ? new FilePath( channel, remote.substring(0,i+1) ) : null;
}
/**
@@ -823,15 +962,38 @@ public final class FilePath implements Serializable {
}
}
+ /**
+ * Creates a temporary directory inside the directory represented by 'this'
+ * @since 1.311
+ */
+ public FilePath createTempDir(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);
+ f.delete();
+ f.mkdir();
+ return f.getName();
+ }
+ }));
+ } catch (IOException e) {
+ throw new IOException2("Failed to create a temp directory on "+remote,e);
+ }
+ }
+
/**
* Deletes this file.
+ * @throws IOException if it exists but could not be successfully deleted
+ * @return true, for a modicum of compatibility
*/
public boolean delete() throws IOException, InterruptedException {
- return act(new FileCallable() {
- public Boolean invoke(File f, VirtualChannel channel) throws IOException {
- return f.delete();
+ act(new FileCallable() {
+ public Void invoke(File f, VirtualChannel channel) throws IOException {
+ Util.deleteFile(f);
+ return null;
}
});
+ return true;
}
/**
@@ -906,19 +1068,40 @@ public final class FilePath implements Serializable {
*
* On Windows, no-op.
*
+ * @param mask
+ * File permission mask. To simplify the permission copying,
+ * if the parameter is -1, this method becomes no-op.
* @since 1.303
+ * @see #mode()
*/
public void chmod(final int mask) throws IOException, InterruptedException {
- if(!isUnix()) return;
+ if(!isUnix() || mask==-1) return;
act(new FileCallable() {
public Void invoke(File f, VirtualChannel channel) throws IOException {
- if(LIBC.chmod(f.getAbsolutePath(),mask)!=0)
+ if(File.separatorChar=='/' && LIBC.chmod(f.getAbsolutePath(),mask)!=0)
throw new IOException("Failed to chmod "+f+" : "+LIBC.strerror(Native.getLastError()));
return null;
}
});
}
+ /**
+ * Gets the file permission bit mask.
+ *
+ * @return
+ * -1 on Windows, since such a concept doesn't make sense.
+ * @since 1.311
+ * @see #chmod(int)
+ */
+ public int mode() throws IOException, InterruptedException {
+ if(!isUnix()) return -1;
+ return act(new FileCallable() {
+ public Integer invoke(File f, VirtualChannel channel) throws IOException {
+ return PosixAPI.get().stat(f.getPath()).mode();
+ }
+ });
+ }
+
/**
* List up files and directories in this directory.
*
@@ -929,6 +1112,22 @@ public final class FilePath implements Serializable {
return list((FileFilter)null);
}
+ /**
+ * List up subdirectories.
+ *
+ * @return can be empty but never null. Doesn't contain "." and ".."
+ */
+ public List listDirectories() throws IOException, InterruptedException {
+ return list(new DirectoryFilter());
+ }
+
+ private static final class DirectoryFilter implements FileFilter, Serializable {
+ public boolean accept(File f) {
+ return f.isDirectory();
+ }
+ private static final long serialVersionUID = 1L;
+ }
+
/**
* List up files in this directory, just like {@link File#listFiles(FileFilter)}.
*
@@ -939,6 +1138,9 @@ public final class FilePath implements Serializable {
* the filter object will be executed on the remote machine.
*/
public List list(final FileFilter filter) throws IOException, InterruptedException {
+ if (filter != null && !(filter instanceof Serializable)) {
+ throw new IllegalArgumentException("Non-serializable filter of " + filter.getClass());
+ }
return act(new FileCallable>() {
public List invoke(File f, VirtualChannel channel) throws IOException {
File[] children = f.listFiles(filter);
@@ -950,7 +1152,7 @@ public final class FilePath implements Serializable {
return r;
}
- });
+ }, (filter!=null?filter:this).getClass().getClassLoader());
}
/**
@@ -983,7 +1185,7 @@ public final class FilePath implements Serializable {
*/
private static String[] glob(File dir, String includes) throws IOException {
if(isAbsolute(includes))
- throw new IOException("Expecting Ant GLOB pattern, but saw '"+includes+"'. See http://ant.apache.org/manual/CoreTypes/fileset.html for syntax");
+ throw new IOException("Expecting Ant GLOB pattern, but saw '"+includes+"'. See http://ant.apache.org/manual/Types/fileset.html for syntax");
FileSet fs = Util.createFileSet(dir,includes);
DirectoryScanner ds = fs.getDirectoryScanner(new Project());
String[] files = ds.getIncludedFiles();
@@ -1015,6 +1217,18 @@ public final class FilePath implements Serializable {
return p.getIn();
}
+ /**
+ * Reads this file into a string, by using the current system encoding.
+ */
+ public String readToString() throws IOException {
+ InputStream in = read();
+ try {
+ return IOUtils.toString(in);
+ } finally {
+ in.close();
+ }
+ }
+
/**
* Writes to this file.
* If this file already exists, it will be overwritten.
@@ -1087,18 +1301,56 @@ public final class FilePath implements Serializable {
});
}
+ /**
+ * Moves all the contents of this directory into the specified directory, then delete this directory itself.
+ *
+ * @since 1.308.
+ */
+ public void moveAllChildrenTo(final FilePath target) throws IOException, InterruptedException {
+ if(this.channel != target.channel) {
+ throw new IOException("pullUpTo target must be on the same host");
+ }
+ act(new FileCallable() {
+ public Void invoke(File f, VirtualChannel channel) throws IOException {
+ File t = new File(target.getRemote());
+
+ for(File child : f.listFiles()) {
+ File target = new File(t, child.getName());
+ if(!child.renameTo(target))
+ throw new IOException("Failed to rename "+child+" to "+target);
+ }
+ f.delete();
+ return null;
+ }
+ });
+ }
+
/**
* 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();
+ OutputStream out = target.write();
+ try {
+ copyTo(out);
+ } finally {
+ out.close();
+ }
+ } catch (IOException e) {
+ throw new IOException2("Failed to copy "+this+" to "+target,e);
}
}
+ /**
+ * Copies this file to the specified target, with file permissions intact.
+ * @since 1.311
+ */
+ public void copyToWithPermission(FilePath target) throws IOException, InterruptedException {
+ copyTo(target);
+ // copy file permission
+ target.chmod(mode());
+ }
+
/**
* Sends the contents of this file into the given {@link OutputStream}.
*/
@@ -1135,6 +1387,14 @@ public final class FilePath implements Serializable {
void close() throws IOException;
}
+ /**
+ * Copies the contents of this directory recursively into the specified target directory.
+ * @since 1.312
+ */
+ public int copyRecursiveTo(FilePath target) throws IOException, InterruptedException {
+ return copyRecursiveTo("**/*",target);
+ }
+
public int copyRecursiveTo(String fileMask, FilePath target) throws IOException, InterruptedException {
return copyRecursiveTo(fileMask,null,target);
}
@@ -1168,6 +1428,7 @@ public final class FilePath implements Serializable {
setProject(new org.apache.tools.ant.Project());
}
+ @Override
protected void doFileOperations() {
copySize = super.fileCopyMap.size();
super.doFileOperations();
@@ -1181,6 +1442,7 @@ public final class FilePath implements Serializable {
CopyImpl copyTask = new CopyImpl();
copyTask.setTodir(new File(target.remote));
copyTask.addFileset(Util.createFileSet(base,fileMask,excludes));
+ copyTask.setOverwrite(true);
copyTask.setIncludeEmptyDirs(false);
copyTask.execute();
@@ -1247,53 +1509,42 @@ public final class FilePath implements Serializable {
}
}
+
/**
- * Writes to a tar stream and stores obtained files to the base dir.
+ * Writes files in 'this' directory to a tar stream.
*
- * @return
- * number of files/directories that are written.
+ * @param glob
+ * Ant file pattern mask, like "**/*.java".
*/
- private Integer writeToTar(File baseDir, String fileMask, String excludes, OutputStream out) throws IOException {
- FileSet fs = Util.createFileSet(baseDir,fileMask,excludes);
-
- byte[] buf = new byte[8192];
-
- TarOutputStream tar = new TarOutputStream(new BufferedOutputStream(out));
- tar.setLongFileMode(TarOutputStream.LONGFILE_GNU);
- String[] files;
- if(baseDir.exists()) {
- DirectoryScanner ds = fs.getDirectoryScanner(new org.apache.tools.ant.Project());
- files = ds.getIncludedFiles();
- } else {
- files = new String[0];
- }
- for( String f : files) {
- if(Functions.isWindows())
- f = f.replace('\\','/');
-
- File file = new File(baseDir, f);
-
- TarEntry te = new TarEntry(f);
- te.setModTime(file.lastModified());
- if(!file.isDirectory())
- te.setSize(file.length());
+ public int tar(OutputStream out, final String glob) throws IOException, InterruptedException {
+ return archive(ArchiverFactory.TAR, out, glob);
+ }
- tar.putNextEntry(te);
+ public int tar(OutputStream out, FileFilter filter) throws IOException, InterruptedException {
+ return archive(ArchiverFactory.TAR, out, filter);
+ }
- if (!file.isDirectory()) {
- FileInputStream in = new FileInputStream(file);
- int len;
- while((len=in.read(buf))>=0)
- tar.write(buf,0,len);
- in.close();
- }
+ /**
+ * Uses the given scanner on 'this' directory to list up files and then archive it to a tar stream.
+ */
+ public int tar(OutputStream out, DirScanner scanner) throws IOException, InterruptedException {
+ return archive(ArchiverFactory.TAR, out, scanner);
+ }
- tar.closeEntry();
+ /**
+ * Writes to a tar stream and stores obtained files to the base dir.
+ *
+ * @return
+ * number of files/directories that are written.
+ */
+ private static Integer writeToTar(File baseDir, String fileMask, String excludes, OutputStream out) throws IOException {
+ Archiver tw = ArchiverFactory.TAR.create(out);
+ try {
+ new DirScanner.Glob(fileMask,excludes).scan(baseDir,tw);
+ } finally {
+ tw.close();
}
-
- tar.close();
-
- return files.length;
+ return tw.countEntries();
}
/**
@@ -1311,16 +1562,15 @@ public final class FilePath implements Serializable {
File parent = f.getParentFile();
if (parent != null) parent.mkdirs();
- OutputStream fos = new FileOutputStream(f);
- try {
- IOUtils.copy(t,fos);
- } finally {
- fos.close();
- }
+ IOUtils.copy(t,f);
f.setLastModified(te.getModTime().getTime());
int mode = te.getMode()&0777;
- if(mode!=0 && !Hudson.isWindows()) // be defensive
- LIBC.chmod(f.getPath(),mode);
+ if(mode!=0 && !Functions.isWindows()) // be defensive
+ try {
+ LIBC.chmod(f.getPath(),mode);
+ } catch (NoClassDefFoundError e) {
+ // be defensive. see http://www.nabble.com/-3.0.6--Site-copy-problem%3A-hudson.util.IOException2%3A--java.lang.NoClassDefFoundError%3A-Could-not-initialize-class--hudson.util.jna.GNUCLibrary-td23588879.html
+ }
}
}
} catch(IOException e) {
@@ -1400,7 +1650,7 @@ public final class FilePath implements Serializable {
}
}
- {// check the (1) above next as this is more expensive.
+ {// check the (2) above next as this is more expensive.
// Try prepending "**/" to see if that results in a match
FileSet fs = Util.createFileSet(dir,"**/"+fileMask);
DirectoryScanner ds = fs.getDirectoryScanner(new Project());
@@ -1437,20 +1687,17 @@ public final class FilePath implements Serializable {
if(hasMatch(dir,pattern)) {
// found a match
if(previous==null)
- return String.format("'%s' doesn't match anything, although '%s' exists",
- fileMask, pattern );
+ return Messages.FilePath_validateAntFileMask_portionMatchAndSuggest(fileMask,pattern);
else
- return String.format("'%s' doesn't match anything: '%s' exists but not '%s'",
- fileMask, pattern, previous );
+ return Messages.FilePath_validateAntFileMask_portionMatchButPreviousNotMatchAndSuggest(fileMask,pattern,previous);
}
int idx = findSeparator(pattern);
if(idx<0) {// no more path component left to go back
if(pattern.equals(fileMask))
- return String.format("'%s' doesn't match anything", fileMask );
+ return Messages.FilePath_validateAntFileMask_doesntMatchAnything(fileMask);
else
- return String.format("'%s' doesn't match anything: even '%s' doesn't exist",
- fileMask, pattern );
+ return Messages.FilePath_validateAntFileMask_doesntMatchAnythingAndSuggest(fileMask,pattern);
}
// cut off the trailing component and try again
@@ -1499,10 +1746,14 @@ public final class FilePath implements Serializable {
}
/**
- * Checks the GLOB-style file mask. See {@link #validateAntFileMask(String)}
+ * Checks the GLOB-style file mask. See {@link #validateAntFileMask(String)}.
+ * Requires configure permission on ancestor AbstractProject object in request.
* @since 1.294
*/
public FormValidation validateFileMask(String value, boolean errorIfNotExist) throws IOException {
+ AbstractProject subject = Stapler.getCurrentRequest().findAncestorObject(AbstractProject.class);
+ subject.checkPermission(Item.CONFIGURE);
+
value = fixEmpty(value);
if(value==null)
return FormValidation.ok();
@@ -1521,6 +1772,7 @@ public final class FilePath implements Serializable {
/**
* Validates a relative file path from this {@link FilePath}.
+ * Requires configure permission on ancestor AbstractProject object in request.
*
* @param value
* The relative path being validated.
@@ -1540,7 +1792,7 @@ public final class FilePath implements Serializable {
if(value==null || (AbstractProject,?>)subject ==null) return FormValidation.ok();
// a common mistake is to use wildcard
- if(value.contains("*")) return FormValidation.error("Wildcard is not allowed here");
+ if(value.contains("*")) return FormValidation.error(Messages.FilePath_validateRelativePath_wildcardNotAllowed());
try {
if(!exists()) // no base directory. can't check
@@ -1552,16 +1804,17 @@ public final class FilePath implements Serializable {
if(!path.isDirectory())
return FormValidation.ok();
else
- return FormValidation.error(value+" is not a file");
+ return FormValidation.error(Messages.FilePath_validateRelativePath_notFile(value));
} else {
if(path.isDirectory())
return FormValidation.ok();
else
- return FormValidation.error(value+" is not a directory");
+ return FormValidation.error(Messages.FilePath_validateRelativePath_notDirectory(value));
}
}
- String msg = "No such "+(expectingFile?"file":"directory")+": " + value;
+ String msg = expectingFile ? Messages.FilePath_validateRelativePath_noSuchFile(value) :
+ Messages.FilePath_validateRelativePath_noSuchDirectory(value);
if(errorIfNotExist) return FormValidation.error(msg);
else return FormValidation.warning(msg);
} catch (InterruptedException e) {
@@ -1580,7 +1833,7 @@ public final class FilePath implements Serializable {
return validateRelativeDirectory(value,true);
}
- @Deprecated
+ @Deprecated @Override
public String toString() {
// to make writing JSPs easily, return local
return remote;
@@ -1629,22 +1882,43 @@ public final class FilePath implements Serializable {
*/
private class FileCallableWrapper implements DelegatingCallable {
private final FileCallable callable;
+ private transient ClassLoader classLoader;
public FileCallableWrapper(FileCallable callable) {
this.callable = callable;
+ this.classLoader = callable.getClass().getClassLoader();
+ }
+
+ private FileCallableWrapper(FileCallable callable, ClassLoader classLoader) {
+ this.callable = callable;
+ this.classLoader = classLoader;
}
public T call() throws IOException {
- return callable.invoke(new File(remote), Channel.current());
+ try {
+ return callable.invoke(new File(remote), Channel.current());
+ } catch (InterruptedException e) {
+ throw new TunneledInterruptedException(e);
+ }
}
public ClassLoader getClassLoader() {
- return callable.getClass().getClassLoader();
+ return classLoader;
}
private static final long serialVersionUID = 1L;
}
+ /**
+ * Used to tunnel {@link InterruptedException} over a Java signature that only allows {@link IOException}
+ */
+ private static class TunneledInterruptedException extends IOException2 {
+ private TunneledInterruptedException(InterruptedException cause) {
+ super(cause);
+ }
+ private static final long serialVersionUID = 1L;
+ }
+
private static final Comparator SHORTER_STRING_FIRST = new Comparator() {
public int compare(String o1, String o2) {
return o1.length()-o2.length();
diff --git a/core/src/main/java/hudson/FileSystemProvisioner.java b/core/src/main/java/hudson/FileSystemProvisioner.java
index d69a2ffa456fdb1521a99fc795bd585b106427c7..40b28d9c541c317ff884c8ffd017230881472023 100644
--- a/core/src/main/java/hudson/FileSystemProvisioner.java
+++ b/core/src/main/java/hudson/FileSystemProvisioner.java
@@ -167,11 +167,14 @@ public abstract class FileSystemProvisioner implements ExtensionPoint, Describab
* @param ws
* New workspace should be prepared in this location. This is the same value as
* {@code build.getProject().getWorkspace()} but passed separately for convenience.
+ * @param glob
+ * Ant-style file glob for files to include in the snapshot. May not be pertinent for all
+ * implementations.
*/
- public abstract WorkspaceSnapshot snapshot(AbstractBuild,?> build, FilePath ws, TaskListener listener) throws IOException, InterruptedException;
+ public abstract WorkspaceSnapshot snapshot(AbstractBuild,?> build, FilePath ws, String glob, TaskListener listener) throws IOException, InterruptedException;
public FileSystemProvisionerDescriptor getDescriptor() {
- return (FileSystemProvisionerDescriptor) Hudson.getInstance().getDescriptor(getClass());
+ return (FileSystemProvisionerDescriptor) Hudson.getInstance().getDescriptorOrDie(getClass());
}
/**
@@ -183,7 +186,7 @@ public abstract class FileSystemProvisioner implements ExtensionPoint, Describab
* Returns all the registered {@link FileSystemProvisioner} descriptors.
*/
public static DescriptorExtensionList all() {
- return Hudson.getInstance().getDescriptorList(FileSystemProvisioner.class);
+ return Hudson.getInstance().getDescriptorList(FileSystemProvisioner.class);
}
/**
@@ -198,13 +201,20 @@ public abstract class FileSystemProvisioner implements ExtensionPoint, Describab
}
/**
- * Creates a tar ball.
+ * @deprecated as of 1.350
*/
public WorkspaceSnapshot snapshot(AbstractBuild, ?> build, FilePath ws, TaskListener listener) throws IOException, InterruptedException {
+ return snapshot(build, ws, "**/*", listener);
+ }
+
+ /**
+ * Creates a tar ball.
+ */
+ public WorkspaceSnapshot snapshot(AbstractBuild, ?> build, FilePath ws, String glob, TaskListener listener) throws IOException, InterruptedException {
File wss = new File(build.getRootDir(),"workspace.zip");
OutputStream os = new BufferedOutputStream(new FileOutputStream(wss));
try {
- ws.createZipArchive(os);
+ ws.zip(os,glob);
} finally {
os.close();
}
diff --git a/core/src/main/java/hudson/FileSystemProvisionerDescriptor.java b/core/src/main/java/hudson/FileSystemProvisionerDescriptor.java
index 15d2e97b89cd6cf2a6c6b7305ed37b5f06428f7f..2cb0506fa9da19b11cea287563d6a6c4343e7b9c 100644
--- a/core/src/main/java/hudson/FileSystemProvisionerDescriptor.java
+++ b/core/src/main/java/hudson/FileSystemProvisionerDescriptor.java
@@ -25,7 +25,6 @@ package hudson;
import hudson.model.Descriptor;
import hudson.model.TaskListener;
-import hudson.model.AbstractBuild;
import java.io.IOException;
diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java
index c9dbc65a9c011f9ce2bce2f8af9de944fa582364..1098c5add69faafc0b73a59c1700e59f061370d5 100644
--- a/core/src/main/java/hudson/Functions.java
+++ b/core/src/main/java/hudson/Functions.java
@@ -1,7 +1,8 @@
/*
* The MIT License
*
- * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo! Inc., Stephen Connolly, Tom Huybrechts
+ * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
+ * Yahoo! Inc., Stephen Connolly, Tom Huybrechts, Alan Harder, Romain Seguy
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -23,8 +24,11 @@
*/
package hudson;
+import hudson.console.ConsoleAnnotationDescriptor;
+import hudson.console.ConsoleAnnotatorFactory;
import hudson.model.AbstractProject;
import hudson.model.Action;
+import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.model.Hudson;
import hudson.model.Item;
@@ -47,6 +51,7 @@ import hudson.security.AccessControlled;
import hudson.security.AuthorizationStrategy;
import hudson.security.Permission;
import hudson.security.SecurityRealm;
+import hudson.security.csrf.CrumbIssuer;
import hudson.slaves.Cloud;
import hudson.slaves.ComputerLauncher;
import hudson.slaves.NodeProperty;
@@ -61,12 +66,13 @@ import hudson.util.Area;
import hudson.util.Iterators;
import hudson.scm.SCM;
import hudson.scm.SCMDescriptor;
+import hudson.util.Secret;
+import hudson.views.MyViewsTabBar;
+import hudson.views.ViewsTabBar;
import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
import org.apache.commons.jelly.JellyContext;
import org.apache.commons.jelly.JellyTagException;
import org.apache.commons.jelly.Script;
-import org.apache.commons.jelly.Tag;
-import org.apache.commons.jelly.TagSupport;
import org.apache.commons.jelly.XMLOutput;
import org.apache.commons.jexl.parser.ASTSizeFunction;
import org.apache.commons.jexl.util.Introspector;
@@ -76,7 +82,6 @@ import org.kohsuke.stapler.Ancestor;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
-import org.kohsuke.stapler.jelly.CustomTagLibrary.StaplerDynamicTag;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
@@ -98,14 +103,20 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
+import java.util.Comparator;
+import java.util.ConcurrentModificationException;
+import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
+import java.util.Date;
+import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.SimpleFormatter;
import java.util.regex.Pattern;
@@ -436,7 +447,7 @@ public class Functions {
response.addCookie(c);
}
if (refresh) {
- response.addHeader("Refresh", "10");
+ response.addHeader("Refresh", System.getProperty("hudson.Functions.autoRefreshSeconds", "10"));
}
}
@@ -650,7 +661,7 @@ public class Functions {
}
public static List> getComputerLauncherDescriptors() {
- return Hudson.getInstance().getDescriptorList(ComputerLauncher.class);
+ return Hudson.getInstance().>getDescriptorList(ComputerLauncher.class);
}
public static List>> getRetentionStrategyDescriptors() {
@@ -661,6 +672,14 @@ public class Functions {
return ParameterDefinition.all();
}
+ public static List> getViewsTabBarDescriptors() {
+ return ViewsTabBar.all();
+ }
+
+ public static List> getMyViewsTabBarDescriptors() {
+ return MyViewsTabBar.all();
+ }
+
public static List getNodePropertyDescriptors(Class extends Node> clazz) {
List result = new ArrayList();
Collection list = (Collection) Hudson.getInstance().getDescriptorList(NodeProperty.class);
@@ -672,6 +691,25 @@ public class Functions {
return result;
}
+ /**
+ * Gets all the descriptors sorted by their inheritance tree of {@link Describable}
+ * so that descriptors of similar types come nearby.
+ */
+ public static Collection getSortedDescriptorsForGlobalConfig() {
+ Map r = new TreeMap();
+ for (Descriptor> d : Hudson.getInstance().getExtensionList(Descriptor.class)) {
+ if (d.getGlobalConfigPage()==null) continue;
+ r.put(buildSuperclassHierarchy(d.clazz, new StringBuilder()).toString(),d);
+ }
+ return r.values();
+ }
+
+ private static StringBuilder buildSuperclassHierarchy(Class c, StringBuilder buf) {
+ Class sc = c.getSuperclass();
+ if (sc!=null) buildSuperclassHierarchy(sc,buf).append(':');
+ return buf.append(c.getName());
+ }
+
/**
* Computes the path to the icon of the given action
* from the context path.
@@ -736,13 +774,70 @@ public class Functions {
}
public static Map dumpAllThreads() {
- return Thread.getAllStackTraces();
+ Map sorted = new TreeMap(new ThreadSorter());
+ sorted.putAll(Thread.getAllStackTraces());
+ return sorted;
}
@IgnoreJRERequirement
public static ThreadInfo[] getThreadInfos() {
ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
- return mbean.getThreadInfo(mbean.getAllThreadIds(),mbean.isObjectMonitorUsageSupported(),mbean.isSynchronizerUsageSupported());
+ return mbean.dumpAllThreads(mbean.isObjectMonitorUsageSupported(),mbean.isSynchronizerUsageSupported());
+ }
+
+ public static ThreadGroupMap sortThreadsAndGetGroupMap(ThreadInfo[] list) {
+ ThreadGroupMap sorter = new ThreadGroupMap();
+ Arrays.sort(list, sorter);
+ return sorter;
+ }
+
+ // Common code for sorting Threads/ThreadInfos by ThreadGroup
+ private static class ThreadSorterBase {
+ protected Map map = new HashMap();
+
+ private ThreadSorterBase() {
+ ThreadGroup tg = Thread.currentThread().getThreadGroup();
+ while (tg.getParent() != null) tg = tg.getParent();
+ Thread[] threads = new Thread[tg.activeCount()*2];
+ int threadsLen = tg.enumerate(threads, true);
+ for (int i = 0; i < threadsLen; i++)
+ map.put(threads[i].getId(), threads[i].getThreadGroup().getName());
+ }
+
+ protected int compare(long idA, long idB) {
+ String tga = map.get(idA), tgb = map.get(idB);
+ int result = (tga!=null?-1:0) + (tgb!=null?1:0); // Will be non-zero if only one is null
+ if (result==0 && tga!=null)
+ result = tga.compareToIgnoreCase(tgb);
+ return result;
+ }
+ }
+
+ public static class ThreadGroupMap extends ThreadSorterBase implements Comparator {
+
+ /**
+ * @return ThreadGroup name or null if unknown
+ */
+ public String getThreadGroup(ThreadInfo ti) {
+ return map.get(ti.getThreadId());
+ }
+
+ public int compare(ThreadInfo a, ThreadInfo b) {
+ int result = compare(a.getThreadId(), b.getThreadId());
+ if (result == 0)
+ result = a.getThreadName().compareToIgnoreCase(b.getThreadName());
+ return result;
+ }
+ }
+
+ private static class ThreadSorter extends ThreadSorterBase implements Comparator {
+
+ public int compare(Thread a, Thread b) {
+ int result = compare(a.getId(), b.getId());
+ if (result == 0)
+ result = a.getName().compareToIgnoreCase(b.getName());
+ return result;
+ }
}
/**
@@ -760,9 +855,11 @@ public class Functions {
// ThreadInfo.toString() truncates the stack trace by first 8, so needed my own version
@IgnoreJRERequirement
- public static String dumpThreadInfo(ThreadInfo ti) {
+ public static String dumpThreadInfo(ThreadInfo ti, ThreadGroupMap map) {
+ String grp = map.getThreadGroup(ti);
StringBuilder sb = new StringBuilder("\"" + ti.getThreadName() + "\"" +
- " Id=" + ti.getThreadId() + " " +
+ " Id=" + ti.getThreadId() + " Group=" +
+ (grp != null ? grp : "?") + " " +
ti.getThreadState());
if (ti.getLockName() != null) {
sb.append(" on " + ti.getLockName());
@@ -1000,10 +1097,10 @@ public class Functions {
if(SCHEME.matcher(urlName).matches())
return urlName; // absolute URL
if(urlName.startsWith("/"))
- return Stapler.getCurrentRequest().getContextPath()+urlName+'/';
+ return Stapler.getCurrentRequest().getContextPath()+urlName;
else
// relative URL name
- return Stapler.getCurrentRequest().getContextPath()+'/'+itUrl+urlName+'/';
+ return Stapler.getCurrentRequest().getContextPath()+'/'+itUrl+urlName;
}
/**
@@ -1065,13 +1162,6 @@ public class Functions {
return null;
}
- /**
- * Gets the URL for the update center server
- */
- public String getUpdateCenterUrl() {
- return Hudson.getInstance().getUpdateCenter().getUrl();
- }
-
/**
* If the given href link is matching the current page, return true.
*
@@ -1079,11 +1169,13 @@ public class Functions {
*/
public boolean hyperlinkMatchesCurrentPage(String href) throws UnsupportedEncodingException {
String url = Stapler.getCurrentRequest().getRequestURL().toString();
+ if (href == null || href.length() <= 1) return ".".equals(href) && url.endsWith("/");
url = URLDecoder.decode(url,"UTF-8");
href = URLDecoder.decode(href,"UTF-8");
+ if (url.endsWith("/")) url = url.substring(0, url.length() - 1);
+ if (href.endsWith("/")) href = href.substring(0, href.length() - 1);
- return (href.length()>1 && url.endsWith(href))
- || (href.equals(".") && url.endsWith("."));
+ return url.endsWith(href);
}
public List singletonList(T t) {
@@ -1104,36 +1196,106 @@ public class Functions {
}
/**
- * Used to assist form databinding. Given the "attrs" object,
- * find the ancestor tag file of the given name.
+ * Prepend a prefix only when there's the specified body.
*/
- public Tag findAncestorTag(Map attributes, String nsUri, String local) {
- Tag tag = (Tag) attributes.get("ownerTag");
- if(tag==null) return null;
+ public String prepend(String prefix, String body) {
+ if(body!=null && body.length()>0)
+ return prefix+body;
+ return body;
+ }
- while(true) {
- tag = TagSupport.findAncestorWithClass(tag.getParent(), StaplerDynamicTag.class);
- if(tag==null)
- return null;
- StaplerDynamicTag stag = (StaplerDynamicTag)tag;
- if(stag.getLocalName().equals(local) && stag.getNsUri().equals(nsUri))
- return tag;
+ public static List> getCrumbIssuerDescriptors() {
+ return CrumbIssuer.all();
+ }
+
+ public static String getCrumb(StaplerRequest req) {
+ Hudson h = Hudson.getInstance();
+ CrumbIssuer issuer = h != null ? h.getCrumbIssuer() : null;
+ return issuer != null ? issuer.getCrumb(req) : "";
+ }
+
+ public static String getCrumbRequestField() {
+ Hudson h = Hudson.getInstance();
+ CrumbIssuer issuer = h != null ? h.getCrumbIssuer() : null;
+ return issuer != null ? issuer.getDescriptor().getCrumbRequestField() : "";
+ }
+
+ public static Date getCurrentTime() {
+ return new Date();
+ }
+
+ /**
+ * Generate a series of <script> tags to include script.js
+ * from {@link ConsoleAnnotatorFactory}s and {@link ConsoleAnnotationDescriptor}s.
+ */
+ public static String generateConsoleAnnotationScriptAndStylesheet() {
+ String cp = Stapler.getCurrentRequest().getContextPath();
+ StringBuilder buf = new StringBuilder();
+ for (ConsoleAnnotatorFactory f : ConsoleAnnotatorFactory.all()) {
+ String path = cp + "/extensionList/" + ConsoleAnnotatorFactory.class.getName() + "/" + f.getClass().getName();
+ if (f.hasScript())
+ buf.append("");
+ if (f.hasStylesheet())
+ buf.append("");
}
+ for (ConsoleAnnotationDescriptor d : ConsoleAnnotationDescriptor.all()) {
+ String path = cp+"/descriptor/"+d.clazz.getName();
+ if (d.hasScript())
+ buf.append("");
+ if (d.hasStylesheet())
+ buf.append("");
+ }
+ return buf.toString();
}
/**
- * Prepend a prefix only when there's the specified body.
+ * Work around for bug 6935026.
*/
- public String prepend(String prefix, String body) {
- if(body!=null && body.length()>0)
- return prefix+body;
- return body;
+ public List getLoggerNames() {
+ while (true) {
+ try {
+ List r = new ArrayList();
+ Enumeration e = LogManager.getLogManager().getLoggerNames();
+ while (e.hasMoreElements())
+ r.add(e.nextElement());
+ return r;
+ } catch (ConcurrentModificationException e) {
+ // retry
+ }
+ }
}
+ /**
+ * Used by <f:password/> so that we send an encrypted value to the client.
+ */
+ public String getPasswordValue(Object o) {
+ if (o==null) return null;
+ if (o instanceof Secret) return ((Secret)o).getEncryptedValue();
+ return o.toString();
+ }
+
private static final Pattern SCHEME = Pattern.compile("[a-z]+://.+");
/**
- * Set to true if we are running unit tests.
+ * Returns true if we are running unit tests.
*/
- public static boolean isUnitTest = false;
+ public static boolean getIsUnitTest() {
+ return Main.isUnitTest;
+ }
+
+ /**
+ * Returns {@code true} if the {@link Run#ARTIFACTS} permission is enabled,
+ * {@code false} otherwise.
+ *
+ *
When the {@link Run#ARTIFACTS} permission is not turned on using the
+ * {@code hudson.security.ArtifactsPermission}, this permission must not be
+ * considered to be set to {@code false} for every user. It must rather be
+ * like if the permission doesn't exist at all (which means that every user
+ * has to have an access to the artifacts but the permission can't be
+ * configured in the security screen). Got it?
+ */
+ public static boolean isArtifactsPermissionEnabled() {
+ return Boolean.getBoolean("hudson.security.ArtifactsPermission");
+ }
+
}
diff --git a/core/src/main/java/hudson/Launcher.java b/core/src/main/java/hudson/Launcher.java
index 3b3f6321dafbdc26a0822ba889d5f23c05092b15..cdc5082f787e16dc191dd374b55a8839fb2554f3 100644
--- a/core/src/main/java/hudson/Launcher.java
+++ b/core/src/main/java/hudson/Launcher.java
@@ -35,8 +35,10 @@ import hudson.remoting.Pipe;
import hudson.remoting.RemoteInputStream;
import hudson.remoting.RemoteOutputStream;
import hudson.remoting.VirtualChannel;
-import hudson.util.ProcessTreeKiller;
import hudson.util.StreamCopyThread;
+import hudson.util.ArgumentListBuilder;
+import hudson.util.ProcessTree;
+import org.apache.commons.io.input.NullInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
@@ -46,6 +48,11 @@ import java.io.OutputStream;
import java.util.Arrays;
import java.util.Map;
import java.util.List;
+import java.util.ArrayList;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static org.apache.commons.io.output.NullOutputStream.NULL_OUTPUT_STREAM;
/**
* Starts a process.
@@ -65,6 +72,7 @@ import java.util.List;
*
*
* @author Kohsuke Kawaguchi
+ * @see FilePath#createLauncher(TaskListener)
*/
public abstract class Launcher {
@@ -115,7 +123,7 @@ public abstract class Launcher {
*
* @return
* null if this launcher is not created from a {@link Computer} object.
- * @deprecated
+ * @deprecated since 2008-11-16.
* See the javadoc for why this is inherently unreliable. If you are trying to
* figure out the current {@link Computer} from within a build, use
* {@link Computer#currentComputer()}
@@ -127,14 +135,187 @@ public abstract class Launcher {
return null;
}
+ /**
+ * Builder pattern for configuring a process to launch.
+ * @since 1.311
+ */
+ public final class ProcStarter {
+ protected List commands;
+ protected boolean[] masks;
+ protected FilePath pwd;
+ protected OutputStream stdout = NULL_OUTPUT_STREAM, stderr;
+ protected InputStream stdin = new NullInputStream(0);
+ protected String[] envs;
+
+ public ProcStarter cmds(String... args) {
+ return cmds(Arrays.asList(args));
+ }
+
+ public ProcStarter cmds(File program, String... args) {
+ commands = new ArrayList(args.length+1);
+ commands.add(program.getPath());
+ commands.addAll(Arrays.asList(args));
+ return this;
+ }
+
+ public ProcStarter cmds(List args) {
+ commands = new ArrayList(args);
+ return this;
+ }
+
+ public ProcStarter cmds(ArgumentListBuilder args) {
+ commands = args.toList();
+ masks = args.toMaskArray();
+ return this;
+ }
+
+ public List cmds() {
+ return commands;
+ }
+
+ public ProcStarter masks(boolean... masks) {
+ this.masks = masks;
+ return this;
+ }
+
+ public boolean[] masks() {
+ return masks;
+ }
+
+ public ProcStarter pwd(FilePath workDir) {
+ this.pwd = workDir;
+ return this;
+ }
+
+ public ProcStarter pwd(File workDir) {
+ return pwd(new FilePath(workDir));
+ }
+
+ public ProcStarter pwd(String workDir) {
+ return pwd(new File(workDir));
+ }
+
+ public FilePath pwd() {
+ return pwd;
+ }
+
+ public ProcStarter stdout(OutputStream out) {
+ this.stdout = out;
+ return this;
+ }
+
+ /**
+ * Sends the stdout to the given {@link TaskListener}.
+ */
+ public ProcStarter stdout(TaskListener out) {
+ return stdout(out.getLogger());
+ }
+
+ public OutputStream stdout() {
+ return stdout;
+ }
+
+ /**
+ * Controls where the stderr of the process goes.
+ * By default, it's bundled into stdout.
+ */
+ public ProcStarter stderr(OutputStream err) {
+ this.stderr = err;
+ return this;
+ }
+
+ public OutputStream stderr() {
+ return stderr;
+ }
+
+ /**
+ * Controls where the stdin of the process comes from.
+ * By default,