/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.brooklyn.util.core.internal.ssh.process; import static org.apache.brooklyn.core.config.ConfigKeys.newConfigKey; import static org.apache.brooklyn.core.config.ConfigKeys.newStringConfigKey; 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.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.internal.ssh.ShellAbstractTool; import org.apache.brooklyn.util.core.internal.ssh.ShellTool; import org.apache.brooklyn.util.core.internal.ssh.SshException; import org.apache.brooklyn.util.core.internal.ssh.process.ProcessTool; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.os.Os; import org.apache.brooklyn.util.stream.StreamGobbler; import org.apache.brooklyn.util.text.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.common.io.ByteSource; 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<Boolean> PROP_LOGIN_SHELL = newConfigKey("loginShell", "Causes the commands to be invoked with bash arguments to forcea login shell", Boolean.FALSE); public static final ConfigKey<String> PROP_DIRECTORY = newStringConfigKey("directory", "the working directory, for executing commands", null); public ProcessTool() { this(null); } public ProcessTool(Map<String,?> flags) { super(getOptionalVal(flags, PROP_LOCAL_TEMP_DIR)); if (flags!=null) { MutableMap<String, Object> 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(final Map<String,?> props, final List<String> commands, final Map<String,?> env) { return new ToolAbstractExecScript(props) { public int run() { try { String directory = getOptionalVal(props, PROP_DIRECTORY); File directoryDir = (directory != null) ? new File(Os.tidyPath(directory)) : null; 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); ByteSource.wrap(scriptContents.getBytes()).copyTo(Files.asByteSink(to)); List<String> cmds = buildRunScriptCommand(); cmds.add(0, "chmod +x "+scriptPath); return asInt(execProcesses(cmds, null, directoryDir, out, err, separator, getOptionalVal(props, PROP_LOGIN_SHELL), this), -1); } catch (IOException e) { throw Throwables.propagate(e); } } }.run(); } @Override public int execCommands(Map<String,?> props, List<String> commands, Map<String,?> env) { if (Boolean.FALSE.equals(props.get("blocks"))) { 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); String directory = getOptionalVal(props, PROP_DIRECTORY); File directoryDir = (directory != null) ? new File(Os.tidyPath(directory)) : null; List<String> allcmds = toCommandSequence(commands, null); String singlecmd = Joiner.on(separator).join(allcmds); if (Boolean.TRUE.equals(getOptionalVal(props, PROP_RUN_AS_ROOT))) { 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, directoryDir, 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 * @deprecated since 0.7; use {@link #execProcesses(List, Map, File, OutputStream, OutputStream, String, boolean, Object)} */ @Deprecated public static int execProcesses(List<String> cmds, Map<String,?> env, OutputStream out, OutputStream err, String separator, Object contextForLogging) { return execProcesses(cmds, env, (File)null, out, err, separator, false, contextForLogging); } /** * @deprecated since 0.7; use {@link #execProcesses(List, Map, File, OutputStream, OutputStream, String, boolean, Object)} */ @Deprecated public static int execProcesses(List<String> cmds, Map<String,?> env, OutputStream out, OutputStream err, String separator, boolean asLoginShell, Object contextForLogging) { return execProcesses(cmds, env, (File)null, out, err, separator, asLoginShell, 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) * <p> * 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<String> cmds, Map<String,?> env, File directory, OutputStream out, OutputStream err, String separator, boolean asLoginShell, Object contextForLogging) { MutableList<String> commands = new MutableList<String>().append("bash"); if (asLoginShell) commands.append("-l"); commands.append("-c", Strings.join(cmds, Preconditions.checkNotNull(separator, "separator"))); return execSingleProcess(commands, env, directory, out, err, contextForLogging); } /** * @deprecated since 0.7; use {@link #execSingleProcess(List, Map, File, OutputStream, OutputStream, Object)} */ @Deprecated public static int execSingleProcess(List<String> cmdWords, Map<String,?> env, OutputStream out, OutputStream err, Object contextForLogging) { return execSingleProcess(cmdWords, env, (File)null, 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<String> cmdWords, Map<String,?> env, File directory, OutputStream out, OutputStream err, Object contextForLogging) { StreamGobbler errgobbler = null; StreamGobbler outgobbler = null; ProcessBuilder pb = new ProcessBuilder(cmdWords); if (env!=null) { for (Map.Entry<String,?> kv: env.entrySet()) pb.environment().put(kv.getKey(), String.valueOf(kv.getValue())); } if (directory != null) { pb.directory(directory); } 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"); } } }