/*-
* Copyright © 2009 Diamond Light Source Ltd.
*
* This file is part of GDA.
*
* GDA is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License version 3 as published by the Free
* Software Foundation.
*
* GDA is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along
* with GDA. If not, see <http://www.gnu.org/licenses/>.
*/
package gda.util;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OSCommandRunner implements Serializable {
private static final Logger logger = LoggerFactory.getLogger(OSCommandRunner.class);
public final Integer exitValue;
public final Exception exception;
public final Boolean succeeded;
public final List<String> commands;
public final boolean keepOutput;
private final List<String> outputLines;
public final List<String> getOutputLines() {
return outputLines;
}
public enum LOGOPTION {
NEVER,
ALWAYS,
ONLY_ON_ERROR
}
public static void runNoWait(String command, LOGOPTION logOption, String stdInFileName) {
runNoWait(command.split("[\\s]"), logOption, stdInFileName);
}
public static void runNoWait(String[] _commands, LOGOPTION logOption, String stdInFileName) {
runNoWait(Arrays.asList(_commands), logOption, stdInFileName);
}
public static void runNoWait(final List<String> _commands, final LOGOPTION logOption, final String stdInFileName){
runNoWait(_commands, logOption, stdInFileName, null);
}
public static void runNoWait(final List<String> _commands, final LOGOPTION logOption, final String stdInFileName, ExecutorService executor) {
_runNoWait(_commands, logOption, stdInFileName, null, null, executor);
}
public static void runNoWait(final List<String> _commands, final LOGOPTION logOption, final String stdInFileName,
final Map<? extends String, ? extends String> envPutAll, final List<String> envRemove) {
_runNoWait(_commands, logOption, stdInFileName, envPutAll, envRemove, null);
}
/**
* Starts process, returns immediately.
*
* @param _commands - program path and arguments
* @param logOption - controls the logging of the output
* @param stdInFileName - if not null stdin is set to this file for the program
*/
private static void _runNoWait(final List<String> _commands, final LOGOPTION logOption, final String stdInFileName,
final Map<? extends String, ? extends String> envPutAll,
final List<String> envRemove, ExecutorService executor) {
Runnable r = new Runnable() {
@Override
public void run() {
OSCommandRunner osCommandRunner = new OSCommandRunner(_commands, logOption != LOGOPTION.NEVER,
stdInFileName, null, envPutAll, envRemove);
if (osCommandRunner.exception != null) {
String msg = "Exception seen trying to run command " + osCommandRunner.getCommandAsString();
logger.error(msg);
logger.error(osCommandRunner.exception.toString());
} else if (osCommandRunner.exitValue != 0) {
String msg = "Exit code = " + Integer.toString(osCommandRunner.exitValue)
+ " returned from command " + osCommandRunner.getCommandAsString();
logger.warn(msg);
if (logOption != LOGOPTION.NEVER) {
osCommandRunner.logOutput();
}
} else {
if (logOption == LOGOPTION.ALWAYS) {
osCommandRunner.logOutput();
}
}
}
};
if (executor != null) {
executor.submit(r);
} else {
new Thread(r, OSCommandRunner.class.getSimpleName()).start();
}
}
public OSCommandRunner(String command, boolean _keepOutput, String stdInFileName, String stdOutFileName) {
this(command.split("[\\s]"), _keepOutput, stdInFileName, stdOutFileName);
}
public OSCommandRunner(String[] _commands, boolean _keepOutput, String stdInFileName, String stdOutFileName) {
this(Arrays.asList(_commands), _keepOutput, stdInFileName, stdOutFileName);
}
public OSCommandRunner(List<String> _commands, boolean _keepOutput, Object stdInFile, String stdOutFileName) {
this(_commands, _keepOutput, stdInFile, stdOutFileName, null, null);
}
public OSCommandRunner(List<String> _commands, boolean _keepOutput, Object stdInFile, String stdOutFileName, Map<? extends String, ? extends String> envPutAll,
List<String> envRemove) {
this(_commands, _keepOutput, stdInFile, stdOutFileName, envPutAll, envRemove, null);
}
public OSCommandRunner(List<String> _commands, boolean _keepOutput, Object stdInFile, String stdOutFileName, Map<? extends String, ? extends String> envPutAll,
List<String> envRemove, String directory) {
this(_commands, _keepOutput, stdInFile, stdOutFileName, envPutAll, envRemove, directory, -1);
}
/**
* Starts process, waits for completion.
*
* @param _commands - this is the program and list of arguments
* @param _keepOutput - true if output is to be later accessed in outputLines
* @param stdInFile - if not null stdin is set to this file. Can be a string for the file path or a list of strings for the lines of the file.
* @param stdOutFileName - if not null stdout is set to this file.
*/
public OSCommandRunner(List<String> _commands, boolean _keepOutput, Object stdInFile, String stdOutFileName, Map<? extends String, ? extends String> envPutAll,
List<String> envRemove, String directory, int timeoutInMs) {
this.commands = _commands;
this.keepOutput = _keepOutput;
Integer _exitValue = 0;
Exception _exception = null;
Boolean _succeeded = false;
Vector<String> _outputLines = null;
try {
ProcessBuilder pb = new ProcessBuilder();
if( envRemove != null){
for(String key : envRemove)
pb.environment().remove(key);
}
if( envPutAll != null)
pb.environment().putAll(envPutAll);
pb.redirectErrorStream(true);
pb.command(commands);
if( directory != null && !directory.isEmpty())
pb.directory(new File(directory));
final Process p = pb.start();
if (timeoutInMs != -1) {
final ProcessKiller killer = new ProcessKiller(p, timeoutInMs);
killer.start();
}
try {
if (stdInFile != null) {
// Copy the file to the process input
final OutputStream ostream = p.getOutputStream();
final InputStream istream = stdInFile instanceof String
? new FileInputStream((String)stdInFile)
: null;
try {
final byte[] buffer = new byte[4096];
if (istream!=null) {
for (int count = 0; (count = istream.read(buffer)) >= 0;) {
ostream.write(buffer, 0, count);
}
} else {
final String [] lines = (String[])stdInFile;
for (int i = 0; i < lines.length; i++) {
ostream.write(lines[i].getBytes("UTF-8"));
ostream.write('\n');
}
}
} catch (IOException ex) {
// TODO This is actually an error - do not do nothing here!
// do nothing
}
ostream.close();
if (istream!=null) istream.close();
}
if(!keepOutput && stdOutFileName == null)
pipe(p.getInputStream(), System.out);
_exitValue = p.waitFor();
if (stdOutFileName != null) {
// Copy the file to the process input
InputStream istream = p.getInputStream();
OutputStream ostream = new FileOutputStream(stdOutFileName);
byte[] buffer = new byte[4096];
for (int count = 0; (count = istream.read(buffer)) >= 0;) {
ostream.write(buffer, 0, count);
}
ostream.close();
}
if (keepOutput) {
BufferedReader output = null;
InputStream istream = null;
try{
if (stdOutFileName != null) {
istream = new FileInputStream(stdOutFileName);
} else {
istream = p.getInputStream();
}
output = new BufferedReader(new InputStreamReader(istream));
String line;
_outputLines = new Vector<String>();
while ((line = output.readLine()) != null) {
_outputLines.add(line);
}
}finally{
if (stdOutFileName != null && istream != null) {
istream.close();
}
}
}
_succeeded = _exitValue == 0;
} catch (Exception ex) {
throw ex;
}
closeStream(p.getInputStream(), "input");
closeStream(p.getOutputStream(), "output");
closeStream(p.getErrorStream(), "error");
p.destroy();
} catch (Exception ex) {
_exception = ex;
} finally {
// TODO The exception has to be explicitly asked for currently.
// Instead it should be thrown if this is an API to processes.
exception = _exception;
exitValue = _exitValue;
succeeded = _succeeded;
if (_outputLines != null) {
outputLines = _outputLines;
} else {
outputLines = null;
}
}
}
private static void closeStream(Closeable stream, String name) {
try {
stream.close();
} catch (IOException ioe) {
logger.warn(String.format("Unable to close process %s stream", name), ioe);
}
}
private static void pipe(final InputStream src, final PrintStream dest) {
new Thread(new Runnable() {
@Override
public void run() {
try {
byte[] buffer = new byte[1024];
for (int n = 0; n != -1; n = src.read(buffer)) {
dest.write(buffer, 0, n);
}
} catch (IOException e) { // just exit
}
}
}).start();
}
public String getCommandAsString() {
String msg = "";
for (String s : commands) {
msg += " " + s;
}
return msg;
}
/**
* Print the output to the logger.
*/
public void logOutput() {
logger.info("Output from command " + getCommandAsString());
if (outputLines != null) {
for (String s : outputLines) {
logger.info(s);
}
}
}
}