/*
* Copyright (c) 2017 Red Hat, Inc. and/or its affiliates.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Cheng Fang - Initial API and implementation
*/
package org.jberet.support.io;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.batch.api.BatchProperty;
import javax.batch.api.Batchlet;
import javax.batch.runtime.context.StepContext;
import javax.enterprise.context.Dependent;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecuteResultHandler;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteStreamHandler;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.ShutdownHookProcessDestroyer;
import org.jberet.support._private.SupportLogger;
import org.jberet.support._private.SupportMessages;
/**
* This batchlet runs a native OS command in a sub-process asynchronously.
* Main features supported include:
* <ul>
* <li>specify command and arguments as a single line;
* <li>specify command and arguments as a comma-separated list of items, for easy handling of spaces in file paths;
* <li>custom working directory;
* <li>specify timeout as seconds so the OS commnad can timeout
* <li>the OS command process can be stopped;
* <li>passing custom environment variables;
* <li>map non-zero exit code from OS command process.
* </ul>
*/
@Named
@Dependent
public class OsCommandBatchlet implements Batchlet {
/**
* Injected {@code StepContext}.
*/
@Inject
protected StepContext stepContext;
/**
* The OS command and its arguments as a single line.
* <p>
* For example,
* <pre>
* "diff out1.txt out2.txt"
* "cp out1.txt /tmp"
* </pre>
*
* Either this property or {@link #commandArray} property must be specified.
* If both are present, this property takes precedence.
* <p>
* If a command argument contains whitespaces, the argument must be quoted
* to prevent this argument being broken into multiple arguments.
* Alternatively, {@link #commandArray} property can be used.
*
* @see #commandArray
*/
@Inject
@BatchProperty
protected String commandLine;
/**
* The OS command and its arguments as a list of string values separated by comma (,).
* <p>
* For example,
* <pre>
* "diff, out1.txt, out2.txt"
* "cp, out1.txt, /tmp"
* </pre>
*
* Either this property or {@link #commandLine} property must be specified.
* If both are present, {@link #commandLine} takes precedence.
*
* @see #commandLine
*/
@Inject
@BatchProperty
protected List<String> commandArray;
/**
* The working directory for running the OS command. Optional property, and if not set,
* it defaults to the current directory.
*/
@Inject
@BatchProperty
protected File workingDir;
/**
* A comma-separated list of int numbers that signal the successful completion of the
* OS command. Optional property, and if not set, it defaults to 0. If the OS command
* process is known to return non-zero exit code upon successful execution,
* this property must be set as such to mark the command execution as successful.
* <p>
* For example,
* <pre>
* "90, 91, 92"
* </pre>
*/
@Inject
@BatchProperty
protected int[] commandOkExitValues;
/**
* The timeout as number of seconds. After the {@code timeoutSeconds} elapses, and the
* OS command still has not finished, its process will be destroyed, and the batch
* job execution will be marked as failed.
* <p>
* Optional property, and if not set, it defaults to no timeout.
*/
@Inject
@BatchProperty
protected long timeoutSeconds;
/**
* Custom environment variables to be used when running the OS command.
* Optional property, and if not set, it defaults to using the environment
* of parent process. If set, it will represent the entire environment in
* the new process, and the parent environment will not be inherited.
* <p>
* For example,
* <pre>
* "LANG=UTF-8, TMPDIR=/tmp"
* </pre>
*/
@Inject
@BatchProperty
protected Map<String, String> environment;
/**
* Fully-qualified name of a class that implements {@code org.apache.commons.exec.ExecuteStreamHandler},
* which handles the input and output of the subprocess.
*
* @see "org.apache.commons.exec.ExecuteStreamHandler"
*/
@Inject
@BatchProperty
protected Class streamHandler;
private ExecuteWatchdog watchdog;
private volatile boolean isStopped;
/**
* {@inheritDoc}
* <p>
* This method runs the OS command.
* If the command completes successfully, its process exit code is returned.
* If there is exception while running the OS command, which may be
* caused by timeout, the command being stopped, or other errors, the process
* exit code is set as the step exit status, and the exception is thrown.
*
* @return the OS command process exit code
*
* @throws Exception upon errors
*/
@Override
public String process() throws Exception {
final DefaultExecutor executor = new DefaultExecutor();
final CommandLine commandLineObj;
if (commandLine != null) {
commandLineObj = CommandLine.parse(commandLine);
} else {
if (commandArray == null) {
throw SupportMessages.MESSAGES.invalidReaderWriterProperty(null, null, "commandArray");
} else if (commandArray.isEmpty()) {
throw SupportMessages.MESSAGES.invalidReaderWriterProperty(null, commandArray.toString(), "commandArray");
}
commandLineObj = new CommandLine(commandArray.get(0));
final int len = commandArray.size();
if (len > 1) {
for (int i = 1; i < len; i++) {
commandLineObj.addArgument(commandArray.get(i));
}
}
}
if (workingDir != null) {
executor.setWorkingDirectory(workingDir);
}
if (streamHandler != null) {
executor.setStreamHandler((ExecuteStreamHandler) streamHandler.newInstance());
}
SupportLogger.LOGGER.runCommand(commandLineObj.getExecutable(),
Arrays.toString(commandLineObj.getArguments()), executor.getWorkingDirectory().getAbsolutePath());
if (commandOkExitValues != null) {
executor.setExitValues(commandOkExitValues);
}
watchdog = new ExecuteWatchdog(timeoutSeconds > 0 ? timeoutSeconds * 1000 : ExecuteWatchdog.INFINITE_TIMEOUT);
executor.setWatchdog(watchdog);
executor.setProcessDestroyer(new ShutdownHookProcessDestroyer());
final DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler();
executor.execute(commandLineObj, environment, resultHandler);
resultHandler.waitFor();
final ExecuteException exception = resultHandler.getException();
if (exception != null) {
stepContext.setExitStatus(String.valueOf(resultHandler.getExitValue()));
if (!isStopped) {
throw exception;
} else {
SupportLogger.LOGGER.warn("", exception);
}
}
return String.valueOf(resultHandler.getExitValue());
}
/**
* {@inheritDoc}
* <p>
* This method tries to destroy the process running the OS command.
*
* @throws Exception upon errors
*/
@Override
public void stop() throws Exception {
if (watchdog != null) {
isStopped = true;
watchdog.destroyProcess();
}
}
}