brooklyn.util.internal.ssh.process.ProcessTool Maven / Gradle / Ivy
Show all versions of brooklyn-core Show documentation
package brooklyn.util.internal.ssh.process;
import static brooklyn.entity.basic.ConfigKeys.newConfigKey;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import brooklyn.config.ConfigKey;
import brooklyn.util.collections.MutableList;
import brooklyn.util.collections.MutableMap;
import brooklyn.util.exceptions.Exceptions;
import brooklyn.util.internal.ssh.ShellAbstractTool;
import brooklyn.util.internal.ssh.ShellTool;
import brooklyn.util.internal.ssh.SshException;
import brooklyn.util.stream.StreamGobbler;
import brooklyn.util.text.Identifiers;
import brooklyn.util.text.Strings;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
/** Implementation of {@link ShellTool} which runs locally. */
public class ProcessTool extends ShellAbstractTool implements ShellTool {
private static final Logger LOG = LoggerFactory.getLogger(ProcessTool.class);
// applies to calls
public static final ConfigKey PROP_LOGIN_SHELL = newConfigKey("loginShell", "Causes the commands to be invoked with bash arguments to forcea login shell", Boolean.FALSE);
public ProcessTool() {
this(null);
}
public ProcessTool(Map flags) {
super(getOptionalVal(flags, PROP_LOCAL_TEMP_DIR));
if (flags!=null) {
MutableMap flags2 = MutableMap.copyOf(flags);
// TODO should remember other flags here? (e.g. NO_EXTRA_OUTPUT, RUN_AS_ROOT, etc)
flags2.remove(PROP_LOCAL_TEMP_DIR.getName());
if (!flags2.isEmpty())
LOG.warn(""+this+" ignoring unsupported constructor flags: "+flags);
}
}
@Override
public int execScript(Map props, List commands, Map env) {
try {
OutputStream out = getOptionalVal(props, PROP_OUT_STREAM);
OutputStream err = getOptionalVal(props, PROP_ERR_STREAM);
String scriptDir = getOptionalVal(props, PROP_SCRIPT_DIR);
Boolean noExtraOutput = getOptionalVal(props, PROP_NO_EXTRA_OUTPUT);
Boolean runAsRoot = getOptionalVal(props, PROP_RUN_AS_ROOT);
String separator = getOptionalVal(props, PROP_SEPARATOR);
String scriptPath = scriptDir+"/brooklyn-"+System.currentTimeMillis()+"-"+Identifiers.makeRandomId(8)+".sh";
String scriptContents = toScript(props, commands, env);
if (LOG.isTraceEnabled()) LOG.trace("Running shell process (process) as script:\n{}", scriptContents);
File to = new File(scriptPath);
Files.createParentDirs(to);
Files.copy(ByteStreams.newInputStreamSupplier(scriptContents.getBytes()), to);
List cmds = buildRunScriptCommand(scriptPath, noExtraOutput, runAsRoot);
cmds.add(0, "chmod +x "+scriptPath);
return asInt(execProcesses(cmds, null, out, err, separator, getOptionalVal(props, PROP_LOGIN_SHELL), this), -1);
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
@Override
public int execCommands(Map props, List commands, Map env) {
if (props.containsKey("blocks") && props.get("blocks") == Boolean.FALSE) {
throw new IllegalArgumentException("Cannot exec non-blocking: command="+commands);
}
OutputStream out = getOptionalVal(props, PROP_OUT_STREAM);
OutputStream err = getOptionalVal(props, PROP_ERR_STREAM);
String separator = getOptionalVal(props, PROP_SEPARATOR);
List allcmds = toCommandSequence(commands, null);
String singlecmd = Joiner.on(separator).join(allcmds);
if (getOptionalVal(props, PROP_RUN_AS_ROOT)==Boolean.TRUE) {
LOG.warn("Cannot run as root when executing as command; run as a script instead (will run as normal user): "+singlecmd);
}
if (LOG.isTraceEnabled()) LOG.trace("Running shell command (process): {}", singlecmd);
return asInt(execProcesses(allcmds, env, out, err, separator, getOptionalVal(props, PROP_LOGIN_SHELL), this), -1);
}
/** as {@link #execProcesses(List, Map, OutputStream, OutputStream, String, boolean, Object)} but not using a login shell
*/
public static int execProcesses(List cmds, Map env, OutputStream out, OutputStream err, String separator, Object contextForLogging) {
return execProcesses(cmds, env, out, err, separator, false, contextForLogging);
}
/** executes a set of commands by sending them as a single process to `bash -c`
* (single command argument of all the commands, joined with separator)
*
* consequence of this is that you should not normally need to escape things oddly in your commands,
* type them just as you would into a bash shell (if you find exceptions please note them here!)
*/
public static int execProcesses(List cmds, Map env, OutputStream out, OutputStream err, String separator, boolean asLoginShell, Object contextForLogging) {
MutableList commands = new MutableList().append("bash");
if (asLoginShell) commands.append("-l");
commands.append("-c", Strings.join(cmds, Preconditions.checkNotNull(separator, "separator")));
return execSingleProcess(commands, env, out, err, contextForLogging);
}
/** executes a single process made up of the given command words (*not* bash escaped);
* should be portable across OS's */
public static int execSingleProcess(List cmdWords, Map env, OutputStream out, OutputStream err, Object contextForLogging) {
StreamGobbler errgobbler = null;
StreamGobbler outgobbler = null;
ProcessBuilder pb = new ProcessBuilder(cmdWords);
if (env!=null) {
for (Map.Entry kv: env.entrySet()) pb.environment().put(kv.getKey(), String.valueOf(kv.getValue()));
}
try {
Process p = pb.start();
if (out != null) {
InputStream outstream = p.getInputStream();
outgobbler = new StreamGobbler(outstream, out, (Logger) null);
outgobbler.start();
}
if (err != null) {
InputStream errstream = p.getErrorStream();
errgobbler = new StreamGobbler(errstream, err, (Logger) null);
errgobbler.start();
}
int result = p.waitFor();
if (outgobbler != null) outgobbler.blockUntilFinished();
if (errgobbler != null) errgobbler.blockUntilFinished();
if (result==255)
// this is not definitive, but tests (and code?) expects throw exception if can't connect;
// only return exit code when it is exit code from underlying process;
// we have no way to distinguish 255 from ssh failure from 255 from the command run through ssh ...
// but probably 255 is from CLI ssh
throw new SshException("exit code 255 from CLI ssh; probably failed to connect");
return result;
} catch (InterruptedException e) {
throw Exceptions.propagate(e);
} catch (IOException e) {
throw Exceptions.propagate(e);
} finally {
closeWhispering(outgobbler, contextForLogging, "execProcess");
closeWhispering(errgobbler, contextForLogging, "execProcess");
}
}
}