/*
* 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.task.system.internal;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.core.flags.TypeCoercions;
import org.apache.brooklyn.util.core.internal.ssh.ShellAbstractTool;
import org.apache.brooklyn.util.core.internal.ssh.ShellTool;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.stream.StreamGobbler;
import org.apache.brooklyn.util.stream.Streams;
import org.apache.brooklyn.util.text.Strings;
import com.google.common.base.Function;
import com.google.common.base.Throwables;
public abstract class ExecWithLoggingHelpers {
public static final ConfigKey<OutputStream> STDOUT = SshMachineLocation.STDOUT;
public static final ConfigKey<OutputStream> STDERR = SshMachineLocation.STDERR;
public static final ConfigKey<Boolean> NO_STDOUT_LOGGING = SshMachineLocation.NO_STDOUT_LOGGING;
public static final ConfigKey<Boolean> NO_STDERR_LOGGING = SshMachineLocation.NO_STDERR_LOGGING;
public static final ConfigKey<String> LOG_PREFIX = SshMachineLocation.LOG_PREFIX;
protected final String shortName;
protected Logger commandLogger = null;
public interface ExecRunner {
public int exec(ShellTool ssh, Map<String,?> flags, List<String> cmds, Map<String,?> env);
}
protected abstract <T> T execWithTool(MutableMap<String, Object> toolCreationAndConnectionProperties, Function<ShellTool, T> runMethodOnTool);
protected abstract void preExecChecks();
protected abstract String getTargetName();
protected abstract String constructDefaultLoggingPrefix(ConfigBag execFlags);
/** takes a very short name for use in blocking details, e.g. SSH or Process */
public ExecWithLoggingHelpers(String shortName) {
this.shortName = shortName;
}
public ExecWithLoggingHelpers logger(Logger commandLogger) {
this.commandLogger = commandLogger;
return this;
}
public int execScript(Map<String,?> props, String summaryForLogging, List<String> commands, Map<String,?> env) {
// TODO scriptHeader are the extra commands we expect the SshTool/ShellTool to add.
// Would be better if could get this from the ssh-tool, rather than assuming it will behave as
// we expect.
String scriptHeader = ShellAbstractTool.getOptionalVal(props, ShellTool.PROP_SCRIPT_HEADER);
return execWithLogging(props, summaryForLogging, commands, env, scriptHeader, new ExecRunner() {
@Override public int exec(ShellTool ssh, Map<String, ?> flags, List<String> cmds, Map<String, ?> env) {
return ssh.execScript(flags, cmds, env);
}});
}
protected static <T> T getOptionalVal(Map<String,?> map, ConfigKey<T> keyC) {
if (keyC==null) return null;
String key = keyC.getName();
if (map!=null && map.containsKey(key)) {
return TypeCoercions.coerce(map.get(key), keyC.getTypeToken());
} else {
return keyC.getDefaultValue();
}
}
public int execCommands(Map<String,?> props, String summaryForLogging, List<String> commands, Map<String,?> env) {
return execWithLogging(props, summaryForLogging, commands, env, new ExecRunner() {
@Override public int exec(ShellTool tool, Map<String,?> flags, List<String> cmds, Map<String,?> env) {
return tool.execCommands(flags, cmds, env);
}});
}
public int execWithLogging(Map<String,?> props, final String summaryForLogging, final List<String> commands,
final Map<String,?> env, final ExecRunner execCommand) {
return execWithLogging(props, summaryForLogging, commands, env, null, execCommand);
}
@SuppressWarnings("resource")
public int execWithLogging(Map<String,?> props, final String summaryForLogging, final List<String> commands,
final Map<String,?> env, String expectedCommandHeaders, final ExecRunner execCommand) {
if (commandLogger!=null && commandLogger.isDebugEnabled()) {
String allcmds = (Strings.isBlank(expectedCommandHeaders) ? "" : expectedCommandHeaders + " ; ") + Strings.join(commands, " ; ");
commandLogger.debug("{}, initiating "+shortName.toLowerCase()+" on machine {}{}: {}",
new Object[] {summaryForLogging, getTargetName(),
env!=null && !env.isEmpty() ? " (env "+env+")": "", allcmds});
}
if (commands.isEmpty()) {
if (commandLogger!=null && commandLogger.isDebugEnabled())
commandLogger.debug("{}, on machine {}, ending: no commands to run", summaryForLogging, getTargetName());
return 0;
}
final ConfigBag execFlags = new ConfigBag().putAll(props);
// some props get overridden in execFlags, so remove them from the tool flags
final ConfigBag toolFlags = new ConfigBag().putAll(props).removeAll(
LOG_PREFIX, STDOUT, STDERR, ShellTool.PROP_NO_EXTRA_OUTPUT);
execFlags.configure(ShellTool.PROP_SUMMARY, summaryForLogging);
PipedOutputStream outO = null;
PipedOutputStream outE = null;
StreamGobbler gO=null, gE=null;
try {
preExecChecks();
String logPrefix = execFlags.get(LOG_PREFIX);
if (logPrefix==null) logPrefix = constructDefaultLoggingPrefix(execFlags);
if (!execFlags.get(NO_STDOUT_LOGGING)) {
PipedInputStream insO = new PipedInputStream();
outO = new PipedOutputStream(insO);
String stdoutLogPrefix = "["+(logPrefix != null ? logPrefix+":stdout" : "stdout")+"] ";
gO = new StreamGobbler(insO, execFlags.get(STDOUT), commandLogger).setLogPrefix(stdoutLogPrefix);
gO.start();
execFlags.put(STDOUT, outO);
}
if (!execFlags.get(NO_STDERR_LOGGING)) {
PipedInputStream insE = new PipedInputStream();
outE = new PipedOutputStream(insE);
String stderrLogPrefix = "["+(logPrefix != null ? logPrefix+":stderr" : "stderr")+"] ";
gE = new StreamGobbler(insE, execFlags.get(STDERR), commandLogger).setLogPrefix(stderrLogPrefix);
gE.start();
execFlags.put(STDERR, outE);
}
Tasks.setBlockingDetails(shortName+" executing, "+summaryForLogging);
try {
return execWithTool(MutableMap.copyOf(toolFlags.getAllConfig()), new Function<ShellTool, Integer>() {
public Integer apply(ShellTool tool) {
int result = execCommand.exec(tool, MutableMap.copyOf(execFlags.getAllConfig()), commands, env);
if (commandLogger!=null && commandLogger.isDebugEnabled())
commandLogger.debug("{}, on machine {}, completed: return status {}",
new Object[] {summaryForLogging, getTargetName(), result});
return result;
}});
} finally {
Tasks.setBlockingDetails(null);
}
} catch (IOException e) {
if (commandLogger!=null && commandLogger.isDebugEnabled())
commandLogger.debug("{}, on machine {}, failed: {}", new Object[] {summaryForLogging, getTargetName(), e});
throw Throwables.propagate(e);
} finally {
// Must close the pipedOutStreams, otherwise input will never read -1 so StreamGobbler thread would never die
if (outO!=null) try { outO.flush(); } catch (IOException e) {}
if (outE!=null) try { outE.flush(); } catch (IOException e) {}
Streams.closeQuietly(outO);
Streams.closeQuietly(outE);
try {
if (gE!=null) { gE.join(); }
if (gO!=null) { gO.join(); }
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Throwables.propagate(e);
}
}
}
}