/*
* 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;
import java.io.ByteArrayOutputStream;
import java.util.concurrent.Callable;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.api.mgmt.TaskWrapper;
import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.core.internal.ssh.ShellTool;
import org.apache.brooklyn.util.core.task.TaskBuilder;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.core.task.system.internal.AbstractProcessTaskFactory;
import org.apache.brooklyn.util.stream.Streams;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
/** Wraps a fully constructed process task, and allows callers to inspect status.
* Note that methods in here such as {@link #getStdout()} will return partially completed streams while the task is ongoing
* (and exit code will be null). You can {@link #block()} or {@link #get()} as conveniences on the underlying {@link #getTask()}. */
public abstract class ProcessTaskWrapper<RET> extends ProcessTaskStub implements TaskWrapper<RET> {
private static final Logger log = LoggerFactory.getLogger(ProcessTaskWrapper.class);
private final Task<RET> task;
// execution details
protected ByteArrayOutputStream stdout = new ByteArrayOutputStream();
protected ByteArrayOutputStream stderr = new ByteArrayOutputStream();
protected Integer exitCode = null;
@SuppressWarnings("unchecked")
protected ProcessTaskWrapper(AbstractProcessTaskFactory<?,RET> constructor) {
super(constructor);
TaskBuilder<Object> tb = constructor.constructCustomizedTaskBuilder();
if (stdout!=null) tb.tag(BrooklynTaskTags.tagForStreamSoft(BrooklynTaskTags.STREAM_STDOUT, stdout));
if (stderr!=null) tb.tag(BrooklynTaskTags.tagForStreamSoft(BrooklynTaskTags.STREAM_STDERR, stderr));
task = (Task<RET>) tb.body(new ProcessTaskInternalJob()).build();
}
@Override
public Task<RET> asTask() {
return getTask();
}
@Override
public Task<RET> getTask() {
return task;
}
public Integer getExitCode() {
return exitCode;
}
public byte[] getStdoutBytes() {
if (stdout==null) return null;
return stdout.toByteArray();
}
public byte[] getStderrBytes() {
if (stderr==null) return null;
return stderr.toByteArray();
}
public String getStdout() {
if (stdout==null) return null;
return stdout.toString();
}
public String getStderr() {
if (stderr==null) return null;
return stderr.toString();
}
protected class ProcessTaskInternalJob implements Callable<Object> {
@Override
public Object call() throws Exception {
run( getConfigForRunning() );
for (Function<ProcessTaskWrapper<?>, Void> listener: completionListeners) {
try {
listener.apply(ProcessTaskWrapper.this);
} catch (Exception e) {
logWithDetailsAndThrow("Error in "+taskTypeShortName()+" task "+getSummary()+": "+e, e);
}
}
if (exitCode!=0 && !Boolean.FALSE.equals(requireExitCodeZero)) {
if (Boolean.TRUE.equals(requireExitCodeZero)) {
logWithDetailsAndThrow(taskTypeShortName()+" task ended with exit code "+exitCode+" when 0 was required, in "+Tasks.current()+": "+getSummary(), null);
} else {
// warn, but allow, on non-zero not explicitly allowed
log.warn(taskTypeShortName()+" task ended with exit code "+exitCode+" when non-zero was not explicitly allowed (error may be thrown in future), in "
+Tasks.current()+": "+getSummary());
}
}
switch (returnType) {
case CUSTOM: return returnResultTransformation.apply(ProcessTaskWrapper.this);
case STDOUT_STRING: return stdout.toString();
case STDOUT_BYTES: return stdout.toByteArray();
case STDERR_STRING: return stderr.toString();
case STDERR_BYTES: return stderr.toByteArray();
case EXIT_CODE: return exitCode;
}
throw new IllegalStateException("Unknown return type for "+taskTypeShortName()+" job "+getSummary()+": "+returnType);
}
protected void logWithDetailsAndThrow(String message, Throwable optionalCause) {
message = (extraErrorMessage!=null ? extraErrorMessage+": " : "") + message;
log.warn(message+" (throwing)");
logProblemDetails("STDERR", stderr, 1024);
logProblemDetails("STDOUT", stdout, 1024);
logProblemDetails("STDIN", Streams.byteArrayOfString(Strings.join(commands,"\n")), 4096);
if (optionalCause!=null) throw new IllegalStateException(message, optionalCause);
throw new IllegalStateException(message);
}
protected void logProblemDetails(String streamName, ByteArrayOutputStream stream, int max) {
Streams.logStreamTail(log, streamName+" for problem in "+Tasks.current(), stream, max);
}
}
@Override
public String toString() {
return super.toString()+"["+task+"]";
}
/** blocks and gets the result, throwing if there was an exception */
public RET get() {
return getTask().getUnchecked();
}
/** blocks until the task completes; does not throw */
public ProcessTaskWrapper<RET> block() {
getTask().blockUntilEnded();
return this;
}
/** true iff the process has completed (with or without failure) */
public boolean isDone() {
return getTask().isDone();
}
/** for overriding */
protected ConfigBag getConfigForRunning() {
ConfigBag config = ConfigBag.newInstanceCopying(ProcessTaskWrapper.this.config);
if (stdout!=null) config.put(ShellTool.PROP_OUT_STREAM, stdout);
if (stderr!=null) config.put(ShellTool.PROP_ERR_STREAM, stderr);
if (!config.containsKey(ShellTool.PROP_NO_EXTRA_OUTPUT))
// by default no extra output (so things like cat, etc work as expected)
config.put(ShellTool.PROP_NO_EXTRA_OUTPUT, true);
if (runAsRoot)
config.put(ShellTool.PROP_RUN_AS_ROOT, true);
return config;
}
protected abstract void run(ConfigBag config);
protected abstract String taskTypeShortName();
}