All Downloads are FREE. Search and download functionalities are using the official Maven repository.

hudson.Launcher Maven / Gradle / Ivy

The newest version!
/*******************************************************************************
 *
 * 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.
 *
 * 

This hides the difference between running programs locally vs remotely. * * *

'env' parameter

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

{@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. * *

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 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, * /dev/null. */ public ProcStarter stdin(InputStream in) { this.stdin = in; return this; } public InputStream stdin() { return stdin; } /** * Sets the environment variable overrides. * *

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 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 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 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 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: The censored portions will remain visible through /proc, * pargs, process explorer, etc. i.e. people logged in on the same * machine 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 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: The censored portions will remain visible through /proc, * pargs, process explorer, etc. i.e. people logged in on the same * machine 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 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: The censored portions will remain visible through /proc, * pargs, process explorer, etc. i.e. people logged in on the same * machine 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: The censored portions will remain visible through /proc, * pargs, process explorer, etc. i.e. people logged in on the same * machine 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: The censored portions will remain visible through /proc, * pargs, process explorer, etc. i.e. people logged in on the same * machine 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. * *

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 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 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 (true) or remain unmasked * (false). * @param workDir The work dir. */ protected final void maskedPrintCommandLine(List 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 envVars) throws IOException, InterruptedException { return outer.launchChannel(prefix(cmd), out, workDir, envVars); } @Override public void kill(Map 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 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 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 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 modelEnvVars) throws IOException, InterruptedException { final NativeUtils nativeUtils = NativeUtils.getInstance(); getChannel().call(new KillTask(modelEnvVars, nativeUtils)); } private static final class KillTask implements Callable { private final Map modelEnvVars; private final NativeUtils nativeUtils; private KillTask(Map 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 { private final List 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 cmd, boolean[] masks, String[] env, InputStream in, OutputStream out, OutputStream err, String workDir, TaskListener listener) { this.cmd = new ArrayList(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 { private final String[] cmd; private final Pipe out; private final String workDir; private final OutputStream err; private final Map envOverrides; public RemoteChannelLaunchCallable(String[] cmd, Pipe out, OutputStream err, String workDir, Map 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 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 overrides) { EnvVars m = new EnvVars(EnvVars.masterEnvVars); for (Map.Entry 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()); }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy