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