blob: a24a80f85ffbbdc91b1327b13df257a7afa53368 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016 EfficiOS Inc., Alexandre Montplaisir
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.tracecompass.common.core.process;
import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.common.core.log.TraceCompassLog;
import org.eclipse.tracecompass.common.core.log.TraceCompassLogUtils;
import org.eclipse.tracecompass.internal.common.core.Activator;
import com.google.common.base.Charsets;
/**
* Common utility methods for launching external processes and retrieving their
* output.
*
* @author Alexandre Montplaisir
* @since 2.2
*/
public final class ProcessUtils {
private static final Logger LOGGER = TraceCompassLog.getLogger(ProcessUtils.class);
private static final int PROGRESS_DURATION = 1000;
private ProcessUtils() {}
/**
* Simple output-getting command. Cannot be cancelled, and will return null
* if the external process exits with a non-zero return code.
*
* @param command
* The command (executable + arguments) to launch
* @return The process's standard output upon completion
*/
public static @Nullable List<String> getOutputFromCommand(List<String> command) {
try (TraceCompassLogUtils.ScopeLog sl = new TraceCompassLogUtils.ScopeLog(LOGGER, Level.FINER, "ProcessUtils#getOutputFromComment", "args", command)) { //$NON-NLS-1$ //$NON-NLS-2$
ProcessBuilder builder = new ProcessBuilder(command);
builder.redirectErrorStream(true);
Process p = builder.start();
try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream(), Charsets.UTF_8));) {
List<String> output = new LinkedList<>();
/*
* We must consume the output before calling Process.waitFor(),
* or else the buffers might fill and block the external program
* if there is a lot of output.
*/
String line = br.readLine();
while (line != null) {
output.add(line);
line = br.readLine();
}
int ret = p.waitFor();
return (ret == 0 ? output : null);
}
} catch (IOException | InterruptedException e) {
return null;
}
}
/**
* Interface defining what do to with a process's output. For use with
* {@link #getOutputFromCommandCancellable}.
*/
@FunctionalInterface
public interface OutputReaderFunction {
/**
* Handle the output of the process. This can include reporting progress
* to the monitor, and pre-processing the returned output.
*
* @param reader
* A buffered reader to the process's standard output.
* Managed internally, so you do not need to
* {@link BufferedReader#close()} it.
* @param monitor
* The progress monitor. Implementation should check
* periodically if it is cancelled to end processing early.
* The monitor's start and end will be managed, but progress
* can be reported via the {@link IProgressMonitor#worked}
* method. The total is 1000 work units.
* @return The process's output
* @throws IOException
* If there was a read error. Letting throw all exception
* from the {@link BufferedReader} is recommended.
*/
List<String> readOutput(BufferedReader reader, IProgressMonitor monitor) throws IOException;
}
/**
* Cancellable output-getting command. The processing, as well as the
* external process itself, can be stopped by cancelling the passed progress
* monitor.
*
* @param command
* The command (executable + arguments) to execute
* @param monitor
* The progress monitor to check for cancellation and optionally
* progress
* @param mainTaskName
* The main task name of the job
* @param readerFunction
* What to do with the output. See {@link OutputReaderFunction}.
* @return The process's standard output, upon normal completion
* @throws CoreException
* If a problem happened with the execution of the external
* process. It can be reported to the user with the help of an
* ErrorDialog.
*/
public static List<String> getOutputFromCommandCancellable(List<String> command,
IProgressMonitor monitor,
String mainTaskName,
OutputReaderFunction readerFunction)
throws CoreException {
CancellableRunnable cancellerRunnable = null;
Thread cancellerThread = null;
try (TraceCompassLogUtils.ScopeLog sl = new TraceCompassLogUtils.ScopeLog(LOGGER, Level.FINER, "ProcessUtils#getOutputFromCommandCancellable", "MainTaskName", mainTaskName, "args", command)) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
monitor.beginTask(mainTaskName, PROGRESS_DURATION);
ProcessBuilder builder = new ProcessBuilder(command);
builder.redirectErrorStream(false);
Process p = checkNotNull(builder.start());
cancellerRunnable = new CancellableRunnable(p, monitor);
cancellerThread = new Thread(cancellerRunnable);
cancellerThread.start();
try (BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(p.getInputStream(), Charsets.UTF_8));) {
List<String> lines = readerFunction.readOutput(stdoutReader, monitor);
int ret = p.waitFor();
if (monitor.isCanceled()) {
/* We were interrupted by the canceller thread. */
IStatus status = new Status(IStatus.CANCEL, Activator.instance().getPluginId(), null);
throw new CoreException(status);
}
if (ret != 0) {
/*
* Something went wrong running the external process. We
* will gather the stderr and report it to the user.
*/
BufferedReader stderrReader = new BufferedReader(new InputStreamReader(p.getErrorStream()));
List<String> stderrOutput = stderrReader.lines().collect(Collectors.toList());
MultiStatus status = new MultiStatus(Activator.instance().getPluginId(),
IStatus.ERROR, Messages.ProcessUtils_ErrorDuringExecution, null);
for (String str : stderrOutput) {
status.add(new Status(IStatus.ERROR, Activator.instance().getPluginId(), str));
}
if (stderrOutput.isEmpty()) {
/*
* At least say "no output", so an error message
* actually shows up.
*/
status.add(new Status(IStatus.ERROR, Activator.instance().getPluginId(), Messages.ProcessUtils_ErrorNoOutput));
}
throw new CoreException(status);
}
return lines;
}
} catch (IOException | InterruptedException e) {
IStatus status = new Status(IStatus.ERROR, Activator.instance().getPluginId(), Messages.ProcessUtils_ExecutionInterrupted, e);
throw new CoreException(status);
} finally {
if (cancellerRunnable != null) {
cancellerRunnable.setFinished();
}
if (cancellerThread != null) {
try {
cancellerThread.join();
} catch (InterruptedException e) {
/*
* If it is interrupted, process is terminated.
*/
}
}
monitor.done();
}
}
/**
* Internal wrapper class that allows forcibly stopping a {@link Process}
* when its corresponding progress monitor is cancelled.
*/
private static class CancellableRunnable implements Runnable {
private static final int SLEEP_DURATION = 500;
private final Process fProcess;
private final IProgressMonitor fMonitor;
private boolean fIsFinished = false;
public CancellableRunnable(Process process, IProgressMonitor monitor) {
fProcess = process;
fMonitor = monitor;
}
public void setFinished() {
fIsFinished = true;
}
@Override
public void run() {
try {
while (!fIsFinished) {
Thread.sleep(SLEEP_DURATION);
if (fMonitor.isCanceled()) {
fProcess.destroy();
return;
}
}
} catch (InterruptedException e) {
/*
* If it is interrupted, process is terminated.
*/
}
}
}
}