common: Add a ProcessUtils for external process launching
[deliverable/tracecompass.git] / common / org.eclipse.tracecompass.common.core / src / org / eclipse / tracecompass / common / core / process / ProcessUtils.java
1 /*******************************************************************************
2 * Copyright (c) 2016 EfficiOS Inc., Alexandre Montplaisir
3 *
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
8 *******************************************************************************/
9
10 package org.eclipse.tracecompass.common.core.process;
11
12 import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
13
14 import java.io.BufferedReader;
15 import java.io.IOException;
16 import java.io.InputStreamReader;
17 import java.util.LinkedList;
18 import java.util.List;
19 import java.util.stream.Collectors;
20
21 import org.eclipse.core.runtime.CoreException;
22 import org.eclipse.core.runtime.IProgressMonitor;
23 import org.eclipse.core.runtime.IStatus;
24 import org.eclipse.core.runtime.MultiStatus;
25 import org.eclipse.core.runtime.Status;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.eclipse.tracecompass.internal.common.core.Activator;
28
29 /**
30 * Common utility methods for launching external processes and retrieving their
31 * output.
32 *
33 * @author Alexandre Montplaisir
34 * @since 2.2
35 */
36 public final class ProcessUtils {
37
38 private ProcessUtils() {}
39
40 /**
41 * Simple output-getting command. Cannot be cancelled, and will return null
42 * if the external process exits with a non-zero return code.
43 *
44 * @param command
45 * The command (executable + arguments) to launch
46 * @return The process's standard output upon completion
47 */
48 public static @Nullable List<String> getOutputFromCommand(List<String> command) {
49 try {
50 ProcessBuilder builder = new ProcessBuilder(command);
51 builder.redirectErrorStream(true);
52
53 Process p = builder.start();
54 try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));) {
55 List<String> output = new LinkedList<>();
56
57 /*
58 * We must consume the output before calling Process.waitFor(),
59 * or else the buffers might fill and block the external program
60 * if there is a lot of output.
61 */
62 String line = br.readLine();
63 while (line != null) {
64 output.add(line);
65 line = br.readLine();
66 }
67
68 int ret = p.waitFor();
69 return (ret == 0 ? output : null);
70 }
71 } catch (IOException | InterruptedException e) {
72 return null;
73 }
74 }
75
76 /**
77 * Interface defining what do to with a process's output. For use with
78 * {@link #getOutputFromCommandCancellable}.
79 */
80 @FunctionalInterface
81 public static interface OutputReaderFunction {
82
83 /**
84 * Handle the output of the process. This can include reporting progress
85 * to the monitor, and pre-processing the returned output.
86 *
87 * @param reader
88 * A buffered reader to the process's standard output.
89 * Managed internally, so you do not need to
90 * {@link BufferedReader#close()} it.
91 * @param monitor
92 * The progress monitor. Implementation should check
93 * periodically if it is cancelled to end processing early.
94 * The monitor's start and end will be managed, but progress
95 * can be reported via the {@link IProgressMonitor#worked}
96 * method. The total is 1000 work units.
97 * @return The process's output
98 * @throws IOException
99 * If there was a read error. Letting throw all exception
100 * from the {@link BufferedReader} is recommended.
101 */
102 List<String> readOutput(BufferedReader reader, IProgressMonitor monitor) throws IOException;
103 }
104
105 /**
106 * Cancellable output-getting command. The processing, as well as the
107 * external process itself, can be stopped by cancelling the passed progress
108 * monitor.
109 *
110 * @param command
111 * The command (executable + arguments) to execute
112 * @param monitor
113 * The progress monitor to check for cancellation and optionally
114 * progress
115 * @param mainTaskName
116 * The main task name of the job
117 * @param readerFunction
118 * What to do with the output. See {@link OutputReaderFunction}.
119 * @return The process's standard output, upon normal completion
120 * @throws CoreException
121 * If a problem happened with the execution of the external
122 * process. It can be reported to the user with the help of an
123 * ErrorDialog.
124 */
125 public static List<String> getOutputFromCommandCancellable(List<String> command,
126 IProgressMonitor monitor,
127 String mainTaskName,
128 OutputReaderFunction readerFunction)
129 throws CoreException {
130
131 CancellableRunnable cancellerRunnable = null;
132 Thread cancellerThread = null;
133
134 try {
135 monitor.beginTask(mainTaskName, 1000);
136
137 ProcessBuilder builder = new ProcessBuilder(command);
138 builder.redirectErrorStream(false);
139
140 Process p = checkNotNull(builder.start());
141
142 cancellerRunnable = new CancellableRunnable(p, monitor);
143 cancellerThread = new Thread(cancellerRunnable);
144 cancellerThread.start();
145
146 try (BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(p.getInputStream()));) {
147
148 List<String> lines = readerFunction.readOutput(stdoutReader, monitor);
149
150 int ret = p.waitFor();
151
152 if (monitor.isCanceled()) {
153 /* We were interrupted by the canceller thread. */
154 IStatus status = new Status(IStatus.CANCEL, Activator.instance().getPluginId(), null);
155 throw new CoreException(status);
156 }
157
158 if (ret != 0) {
159 /*
160 * Something went wrong running the external process. We
161 * will gather the stderr and report it to the user.
162 */
163 BufferedReader stderrReader = new BufferedReader(new InputStreamReader(p.getErrorStream()));
164 List<String> stderrOutput = stderrReader.lines().collect(Collectors.toList());
165
166 MultiStatus status = new MultiStatus(Activator.instance().getPluginId(),
167 IStatus.ERROR, Messages.ProcessUtils_ErrorDuringExecution, null);
168 for (String str : stderrOutput) {
169 status.add(new Status(IStatus.ERROR, Activator.instance().getPluginId(), str));
170 }
171 if (stderrOutput.isEmpty()) {
172 /*
173 * At least say "no output", so an error message actually
174 * shows up.
175 */
176 status.add(new Status(IStatus.ERROR, Activator.instance().getPluginId(), Messages.ProcessUtils_ErrorNoOutput));
177 }
178 throw new CoreException(status);
179 }
180
181 return lines;
182 }
183
184 } catch (IOException | InterruptedException e) {
185 IStatus status = new Status(IStatus.ERROR, Activator.instance().getPluginId(), Messages.ProcessUtils_ExecutionInterrupted, e);
186 throw new CoreException(status);
187
188 } finally {
189 if (cancellerRunnable != null) {
190 cancellerRunnable.setFinished();
191 }
192 if (cancellerThread != null) {
193 try {
194 cancellerThread.join();
195 } catch (InterruptedException e) {
196 }
197 }
198
199 monitor.done();
200 }
201 }
202
203 /**
204 * Internal wrapper class that allows forcibly stopping a {@link Process}
205 * when its corresponding progress monitor is cancelled.
206 */
207 private static class CancellableRunnable implements Runnable {
208
209 private final Process fProcess;
210 private final IProgressMonitor fMonitor;
211
212 private boolean fIsFinished = false;
213
214 public CancellableRunnable(Process process, IProgressMonitor monitor) {
215 fProcess = process;
216 fMonitor = monitor;
217 }
218
219 public void setFinished() {
220 fIsFinished = true;
221 }
222
223 @Override
224 public void run() {
225 try {
226 while (!fIsFinished) {
227 Thread.sleep(500);
228 if (fMonitor.isCanceled()) {
229 fProcess.destroy();
230 return;
231 }
232 }
233 } catch (InterruptedException e) {
234 }
235 }
236
237 }
238 }
This page took 0.035061 seconds and 5 git commands to generate.