| /******************************************************************************* |
| * 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. |
| */ |
| } |
| } |
| |
| } |
| } |