/*
* Copyright (c) 2014 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.management.backup.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
/**
* Common class to run an external process.
*/
public class ProcessRunner implements AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(ProcessRunner.class);
private Process proc;
private InputStream stdoutStream;
private InputStream stderrStream;
private OutputStream stdinStream;
private List<Thread> backgroundThreads;
public InputStream getStdOut() {
return this.stdoutStream;
}
public InputStream getStdErr() {
return this.stderrStream;
}
public OutputStream getStdIn() {
return this.stdinStream;
}
/**
* Create an instance of ProcessRunner.
*
* @param proc the process started.
* @param hasInput Whether you have anything to feed into STDIN of the child process.
* If you pass true, it's your responsibility to call .getStdIn().close()
* @throws IOException
*/
public ProcessRunner(Process proc, boolean hasInput) throws IOException {
this.proc = proc;
this.stdoutStream = proc.getInputStream();
this.stderrStream = proc.getErrorStream();
this.stdinStream = proc.getOutputStream();
if (!hasInput) {
this.stdinStream.close();
this.stdinStream = null;
}
}
public void captureAllText(InputStream input, StringBuilder capture) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String line;
while ((line = reader.readLine()) != null) {
if (capture.length() > ProcessOutputStream.ERROR_TEXT_MAX_LENGTH) {
log.warn("Current error text length {} exceeds maximum error text length {}. Discard further errors",
capture.length(), ProcessOutputStream.ERROR_TEXT_MAX_LENGTH);
return;
} else {
capture.append(line);
capture.append("\n");
}
}
}
/**
* Capture all outputs from given stream into given StringBuilder.
* This method itself is not thread-safe.
*
* @param input The input stream
* @param capture A String builder to contain all text outputs from given stream.
* If null is passed, a new StringBuilder will be created and returned.
* @return The StringBuilder passed to capture, or new StringBuilder if capture is null.
*/
public void captureAllTextInBackground(final InputStream input, final StringBuilder capture) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
captureAllText(input, capture);
} catch (IOException e) {
log.error("Error when capturing output text", e);
capture.append(e.toString());
capture.append("\n");
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw)); // NOSONAR ("squid:S1148 Suppressing sonar violation of no printStackTrace")
capture.append(sw.toString());
}
}
});
if (this.backgroundThreads == null) {
this.backgroundThreads = new ArrayList<>();
}
this.backgroundThreads.add(thread);
thread.setDaemon(true);
thread.start();
}
public Iterable<String> enumLines(final InputStream stream) {
return new Iterable<String>() {
@Override
public Iterator<String> iterator() {
return new LineIterator(stream);
}
};
}
static class LineIterator implements Iterator<String> {
BufferedReader reader;
String line;
public LineIterator(InputStream stream) {
this.reader = new BufferedReader(new InputStreamReader(stream));
pull();
}
protected void pull() {
try {
this.line = this.reader.readLine();
} catch (IOException e) {
log.error("Failed to read line from input stream", e);
}
}
@Override
public boolean hasNext() {
return this.line != null;
}
@Override
public String next() {
if (this.line == null) {
throw new NoSuchElementException();
}
String prevLine = this.line;
pull();
return prevLine;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
private static void closeStream(Closeable stream, String name) {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
log.error(String.format("Failed to close %s of child process", name), e);
}
}
}
/**
* Close the runner and free up resources (background threads and streams).
*
*/
@Override
public void close() {
closeStream(this.stdinStream, "STDIN");
this.stdinStream = null;
if (this.proc != null) {
try {
this.proc.destroy();
this.proc.waitFor();
} catch (Exception e) {
log.error("Exception when closing child process", e);
}
}
if (this.backgroundThreads != null) {
for (Thread t : this.backgroundThreads) {
try {
t.join();
} catch (Exception e) {
log.error("Failed to join thread", e);
}
}
this.backgroundThreads = null;
}
closeStream(this.stdoutStream, "STDOUT");
this.stdoutStream = null;
closeStream(this.stderrStream, "STDERR");
this.stderrStream = null;
}
// NOTE: If you're feeding STDIN in another thread, quit that thread before joining
/**
* Wait for the child process to quit by itself. Normally the child process quits when it outputs
* all data to STDOUT or consumed all input from STDIN.
* If you have passed true to constructor's hasInput parameter, be sure to call .getStdIn().close(), or
* this method usually won't return as child process is waiting for more input.
*
* @return The exit code of the child process.
* @throws IOException
* @throws InterruptedException
*/
public int join() throws IOException, InterruptedException {
// Normally the process will quit because either STDOUT or STDIN is end
this.proc.waitFor();
int exitVal = this.proc.exitValue();
this.proc = null;
return exitVal;
}
}