package cz.cuni.mff.d3s.been.hostruntime.task;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.apache.commons.exec.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import cz.cuni.mff.d3s.been.bpk.ArtifactIdentifier;
import cz.cuni.mff.d3s.been.bpk.BpkIdentifier;
import cz.cuni.mff.d3s.been.hostruntime.TaskException;
/**
*
* @author "Tadeas Palusga"
*
*/
public class TaskProcess implements AutoCloseable {
/**
* Logger
*/
private static final Logger log = LoggerFactory.getLogger(TaskProcess.class);
/** magic constant which is used to set process timeout to infinite */
public static final long NO_TIMEOUT = ExecuteWatchdog.INFINITE_TIMEOUT;
/** process working directory */
private final Path wrkDir;
/** prepared command line for the process */
private final TaskCommandLine cmd;
/** environment variables set for the process */
private final Map<String, String> environment;
/** std/err output stream handlers for the process */
private final ExecuteStreamHandler streamHandler;
/** process timeout in seconds */
private long timeoutInMillis;
/** long time run watchdog. */
private ExecuteWatchdog watchdog;
/** all identifiers of Bpks needed by the process. */
private final Collection<BpkIdentifier> bkpDependencies;
/** All identifiers of Artifacts needed by the process. */
private final Collection<ArtifactIdentifier> artifactDependencies;
/** tells if manual shutdown has been requested */
private boolean killed;
private OutputStream stdOutOutputStream;
private OutputStream stdErrOutputStream;
/**
* Creates new task process.
*
* @param cmdLineBuilder
* process command line builder
* @param wrkDir
* working directory of the process
* @param environment
* environment variables for process to be set
* @param artifactDownloader
* Artifact downloader
* @param stdOutOutputStream
* where to send standard output
* @param stdErrOutputStream
* where to send standard error
*
* @throws TaskException
* when the task process cannot be created
*/
public TaskProcess(
CmdLineBuilder cmdLineBuilder,
Path wrkDir,
Map<String, String> environment,
OutputStream stdOutOutputStream,
OutputStream stdErrOutputStream,
DependencyDownloader artifactDownloader) throws TaskException {
this.artifactDependencies = artifactDownloader.getArtifactDependencies();
this.bkpDependencies = artifactDownloader.getBkpDependencies();
this.cmd = cmdLineBuilder.build();
this.wrkDir = wrkDir;
if (!wrkDir.toFile().exists()) {
wrkDir.toFile().mkdirs();
}
this.environment = environment;
this.stdOutOutputStream = stdOutOutputStream;
this.stdErrOutputStream = stdErrOutputStream;
this.streamHandler = new PumpStreamHandler(stdOutOutputStream, stdErrOutputStream);
this.watchdog = new ExecuteWatchdog(NO_TIMEOUT);
}
/**
* Starts process using Apache {@link Executor}.
*
* @return process exit value (throws {@link TaskException}
* //To change body of implemented methods use File | Settings | File
* Templates. on error exit values)
* @throws TaskException
* when process cannot be started from some reason or process ends
* with error exit value
*/
public int start() throws TaskException {
Executor executor = prepare();
return start(executor);
}
/**
* Prepares executor for the underlying process. Sets the working directory,
* watchdog (watches for long running time) and std/err output stream handler.
*
* @return prepared executor
*/
private Executor prepare() {
Executor executor = new DefaultExecutor();
executor.setWorkingDirectory(wrkDir.toFile());
executor.setWatchdog(watchdog);
executor.setStreamHandler(streamHandler);
// FIXME issue #84 - we should be able to set expected process exit values
return executor;
/*
* we do not care about executor.addProcessDestroyer(..) shutdown hook for
* each task should be handled manually (we want to sent some info after
* successful termination to cluster)
*/
}
/**
* Starts synchronous execution of the process.
*
* @param executor
* prepared executor to be started
* @return exit value of the process (if exit value is not expected, throws
* {@link TaskException})
* @throws TaskException
* when task process ended with error exit value or execution of
* task process failed
*/
private int start(Executor executor) throws TaskException {
try {
return executor.execute(cmd, environment);
} catch (ExecuteException e) {
if (killed) {
throw new TaskException(String.format("Task has been killed with exit value %d", e.getExitValue()), e.getExitValue());
}
if (executor.isFailure(e.getExitValue()) && watchdog.killedProcess()) {
throw new TaskException(String.format("Timeout (%d seconds) exceeded", timeoutInMillis / 1000), e.getExitValue());
}
if (executor.isFailure(e.getExitValue())) {
throw new TaskException(String.format("Task process ended with error exit value %d", e.getExitValue()), e, e.getExitValue());
}
throw new TaskException(String.format("Execution of task process failed"), e, e.getExitValue());
} catch (IOException e) {
throw new TaskException("Execution of task process failed", e);
} catch (Throwable t) {
// should not happen, but one never knows :)
throw new TaskException("Execution of task process failed for unknown reason", t);
}
}
/**
* Destroys the running process manually.
*/
public void kill() {
this.killed = true;
watchdog.destroyProcess();
}
@Override
public void close() throws Exception {
if (!this.killed) {
watchdog.destroyProcess();
}
this.streamHandler.stop();
try {
stdOutOutputStream.close();
} catch (IOException e) {
String msg = "Unable to close output stream with stdout redirection";
log.warn(msg, e);
}
try {
stdErrOutputStream.close();
} catch (IOException e) {
String msg = "Unable to close output stream with stderr redirection";
log.warn(msg, e);
}
}
/**
* Whether the task listens on debug.
*
* @return Whether the task listens on debug
*/
public boolean isDebugListeningMode() {
return cmd.isDebugListeningMode();
}
/**
* Returns tasks debug port
*
* @return tasks debug port
*/
public int getDebugPort() {
return cmd.getDebugPort();
}
/**
* Whether the process is started suspended.
*
* This is currently supported only for JVM based processes.
*
* @return whether the process is started suspended
*/
public boolean isSuspended() {
return cmd.isSuspended();
}
/**
*
* Sets timeout for the process.
*
* @param timeout
* timeout in seconds
*/
public void setTimeout(long timeout) {
timeoutInMillis = timeout <= 0 ? NO_TIMEOUT : TimeUnit.SECONDS.toMillis(timeout);
this.watchdog = new ExecuteWatchdog(timeoutInMillis);
}
/**
* Returns task arguments including executable name.
*
* @return task arguments including executable name
*/
public List<String> getArgs() {
return Arrays.asList(cmd.toStrings());
}
/**
* Returns task's working directory.
*
* @return task's working directory
*/
public String getWorkingDirectory() {
return wrkDir.toAbsolutePath().toString();
}
/**
* Returns all BPK dependencies of the process
*
* @return BPK dependencies of the process
*/
public Collection<BpkIdentifier> getBkpDependencies() {
return bkpDependencies;
}
/**
* Returns all Artifact dependencies of the process
*
* @return Artifact dependencies of the process
*/
public Collection<ArtifactIdentifier> getArtifactDependencies() {
return artifactDependencies;
}
/**
* Returns environment properties of the process
*
* @return environment properties of the process
*/
public Map<String, String> getEnvironmentProperties() {
return this.environment;
}
}