/*******************************************************************************
* Copyright (c) 2013 Bruno Medeiros and other Contributors.
* 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:
* Bruno Medeiros - initial API and implementation
*******************************************************************************/
package melnorme.utilbox.process;
import static melnorme.utilbox.core.Assert.AssertNamespace.assertNotNull;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import melnorme.utilbox.concurrency.ICancelMonitor;
import melnorme.utilbox.concurrency.ICancelMonitor.NullCancelMonitor;
import melnorme.utilbox.concurrency.IRunnableFuture2;
import melnorme.utilbox.concurrency.OperationCancellation;
import melnorme.utilbox.core.fntypes.Result;
import melnorme.utilbox.misc.StreamUtil;
/**
* Abstract helper class to start an external process and read its output concurrently,
* using one or two reader threads (for stdout and stderr).
* It also supports waiting for process termination with timeouts.
*
* Subclasses must specify Runnable's for the worker threads reading the process stdout and stderr streams.
*/
public abstract class ExternalProcessHandler<
STDOUT_TASK extends IRunnableFuture2<? extends Result<?, IOException>>,
STDERR_TASK extends IRunnableFuture2<? extends Result<?, IOException>>
> implements IExternalProcessHandler {
protected final Process process;
protected final boolean readStdErr;
protected final ICancelMonitor cancelMonitor;
protected final STDOUT_TASK stdoutReaderTask;
protected final STDERR_TASK stderrReaderTask; // Can be null
/** This latch exists to signal that the process has terminated, and also that both reader threads
* have finished reading all input. This last aspect is very important. */
protected final CountDownLatch readersAndProcessTerminationLatch;
protected final Thread mainReaderThread;
protected final Thread stderrReaderThread; // Can be null
public ExternalProcessHandler(Process process, boolean readStdErr, boolean startReaders,
ICancelMonitor cancelMonitor) {
this.process = process;
this.readStdErr = readStdErr;
this.cancelMonitor = cancelMonitor == null ? new NullCancelMonitor() : cancelMonitor;
this.readersAndProcessTerminationLatch = new CountDownLatch(1);
this.stdoutReaderTask = assertNotNull(init_StdOutReaderTask());
this.stderrReaderTask = assertNotNull(init_StdErrReaderTask());
this.mainReaderThread = new ProcessHelperMainThread(stdoutReaderTask);
this.stderrReaderThread = init_StdErrThread(readStdErr);
if(startReaders) {
startReaderThreads();
}
}
protected abstract STDOUT_TASK init_StdOutReaderTask();
protected abstract STDERR_TASK init_StdErrReaderTask();
protected ProcessHelperStdErrThread init_StdErrThread(boolean readStdErr) {
if(readStdErr) {
return new ProcessHelperStdErrThread(stderrReaderTask);
} else {
completeStderrResult(stderrReaderTask);
assertNotNull(stderrReaderTask.getResult_forSuccessfulyCompleted());
return null;
}
}
protected abstract void completeStderrResult(STDERR_TASK stderrReaderTask);
@Override
public STDOUT_TASK getStdOutTask() {
return stdoutReaderTask;
}
@Override
public STDERR_TASK getStdErrTask() {
return stderrReaderTask;
}
public void startReaderThreads() {
mainReaderThread.start();
if(stderrReaderThread != null) {
stderrReaderThread.start();
}
}
@Override
public Process getProcess() {
return process;
}
public boolean isReadingStdErr() {
return readStdErr;
}
public boolean areReadersTerminated2() {
return stdoutReaderTask.isTerminated() && stderrReaderTask.isTerminated();
}
public boolean areReadersAndProcessTerminated() {
return readersAndProcessTerminationLatch.getCount() == 0;
}
protected String getBaseNameForWorkerThreads() {
String simpleName = getClass().getSimpleName();
if(simpleName.isEmpty()) {
return getClass().getName();
}
return simpleName;
}
protected class ProcessHelperMainThread extends Thread {
public ProcessHelperMainThread(Runnable runnable) {
super(runnable, getBaseNameForWorkerThreads() + "/StdOutReader");
setDaemon(true);
}
@Override
public void run() {
try {
super.run();
} finally {
waitForProcessIndefinitely();
readersAndProcessTerminationLatch.countDown();
mainReaderThread_Terminated();
}
}
protected void waitForProcessIndefinitely() {
while(true) {
try {
process.waitFor();
// Await stderr too:
stderrReaderTask.awaitTermination();
return;
} catch (InterruptedException e) {
// retry waitfor, we must ensure process is terminated.
}
}
}
}
/** Callback method for when main reader thread is about to terminate. Subclasses can extend. */
public void mainReaderThread_Terminated() {
}
protected class ProcessHelperStdErrThread extends Thread {
public ProcessHelperStdErrThread(Runnable runnable) {
super(runnable, getBaseNameForWorkerThreads() + "/StdErrReader");
setDaemon(true);
}
}
/*---------- Termination awaiting functionality ----------*/
protected boolean isCanceled() {
return cancelMonitor.isCancelled();
}
/**
* Await termination of process, with given timeoutMs timeout in milliseconds (-1 for no timeout).
* Periodically polls for cancellation.
* @return the process exit value.
* @throws InterruptedException if thread interrupted.
* @throws TimeoutException if timeout reached.
* @throws OperationCancellation if process reader cancellation was requested.
*/
protected void awaitReadersTermination(int timeoutMs)
throws InterruptedException, TimeoutException, OperationCancellation {
int waitedTime = 0;
while(true) {
if(isCanceled()) {
throw new OperationCancellation();
}
int cancelPollPeriodMs = getCancelPollingPeriodMs();
boolean latchSuccess = doAwaitTermination(cancelPollPeriodMs);
if(latchSuccess) {
return;
}
if(timeoutMs != NO_TIMEOUT && waitedTime >= timeoutMs) {
throw new TimeoutException();
}
waitedTime += cancelPollPeriodMs;
}
}
protected int getCancelPollingPeriodMs() {
return 200;
}
protected boolean doAwaitTermination(int cancelPollPeriodMs) throws InterruptedException {
return readersAndProcessTerminationLatch.await(cancelPollPeriodMs, TimeUnit.MILLISECONDS);
}
@Override
public void awaitTermination(int timeoutMs, boolean destroyOnError)
throws InterruptedException, TimeoutException, OperationCancellation, IOException {
try {
awaitReadersTermination(timeoutMs);
// Check for IOExceptions (although I'm not sure this scenario is possible)
stdoutReaderTask.awaitResult2().get();
stderrReaderTask.awaitResult2().get();
} catch(Exception e) {
if(destroyOnError) {
destroyProcess();
}
throw e;
}
}
protected void destroyProcess() {
process.destroy();
}
/* ----------------- writing helpers ----------------- */
@Override
public void writeInput(String input, Charset charset) throws IOException {
if(input == null)
return;
OutputStream processInputStream = getProcess().getOutputStream();
StreamUtil.writeStringToStream(input, processInputStream, charset);
}
}