15
15
*/
16
16
package com .diffplug .spotless ;
17
17
18
+ import static java .util .Objects .requireNonNull ;
19
+
18
20
import java .io .ByteArrayOutputStream ;
19
21
import java .io .File ;
20
22
import java .io .IOException ;
29
31
import java .util .concurrent .ExecutorService ;
30
32
import java .util .concurrent .Executors ;
31
33
import java .util .concurrent .Future ;
34
+ import java .util .concurrent .TimeUnit ;
32
35
import java .util .function .BiConsumer ;
33
36
34
- import edu .umd .cs .findbugs .annotations .Nullable ;
37
+ import javax .annotation .Nonnull ;
38
+ import javax .annotation .Nullable ;
39
+
35
40
import edu .umd .cs .findbugs .annotations .SuppressFBWarnings ;
36
41
37
42
/**
@@ -95,6 +100,36 @@ public Result exec(@Nullable byte[] stdin, List<String> args) throws IOException
95
100
96
101
/** Creates a process with the given arguments, the given byte array is written to stdin immediately. */
97
102
public Result exec (@ Nullable File cwd , @ Nullable Map <String , String > environment , @ Nullable byte [] stdin , List <String > args ) throws IOException , InterruptedException {
103
+ LongRunningProcess process = start (cwd , environment , stdin , args );
104
+ try {
105
+ // wait for the process to finish
106
+ process .waitFor ();
107
+ // collect the output
108
+ return process .result ();
109
+ } catch (ExecutionException e ) {
110
+ throw ThrowingEx .asRuntime (e );
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Creates a process with the given arguments, the given byte array is written to stdin immediately.
116
+ * <br>
117
+ * Delegates to {@link #start(File, Map, byte[], boolean, List)} with {@code false} for {@code redirectErrorStream}.
118
+ */
119
+ public LongRunningProcess start (@ Nullable File cwd , @ Nullable Map <String , String > environment , @ Nullable byte [] stdin , List <String > args ) throws IOException {
120
+ return start (cwd , environment , stdin , false , args );
121
+ }
122
+
123
+ /**
124
+ * Creates a process with the given arguments, the given byte array is written to stdin immediately.
125
+ * <br>
126
+ * The process is not waited for, so the caller is responsible for calling {@link LongRunningProcess#waitFor()} (if needed).
127
+ * <br>
128
+ * To dispose this {@code ProcessRunner} instance, either call {@link #close()} or {@link LongRunningProcess#close()}. After
129
+ * {@link #close()} or {@link LongRunningProcess#close()} has been called, this {@code ProcessRunner} instance must not be used anymore.
130
+ */
131
+ public LongRunningProcess start (@ Nullable File cwd , @ Nullable Map <String , String > environment , @ Nullable byte [] stdin , boolean redirectErrorStream , List <String > args ) throws IOException {
132
+ checkState ();
98
133
ProcessBuilder builder = new ProcessBuilder (args );
99
134
if (cwd != null ) {
100
135
builder .directory (cwd );
@@ -105,20 +140,20 @@ public Result exec(@Nullable File cwd, @Nullable Map<String, String> environment
105
140
if (stdin == null ) {
106
141
stdin = new byte [0 ];
107
142
}
143
+ if (redirectErrorStream ) {
144
+ builder .redirectErrorStream (true );
145
+ }
146
+
108
147
Process process = builder .start ();
109
148
Future <byte []> outputFut = threadStdOut .submit (() -> drainToBytes (process .getInputStream (), bufStdOut ));
110
- Future <byte []> errorFut = threadStdErr .submit (() -> drainToBytes (process .getErrorStream (), bufStdErr ));
149
+ Future <byte []> errorFut = null ;
150
+ if (!redirectErrorStream ) {
151
+ errorFut = threadStdErr .submit (() -> drainToBytes (process .getErrorStream (), bufStdErr ));
152
+ }
111
153
// write stdin
112
154
process .getOutputStream ().write (stdin );
113
155
process .getOutputStream ().close ();
114
- // wait for the process to finish
115
- int exitCode = process .waitFor ();
116
- try {
117
- // collect the output
118
- return new Result (args , exitCode , outputFut .get (), errorFut .get ());
119
- } catch (ExecutionException e ) {
120
- throw ThrowingEx .asRuntime (e );
121
- }
156
+ return new LongRunningProcess (process , args , outputFut , errorFut );
122
157
}
123
158
124
159
private static void drain (InputStream input , OutputStream output ) throws IOException {
@@ -141,17 +176,24 @@ public void close() {
141
176
threadStdErr .shutdown ();
142
177
}
143
178
179
+ /** Checks if this {@code ProcessRunner} instance is still usable. */
180
+ private void checkState () {
181
+ if (threadStdOut .isShutdown () || threadStdErr .isShutdown ()) {
182
+ throw new IllegalStateException ("ProcessRunner has been closed and must not be used anymore." );
183
+ }
184
+ }
185
+
144
186
@ SuppressFBWarnings ({"EI_EXPOSE_REP" , "EI_EXPOSE_REP2" })
145
187
public static class Result {
146
188
private final List <String > args ;
147
189
private final int exitCode ;
148
190
private final byte [] stdOut , stdErr ;
149
191
150
- public Result (List <String > args , int exitCode , byte [] stdOut , byte [] stdErr ) {
192
+ public Result (@ Nonnull List <String > args , int exitCode , @ Nonnull byte [] stdOut , @ Nullable byte [] stdErr ) {
151
193
this .args = args ;
152
194
this .exitCode = exitCode ;
153
195
this .stdOut = stdOut ;
154
- this .stdErr = stdErr ;
196
+ this .stdErr = ( stdErr == null ? new byte [ 0 ] : stdErr ) ;
155
197
}
156
198
157
199
public List <String > args () {
@@ -222,8 +264,86 @@ public String toString() {
222
264
}
223
265
};
224
266
perStream .accept (" stdout" , stdOut );
225
- perStream .accept (" stderr" , stdErr );
267
+ if (stdErr .length > 0 ) {
268
+ perStream .accept (" stderr" , stdErr );
269
+ }
226
270
return builder .toString ();
227
271
}
228
272
}
273
+
274
+ /**
275
+ * A long-running process that can be waited for.
276
+ */
277
+ public class LongRunningProcess extends Process implements AutoCloseable {
278
+
279
+ private final Process delegate ;
280
+ private final List <String > args ;
281
+ private final Future <byte []> outputFut ;
282
+ private final Future <byte []> errorFut ;
283
+
284
+ public LongRunningProcess (@ Nonnull Process delegate , @ Nonnull List <String > args , @ Nonnull Future <byte []> outputFut , @ Nullable Future <byte []> errorFut ) {
285
+ this .delegate = requireNonNull (delegate );
286
+ this .args = args ;
287
+ this .outputFut = outputFut ;
288
+ this .errorFut = errorFut ;
289
+ }
290
+
291
+ @ Override
292
+ public OutputStream getOutputStream () {
293
+ return delegate .getOutputStream ();
294
+ }
295
+
296
+ @ Override
297
+ public InputStream getInputStream () {
298
+ return delegate .getInputStream ();
299
+ }
300
+
301
+ @ Override
302
+ public InputStream getErrorStream () {
303
+ return delegate .getErrorStream ();
304
+ }
305
+
306
+ @ Override
307
+ public int waitFor () throws InterruptedException {
308
+ return delegate .waitFor ();
309
+ }
310
+
311
+ @ Override
312
+ public boolean waitFor (long timeout , TimeUnit unit ) throws InterruptedException {
313
+ return delegate .waitFor (timeout , unit );
314
+ }
315
+
316
+ @ Override
317
+ public int exitValue () {
318
+ return delegate .exitValue ();
319
+ }
320
+
321
+ @ Override
322
+ public void destroy () {
323
+ delegate .destroy ();
324
+ }
325
+
326
+ @ Override
327
+ public Process destroyForcibly () {
328
+ return delegate .destroyForcibly ();
329
+ }
330
+
331
+ @ Override
332
+ public boolean isAlive () {
333
+ return delegate .isAlive ();
334
+ }
335
+
336
+ public Result result () throws ExecutionException , InterruptedException {
337
+ int exitCode = waitFor ();
338
+ return new Result (args , exitCode , this .outputFut .get (), (this .errorFut != null ? this .errorFut .get () : null ));
339
+ }
340
+
341
+ @ Override
342
+ public void close () {
343
+ if (isAlive ()) {
344
+ destroy ();
345
+ }
346
+ ProcessRunner .this .close ();
347
+ }
348
+ }
229
349
}
0 commit comments