/*******************************************************************************
*
* Copyright (c) 2004-2009 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Kohsuke Kawaguchi, Stephen Connolly
*
*
*******************************************************************************/
package hudson;
import hudson.Proc.LocalProc;
import hudson.Proc.RemoteProc;
import hudson.model.Computer;
import hudson.model.Hudson;
import hudson.model.TaskListener;
import hudson.model.Node;
import hudson.remoting.Callable;
import hudson.remoting.Channel;
import hudson.remoting.Pipe;
import hudson.remoting.RemoteInputStream;
import hudson.remoting.RemoteOutputStream;
import hudson.remoting.VirtualChannel;
import hudson.util.StreamCopyThread;
import hudson.util.ArgumentListBuilder;
import hudson.util.ProcessTree;
import org.eclipse.hudson.jna.NativeUtils;
import org.apache.commons.io.input.NullInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
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.
*
* <p> This hides the difference between running programs locally vs remotely.
*
*
* <h2>'env' parameter</h2> <p> To allow important environment variables to be
* copied over to the remote machine, the 'env' parameter shouldn't contain
* default inherited environment variables (which often contains
* machine-specific information, like PATH, TIMEZONE, etc.)
*
* <p> {@link Launcher} is responsible for inheriting environment variables.
*
*
* @author Kohsuke Kawaguchi, Winston Prakash (bug fixes)
*
* @see FilePath#createLauncher(TaskListener)
*/
public abstract class Launcher {
protected final TaskListener listener;
protected final VirtualChannel channel;
public Launcher(TaskListener listener, VirtualChannel channel) {
this.listener = listener;
this.channel = channel;
}
/**
* Constructor for a decorator.
*/
protected Launcher(Launcher launcher) {
this(launcher.listener, launcher.channel);
}
/**
* Gets the channel that can be used to run a program remotely.
*
* @return null if the target node is not configured to support this. this
* is a transitional measure. Note that a launcher for the master is always
* non-null.
*/
public VirtualChannel getChannel() {
return channel;
}
/**
* Gets the {@link TaskListener} that this launcher uses to report the
* commands that it's executing.
*/
public TaskListener getListener() {
return listener;
}
/**
* If this {@link Launcher} is encapsulating an execution on a specific
* {@link Computer}, return it.
*
* <p> Because of the way internal Hudson abstractions are set up (that is,
* {@link Launcher} only needs a {@link VirtualChannel} to do its job and
* isn't really required that the channel comes from an existing
* {@link Computer}), this method may not always the right {@link Computer}
* instance.
*
* @return null if this launcher is not created from a {@link Computer}
* object.
* @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()}
*/
public Computer getComputer() {
for (Computer c : Hudson.getInstance().getComputers()) {
if (c.getChannel() == channel) {
return c;
}
}
return null;
}
/**
* Builder pattern for configuring a process to launch.
*
* @since 1.311
*/
public final class ProcStarter {
protected List<String> 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<String>(args.length + 1);
commands.add(program.getPath());
commands.addAll(Arrays.asList(args));
return this;
}
public ProcStarter cmds(List<String> args) {
commands = new ArrayList<String>(args);
return this;
}
public ProcStarter cmds(ArgumentListBuilder args) {
commands = args.toList();
masks = args.toMaskArray();
return this;
}
public List<String> 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,
* <tt>/dev/null</tt>.
*/
public ProcStarter stdin(InputStream in) {
this.stdin = in;
return this;
}
public InputStream stdin() {
return stdin;
}
/**
* Sets the environment variable overrides.
*
* <p> In adition to what the current process is inherited (if this is
* going to be launched from a slave agent, that becomes the "current"
* process), these variables will be also set.
*/
public ProcStarter envs(Map<String, String> overrides) {
return envs(Util.mapToEnv(overrides));
}
/**
* @param overrides List of "VAR=VALUE". See {@link #envs(Map)} for the
* semantics.
*/
public ProcStarter envs(String... overrides) {
this.envs = overrides;
return this;
}
public String[] envs() {
return envs;
}
/**
* Starts the new process as configured.
*/
public Proc start() throws IOException {
return launch(this);
}
/**
* Starts the process and waits for its completion.
*/
public int join() throws IOException, InterruptedException {
return start().join();
}
/**
* Copies a {@link ProcStarter}.
*/
public ProcStarter copy() {
return new ProcStarter().cmds(commands).pwd(pwd).masks(masks).stdin(stdin).stdout(stdout).stderr(stderr).envs(envs);
}
}
/**
* Launches a process by using a {@linkplain ProcStarter builder-pattern} to
* configure the parameters.
*/
public final ProcStarter launch() {
return new ProcStarter();
}
/**
* @deprecated as of 1.311 Use {@link #launch()} and its associated builder
* pattern
*/
public final Proc launch(String cmd, Map<String, String> env, OutputStream out, FilePath workDir) throws IOException {
return launch(cmd, Util.mapToEnv(env), out, workDir);
}
/**
* @deprecated as of 1.311 Use {@link #launch()} and its associated builder
* pattern
*/
public final Proc launch(String[] cmd, Map<String, String> env, OutputStream out, FilePath workDir) throws IOException {
return launch(cmd, Util.mapToEnv(env), out, workDir);
}
/**
* @deprecated as of 1.311 Use {@link #launch()} and its associated builder
* pattern
*/
public final Proc launch(String[] cmd, Map<String, String> env, InputStream in, OutputStream out) throws IOException {
return launch(cmd, Util.mapToEnv(env), in, out);
}
/**
* Launch a command with optional censoring of arguments from the listener
* (Note: <strong>The censored portions will remain visible through /proc,
* pargs, process explorer, etc. i.e. people logged in on the same
* machine</strong> This version of the launch command just ensures that it
* is not visible from a build log which is exposed via the web)
*
* @param cmd The command and all it's arguments.
* @param mask Which of the command and arguments should be masked from the
* listener
* @param env Environment variable overrides.
* @param out stdout and stderr of the process will be sent to this stream.
* the stream won't be closed.
* @param workDir null if the working directory could be anything.
* @return The process of the command.
* @throws IOException When there are IO problems.
*
* @deprecated as of 1.311 Use {@link #launch()} and its associated builder
* pattern
*/
public final Proc launch(String[] cmd, boolean[] mask, Map<String, String> env, OutputStream out, FilePath workDir) throws IOException {
return launch(cmd, mask, Util.mapToEnv(env), out, workDir);
}
/**
* Launch a command with optional censoring of arguments from the listener
* (Note: <strong>The censored portions will remain visible through /proc,
* pargs, process explorer, etc. i.e. people logged in on the same
* machine</strong> This version of the launch command just ensures that it
* is not visible from a build log which is exposed via the web)
*
* @param cmd The command and all it's arguments.
* @param mask Which of the command and arguments should be masked from the
* listener
* @param env Environment variable overrides.
* @param in null if there's no input.
* @param out stdout and stderr of the process will be sent to this stream.
* the stream won't be closed.
* @return The process of the command.
* @throws IOException When there are IO problems.
*
* @deprecated as of 1.311 Use {@link #launch()} and its associated builder
* pattern
*/
public final Proc launch(String[] cmd, boolean[] mask, Map<String, String> env, InputStream in, OutputStream out) throws IOException {
return launch(cmd, mask, Util.mapToEnv(env), in, out);
}
/**
* @deprecated as of 1.311 Use {@link #launch()} and its associated builder
* pattern
*/
public final Proc launch(String cmd, String[] env, OutputStream out, FilePath workDir) throws IOException {
return launch(Util.tokenize(cmd), env, out, workDir);
}
/**
* @deprecated as of 1.311 Use {@link #launch()} and its associated builder
* pattern
*/
public final Proc launch(String[] cmd, String[] env, OutputStream out, FilePath workDir) throws IOException {
return launch(cmd, env, null, out, workDir);
}
/**
* @deprecated as of 1.311 Use {@link #launch()} and its associated builder
* pattern
*/
public final Proc launch(String[] cmd, String[] env, InputStream in, OutputStream out) throws IOException {
return launch(cmd, env, in, out, null);
}
/**
* Launch a command with optional censoring of arguments from the listener
* (Note: <strong>The censored portions will remain visible through /proc,
* pargs, process explorer, etc. i.e. people logged in on the same
* machine</strong> This version of the launch command just ensures that it
* is not visible from a build log which is exposed via the web)
*
* @param cmd The command and all it's arguments.
* @param mask Which of the command and arguments should be masked from the
* listener
* @param env Environment variable overrides.
* @param out stdout and stderr of the process will be sent to this stream.
* the stream won't be closed.
* @param workDir null if the working directory could be anything.
* @return The process of the command.
* @throws IOException When there are IO problems.
*
* @deprecated as of 1.311 Use {@link #launch()} and its associated builder
* pattern
*/
public final Proc launch(String[] cmd, boolean[] mask, String[] env, OutputStream out, FilePath workDir) throws IOException {
return launch(cmd, mask, env, null, out, workDir);
}
/**
* Launch a command with optional censoring of arguments from the listener
* (Note: <strong>The censored portions will remain visible through /proc,
* pargs, process explorer, etc. i.e. people logged in on the same
* machine</strong> This version of the launch command just ensures that it
* is not visible from a build log which is exposed via the web)
*
* @param cmd The command and all it's arguments.
* @param mask Which of the command and arguments should be masked from the
* listener
* @param env Environment variable overrides.
* @param in null if there's no input.
* @param out stdout and stderr of the process will be sent to this stream.
* the stream won't be closed.
* @return The process of the command.
* @throws IOException When there are IO problems.
*
* @deprecated as of 1.311 Use {@link #launch()} and its associated builder
* pattern
*/
public final Proc launch(String[] cmd, boolean[] mask, String[] env, InputStream in, OutputStream out) throws IOException {
return launch(cmd, mask, env, in, out, null);
}
/**
* @param env Environment variable overrides.
* @param in null if there's no input.
* @param workDir null if the working directory could be anything.
* @param out stdout and stderr of the process will be sent to this stream.
* the stream won't be closed.
*
* @deprecated as of 1.311 Use {@link #launch()} and its associated builder
* pattern
*/
public Proc launch(String[] cmd, String[] env, InputStream in, OutputStream out, FilePath workDir) throws IOException {
return launch(launch().cmds(cmd).envs(env).stdin(in).stdout(out).pwd(workDir));
}
/**
* Launch a command with optional censoring of arguments from the listener
* (Note: <strong>The censored portions will remain visible through /proc,
* pargs, process explorer, etc. i.e. people logged in on the same
* machine</strong> This version of the launch command just ensures that it
* is not visible from a build log which is exposed via the web)
*
* @param cmd The command and all it's arguments.
* @param mask Which of the command and arguments should be masked from the
* listener
* @param env Environment variable overrides.
* @param in null if there's no input.
* @param out stdout and stderr of the process will be sent to this stream.
* the stream won't be closed.
* @param workDir null if the working directory could be anything.
* @return The process of the command.
* @throws IOException When there are IO problems.
*
* @deprecated as of 1.311 Use {@link #launch()} and its associated builder
* pattern
*/
public Proc launch(String[] cmd, boolean[] mask, String[] env, InputStream in, OutputStream out, FilePath workDir) throws IOException {
return launch(launch().cmds(cmd).masks(mask).envs(env).stdin(in).stdout(out).pwd(workDir));
}
/**
* Primarily invoked from {@link ProcStarter#start()} to start a process
* with a specific launcher.
*/
public abstract Proc launch(ProcStarter starter) throws IOException;
/**
* Launches a specified process and connects its input/output to a
* {@link Channel}, then return it.
*
* <p> When the returned channel is terminated, the process will be killed.
*
* @param out Where the stderr from the launched process will be sent.
* @param workDir The working directory of the new process, or null to
* inherit from the current process
* @param envVars Environment variable overrides. In addition to what the
* current process is inherited (if this is going to be launched from a
* slave agent, that becomes the "current" process), these variables will be
* also set.
*/
public abstract Channel launchChannel(String[] cmd, OutputStream out, FilePath workDir, Map<String, String> envVars) throws IOException, InterruptedException;
/**
* Returns true if this {@link Launcher} is going to launch on Unix.
*/
public boolean isUnix() {
return File.pathSeparatorChar == ':';
}
/**
* Calls {@link ProcessTree#killAll(Map)} to kill processes.
*/
public abstract void kill(Map<String, String> modelEnvVars) throws IOException, InterruptedException;
/**
* Prints out the command line to the listener so that users know what we
* are doing.
*/
protected final void printCommandLine(String[] cmd, FilePath workDir) {
StringBuilder buf = new StringBuilder();
if (workDir != null) {
buf.append('[');
if (showFullPath) {
buf.append(workDir.getRemote());
} else {
buf.append(workDir.getRemote().replaceFirst("^.+[/\\\\]", ""));
}
buf.append("] ");
}
buf.append('$');
for (String c : cmd) {
buf.append(' ');
if (c.indexOf(' ') >= 0) {
if (c.indexOf('"') >= 0) {
buf.append('\'').append(c).append('\'');
} else {
buf.append('"').append(c).append('"');
}
} else {
buf.append(c);
}
}
listener.getLogger().println(buf.toString());
}
/**
* Prints out the command line to the listener with some portions masked to
* prevent sensitive information from being recorded on the listener.
*
* @param cmd The commands
* @param mask An array of booleans which control whether a cmd element
* should be masked (<code>true</code>) or remain unmasked
* (<code>false</code>).
* @param workDir The work dir.
*/
protected final void maskedPrintCommandLine(List<String> cmd, boolean[] mask, FilePath workDir) {
if (mask == null) {
printCommandLine(cmd.toArray(new String[cmd.size()]), workDir);
return;
}
assert mask.length == cmd.size();
final String[] masked = new String[cmd.size()];
for (int i = 0; i < cmd.size(); i++) {
if (mask[i]) {
masked[i] = "********";
} else {
masked[i] = cmd.get(i);
}
}
printCommandLine(masked, workDir);
}
protected final void maskedPrintCommandLine(String[] cmd, boolean[] mask, FilePath workDir) {
maskedPrintCommandLine(Arrays.asList(cmd), mask, workDir);
}
/**
* Returns a decorated {@link Launcher} for the given node.
*/
public final Launcher decorateFor(Node node) {
Launcher l = this;
for (LauncherDecorator d : LauncherDecorator.all()) {
l = d.decorate(l, node);
}
return l;
}
/**
* Returns a decorated {@link Launcher} that puts the given set of arguments
* as a prefix to any commands that it invokes.
*
* @since 1.299
*/
public final Launcher decorateByPrefix(final String... prefix) {
final Launcher outer = this;
return new Launcher(outer) {
@Override
public Proc launch(ProcStarter starter) throws IOException {
starter.commands.addAll(0, Arrays.asList(prefix));
starter.masks = prefix(starter.masks);
return outer.launch(starter);
}
@Override
public Channel launchChannel(String[] cmd, OutputStream out, FilePath workDir, Map<String, String> envVars) throws IOException, InterruptedException {
return outer.launchChannel(prefix(cmd), out, workDir, envVars);
}
@Override
public void kill(Map<String, String> modelEnvVars) throws IOException, InterruptedException {
outer.kill(modelEnvVars);
}
private String[] prefix(String[] args) {
String[] newArgs = new String[args.length + prefix.length];
System.arraycopy(prefix, 0, newArgs, 0, prefix.length);
System.arraycopy(args, 0, newArgs, prefix.length, args.length);
return newArgs;
}
private boolean[] prefix(boolean[] args) {
boolean[] newArgs = new boolean[args.length + prefix.length];
System.arraycopy(args, 0, newArgs, prefix.length, args.length);
return newArgs;
}
};
}
/**
* {@link Launcher} that launches process locally.
*/
public static class LocalLauncher extends Launcher {
public LocalLauncher(TaskListener listener) {
this(listener, Hudson.MasterComputer.localChannel);
}
public LocalLauncher(TaskListener listener, VirtualChannel channel) {
super(listener, channel);
}
@Override
public Proc launch(ProcStarter ps) throws IOException {
maskedPrintCommandLine(ps.commands, ps.masks, ps.pwd);
EnvVars jobEnv = inherit(ps.envs);
// replace variables in command line
String[] jobCmd = new String[ps.commands.size()];
for (int idx = 0; idx < jobCmd.length; idx++) {
jobCmd[idx] = jobEnv.expand(ps.commands.get(idx));
}
return new LocalProc(jobCmd, Util.mapToEnv(jobEnv), ps.stdin, ps.stdout, ps.stderr, toFile(ps.pwd));
}
private File toFile(FilePath f) {
return f == null ? null : new File(f.getRemote());
}
public Channel launchChannel(String[] cmd, OutputStream out, FilePath workDir, Map<String, String> envVars) throws IOException {
printCommandLine(cmd, workDir);
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.directory(toFile(workDir));
if (envVars != null) {
pb.environment().putAll(envVars);
}
return launchChannel(out, pb);
}
@Override
public void kill(Map<String, String> modelEnvVars) throws InterruptedException {
ProcessTree.get().killAll(modelEnvVars);
}
/**
* @param out Where the stderr from the launched process will be sent.
*/
public Channel launchChannel(OutputStream out, ProcessBuilder pb) throws IOException {
final EnvVars cookie = EnvVars.createCookie();
pb.environment().putAll(cookie);
final Process proc = pb.start();
final Thread t2 = new StreamCopyThread(pb.command() + ": stderr copier", proc.getErrorStream(), out);
t2.start();
final NativeUtils nativeUtils = NativeUtils.getInstance();
return new Channel("locally launched channel on " + pb.command(),
Computer.threadPoolForRemoting, proc.getInputStream(), proc.getOutputStream(), out) {
/**
* Kill the process when the channel is severed.
*/
@Override
protected synchronized void terminate(IOException e) {
super.terminate(e);
ProcessTree pt = ProcessTree.get(nativeUtils);
try {
pt.killAll(proc, cookie);
} catch (InterruptedException x) {
LOGGER.log(Level.INFO, "Interrupted", x);
}
}
@Override
public synchronized void close() throws IOException {
super.close();
// wait for all the output from the process to be picked up
try {
t2.join();
} catch (InterruptedException e) {
// process the interrupt later
Thread.currentThread().interrupt();
}
}
};
}
}
/**
* Launches processes remotely by using the given channel.
*/
public static class RemoteLauncher extends Launcher {
private final boolean isUnix;
public RemoteLauncher(TaskListener listener, VirtualChannel channel, boolean isUnix) {
super(listener, channel);
this.isUnix = isUnix;
}
public Proc launch(ProcStarter ps) throws IOException {
OutputStream out = null;
OutputStream err = null;
InputStream in = null;
if (ps.stdout != null) {
out = new RemoteOutputStream(new CloseProofOutputStream(ps.stdout));
}
if (ps.stderr != null) {
err = new RemoteOutputStream(new CloseProofOutputStream(ps.stderr));
}
if (ps.stdin != null) {
in = new RemoteInputStream(ps.stdin);
}
final String workDir = ps.pwd == null ? null : ps.pwd.getRemote();
return new RemoteProc(getChannel().callAsync(new RemoteLaunchCallable(ps.commands, ps.masks, ps.envs, in, out, err, workDir, listener)));
}
public Channel launchChannel(String[] cmd, OutputStream err, FilePath _workDir, Map<String, String> envOverrides) throws IOException, InterruptedException {
printCommandLine(cmd, _workDir);
Pipe out = Pipe.createRemoteToLocal();
final String workDir = _workDir == null ? null : _workDir.getRemote();
OutputStream os = getChannel().call(new RemoteChannelLaunchCallable(cmd, out, err, workDir, envOverrides));
return new Channel("remotely launched channel on " + channel,
Computer.threadPoolForRemoting, out.getIn(), new BufferedOutputStream(os));
}
@Override
public boolean isUnix() {
return isUnix;
}
@Override
public void kill(final Map<String, String> modelEnvVars) throws IOException, InterruptedException {
final NativeUtils nativeUtils = NativeUtils.getInstance();
getChannel().call(new KillTask(modelEnvVars, nativeUtils));
}
private static final class KillTask implements Callable<Void, RuntimeException> {
private final Map<String, String> modelEnvVars;
private final NativeUtils nativeUtils;
private KillTask(Map<String, String> modelEnvVars, NativeUtils nativeUtils) {
this.modelEnvVars = modelEnvVars;
this.nativeUtils = nativeUtils;
}
public Void call() throws RuntimeException {
try {
ProcessTree.get(nativeUtils).killAll(modelEnvVars);
} catch (InterruptedException e) {
// we are asked to terminate early by the caller, so no need to do anything
}
return null;
}
private static final long serialVersionUID = 1L;
}
}
private static class RemoteLaunchCallable implements Callable<Integer, IOException> {
private final List<String> cmd;
private final boolean[] masks;
private final String[] env;
private final InputStream in;
private final OutputStream out;
private final OutputStream err;
private final String workDir;
private final TaskListener listener;
RemoteLaunchCallable(List<String> cmd, boolean[] masks, String[] env, InputStream in, OutputStream out, OutputStream err, String workDir, TaskListener listener) {
this.cmd = new ArrayList<String>(cmd);
this.masks = masks;
this.env = env;
this.in = in;
this.out = out;
this.err = err;
this.workDir = workDir;
this.listener = listener;
}
public Integer call() throws IOException {
Launcher.ProcStarter ps = new LocalLauncher(listener).launch();
ps.cmds(cmd).masks(masks).envs(env).stdin(in).stdout(out).stderr(err);
if (workDir != null) {
ps.pwd(workDir);
}
Proc p = ps.start();
try {
return p.join();
} catch (InterruptedException e) {
return -1;
} finally {
try {
// Fix: http://issues.hudson-ci.org/browse/HUDSON-7809
// This call should not return immediately after the
// process is done. The pipe associated with the channel
// may be still transmitting data.
// Get the channel associated with this thread and flush
// its IO pipe
Channel.current().flushPipe();
} catch (InterruptedException ex) {
Logger.getLogger(Launcher.class.getName()).log(Level.INFO, null, ex);
}
}
}
private static final long serialVersionUID = 1L;
}
private static class RemoteChannelLaunchCallable implements Callable<OutputStream, IOException> {
private final String[] cmd;
private final Pipe out;
private final String workDir;
private final OutputStream err;
private final Map<String, String> envOverrides;
public RemoteChannelLaunchCallable(String[] cmd, Pipe out, OutputStream err, String workDir, Map<String, String> envOverrides) {
this.cmd = cmd;
this.out = out;
this.err = new RemoteOutputStream(err);
this.workDir = workDir;
this.envOverrides = envOverrides;
}
public OutputStream call() throws IOException {
Process p = Runtime.getRuntime().exec(cmd,
Util.mapToEnv(inherit(envOverrides)),
workDir == null ? null : new File(workDir));
List<String> cmdLines = Arrays.asList(cmd);
new StreamCopyThread("stdin copier for remote agent on " + cmdLines,
p.getInputStream(), out.getOut()).start();
new StreamCopyThread("stderr copier for remote agent on " + cmdLines,
p.getErrorStream(), err).start();
// TODO: don't we need to join?
return new RemoteOutputStream(p.getOutputStream());
}
private static final long serialVersionUID = 1L;
}
/**
* Expands the list of environment variables by inheriting current env
* variables.
*/
private static EnvVars inherit(String[] env) {
// convert String[] to Map first
EnvVars m = new EnvVars();
if (env != null) {
for (String e : env) {
int index = e.indexOf('=');
m.put(e.substring(0, index), e.substring(index + 1));
}
}
// then do the inheritance
return inherit(m);
}
/**
* Expands the list of environment variables by inheriting current env
* variables.
*/
private static EnvVars inherit(Map<String, String> overrides) {
EnvVars m = new EnvVars(EnvVars.masterEnvVars);
for (Map.Entry<String, String> o : overrides.entrySet()) {
m.override(o.getKey(), m.expand(o.getValue()));
}
return m;
}
/**
* Debug option to display full current path instead of just the last token.
*/
public static boolean showFullPath = false;
private static final Logger LOGGER = Logger.getLogger(Launcher.class.getName());
}