Commit | Line | Data |
---|---|---|
4bd7cc77 AM |
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 | } |