/** * VMware Continuent Tungsten Replicator * Copyright (C) 2015 VMware, Inc. All rights reserved. * * Licensed 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. * * Initial developer(s): Robert Hodges * Contributor(s): Linas Virbalas */ package com.continuent.tungsten.common.exec; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.StringReader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import org.apache.log4j.Logger; /** * Wrapper class to encapsulate all aspects of executing native operating system * commands which are typically characterized . The wrapper handles up-front * collection of inputs such as setting the working directory, environment, and * stdin. It also manages the collection output from stdout and stderr. * <p> * Here is an example of typical usage: * <p> * <code><pre> * ProcessExecutor pe = new ProcessExecutor(); * pe.setCommands(new String[] {"echo", "hi!"}); * pe.setTimeout(1000); * pe.setEnv("myenvvar", "myvalue"); * pe.run(); * </pre></code> After the run() method completes it is safe for clients to * examine output values. The class is implemented as runnable to permit callers * to run it easily in another thread. * * @author <a href="mailto:robert.hodges@continuent.com">Robert Hodges</a> * @version 1.0 */ public class ProcessExecutor implements Runnable { private static final Logger logger = Logger .getLogger(ProcessExecutor.class); /** * Maximum milliseconds to wait for output threads to complete after process * completion. */ protected static final int THREAD_WAIT_MILLIS = 30 * 1000; // Input values. protected HashMap<String, String> env = new HashMap<String, String>(); protected InputStream stdin; protected File workDirectory; protected String[] commands; protected int timeout; // Output values. protected boolean stdOutAppend; protected boolean stdErrAppend; protected File stdOutFile; protected File stdErrFile; protected String stdout; protected String stderr; protected Logger stdOutLogger; protected Logger stdErrLogger; protected Throwable error; protected int exitValue; protected boolean timedout; protected boolean succeeded; protected Process process = null; protected boolean redirectStdErr = false; /** * Creates a new instance. */ public ProcessExecutor() { } /** * Creates a new instance. */ public ProcessExecutor(boolean redirectStdErr) { this.redirectStdErr = true; } /** Returns the program and command line argumetns. */ public String[] getCommands() { return commands; } /** Sets the program and command line arguments. */ public void setCommands(String[] commands) { this.commands = commands; } /** Sets an environmental variable. */ public void setEnv(String name, String value) { env.put(name, value); } /** Returns the table of environmental variables. */ public HashMap<String, String> getEnv() { return env; } /** Sets environmental variables to be used by this command. */ public void setEnv(HashMap<String, String> env) { this.env = env; } /** Returns the inputstream fed to the process. */ public InputStream getStdin() { return stdin; } /** * Sets the input stream fed to the process. The input stream is * automatically closed at the end of the command. Null is the default value * and means there is no stdin for this process. */ public void setStdin(InputStream stdin) { this.stdin = stdin; } /** * Send stdout to a file * * @param stdOutFile a file for stdout to be stored to */ public void setStdOut(File stdOutFile) { this.stdOutFile = stdOutFile; } /** * Send stdout to a logger. * * @param stdOutLogger an initialized Logger for stdout to be appended to. */ public void setStdOut(Logger stdOutLogger) { this.stdOutLogger = stdOutLogger; } /** * Send stderr to a file * * @param stdErrFile a file for stderr to be stored to */ public void setStdErr(File stdErrFile) { this.stdErrFile = stdErrFile; } /** * Send stderr to a logger. * * @param stdErrLogger an initialized Logger for stderr to be appended to. */ public void setStdErr(Logger stdErrLogger) { this.stdErrLogger = stdErrLogger; } /** * Returns true if stdout should append to existing file. */ public boolean isStdOutAppend() { return stdOutAppend; } /** * If true, append stdout to existing file. Otherwise, * stdout overwrites the file if it exists. */ public void setStdOutAppend(boolean appendStdout) { this.stdOutAppend = appendStdout; } /** * Returns true if stderr should append to existing file. */ public boolean isStdErrAppend() { return stdErrAppend; } /** * If true, append stderr to existing file. Otherwise, * stderr overwrites the file if it exists. */ public void setStdErrAppend(boolean appendStderr) { this.stdErrAppend = appendStderr; } /** * Returns the process timeout in milliseconds. */ public int getTimeout() { return timeout; } /** * Sets the process timeout in milliseconds. We consider the process failed * if it does not terminate within this time. A value of 0 is the default * and means to wait indefinitely. */ public void setTimeout(int timeout) { this.timeout = timeout; } /** * Returns the process working directory. */ public File getWorkDirectory() { return workDirectory; } /** * Sets the process working direcgtory. Null is the default and means to run * in the current directory of the Java process that launches this command. */ public void setWorkDirectory(File workDirectory) { this.workDirectory = workDirectory; } /** * Returns the exception, if any, generated while executing the command. */ public Throwable getError() { return error; } /** * Returns the exit value of the command or -1 if the command failed to * execute. */ public int getExitValue() { return exitValue; } /** * Returns a String containing stderr output. An empty string means there is * no output. */ public String getStderr() { return stderr; } /** * Returns stderr in the form of a String array where each string contains * one line of output. */ public List<String> getStderrByLine() { return toStringList(stderr); } /** * Returns a String containing stdout. An empty string means there is no * output. */ public String getStdout() { return stdout; } /** * Returns the stdout in the form of a String array where each string * contains one line of output. */ public List<String> getStdoutByLine() { return toStringList(stdout); } /** * Returns true if this processed exceeded its timeout and was killed. */ public boolean isTimedout() { return timedout; } /** * Returns true if we think the process succeeded based on the return code * and lack of exceptions or timeout during execution. */ public boolean isSuccessful() { return succeeded; } /** * Execute the command. When this method returns it is safe for callers to * read output values. */ public void run() { // Clear output values. stdout = ""; stderr = ""; exitValue = -1; error = null; timedout = false; succeeded = false; // Construct and run the process. try { // Create a process instance. ProcessBuilder pb = new ProcessBuilder(commands); Map<String, String> localEnv = pb.environment(); for (String key : env.keySet()) { localEnv.put(key, env.get(key)); } pb.directory(workDirectory); pb.redirectErrorStream(redirectStdErr); process = pb.start(); exitValue = handleProcessIO(process); } catch (InterruptedException e) { logger.warn("Command timed out: command=" + commandsToString(commands) + " timeout=" + timeout); timedout = true; } catch (IOException e) { logger.warn("Command failed with I/O error: command=" + commandsToString(commands)); logger.debug("Command I/O exception: " + e); error = e; } catch (Throwable e) { logger.warn("Command failed with unexpected exception: command=" + commandsToString(commands), e); error = e; } // Figure out whether we succeeded or failed. if (this.error != null) succeeded = false; else if (this.exitValue != 0) succeeded = false; else if (this.timedout == true) succeeded = false; else succeeded = true; } // Concatenates an array of strings into a single space-separated string. private String commandsToString(String[] commands) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < commands.length; i++) { if (i > 0) sb.append(" "); sb.append(commands[i]); } return sb.toString(); } /** * Manage process input and output. This is messy so we put it in a separate * routine. */ protected int handleProcessIO(Process process) throws InterruptedException { if (logger.isDebugEnabled()) { logger.debug("Starting execution of \"" + commandsToString(commands) + "\""); } // Manage command execution. InputStreamSink stdoutProcessor = null; InputStreamSink stderrProcessor = null; TimerTask task = null; try { // Use threads to capture process output. stdoutProcessor = getInputSink("stdout", process.getInputStream(), stdOutFile, stdOutAppend, stdOutLogger); stderrProcessor = getInputSink("stderr", process.getErrorStream(), stdErrFile, stdErrAppend, stdErrLogger); Thread stdoutThread = new Thread(stdoutProcessor); Thread stderrThread = new Thread(stderrProcessor); stdoutThread.start(); stderrThread.start(); // Schedule the timer if we need a timeout. if (timeout > 0) { final Thread t = Thread.currentThread(); task = new TimerTask() { public void run() { t.interrupt(); } }; Timer timer = new Timer(); timer.schedule(task, timeout); } // Copy in standard input if present. if (stdin != null) { OutputStream processStdin = process.getOutputStream(); try { // Write output from stream. byte[] buff = new byte[1024]; int len = 0; while ((len = stdin.read(buff)) != -1) { processStdin.write(buff, 0, len); } } catch (IOException e) { logger.warn("Writing of data to stdin halted by exception", e); } finally { try { stdin.close(); } catch (IOException e) { logger .warn( "Input stdin close operation generated exception", e); } try { processStdin.close(); } catch (IOException e) { logger .warn( "Process stdin close operation generated exception", e); } } } // Wait for process to complete. process.waitFor(); // Wait for threads to complete. Not strictly necessary // but makes it more likely we will read output properly. stdoutThread.join(THREAD_WAIT_MILLIS); stderrThread.join(THREAD_WAIT_MILLIS); } catch (FileNotFoundException e) { logger.debug("Unable to open file: " + e.getMessage(), e); throw new ProcessRuntimeException("Unable to open file " + e.getMessage(), e); } catch (InterruptedException e) { logger.warn("Command exceeded timeout: " + commandsToString(commands)); process.destroy(); throw e; } finally { // Cancel the timer and cleanup process stdin. if (task != null) task.cancel(); // Collect output--this needs to happen no matter what. stderr = stderrProcessor.getOutput(); stdout = stdoutProcessor.getOutput(); } if (logger.isDebugEnabled()) { logger.debug("Command \"" + commandsToString(commands) + "\" " + "terminated with exitcode " + process.exitValue()); } return process.exitValue(); } // Utility routine to allocate a proper input stream sink based on whether // input is capture in memory of within a file. private InputStreamSink getInputSink(String tag, InputStream inputStream, File outFile, boolean append, Logger outLogger) throws FileNotFoundException { if (outFile != null) return new FileInputStreamSink(tag, inputStream, outFile, append); else if (outLogger != null) return new LoggerInputStreamSink(tag, inputStream, outLogger); else return new StringInputStreamSink(tag, inputStream, 0); } // Utility routine to turn output in string form into a list where each // string represents a single line. public static List<String> toStringList(String output) { BufferedReader br = new BufferedReader(new StringReader(output)); ArrayList<String> list = new ArrayList<String>(); String line; try { while ((line = br.readLine()) != null) { list.add(line); } } catch (IOException e) { // This should not happen if we are reading from a String. logger .warn("Converting string output to list resulted in error", e); } return list; } /** * Returns the process value. * * @return Returns the process. */ public Process getProcess() { return process; } }