1515 */
1616package com .diffplug .spotless ;
1717
18+ import static java .util .Objects .requireNonNull ;
19+
1820import java .io .ByteArrayOutputStream ;
1921import java .io .File ;
2022import java .io .IOException ;
2931import java .util .concurrent .ExecutorService ;
3032import java .util .concurrent .Executors ;
3133import java .util .concurrent .Future ;
34+ import java .util .concurrent .TimeUnit ;
3235import java .util .function .BiConsumer ;
3336
34- import edu .umd .cs .findbugs .annotations .Nullable ;
37+ import javax .annotation .Nonnull ;
38+ import javax .annotation .Nullable ;
39+
3540import edu .umd .cs .findbugs .annotations .SuppressFBWarnings ;
3641
3742/**
4752public class ProcessRunner implements AutoCloseable {
4853 private final ExecutorService threadStdOut = Executors .newSingleThreadExecutor ();
4954 private final ExecutorService threadStdErr = Executors .newSingleThreadExecutor ();
50- private final ByteArrayOutputStream bufStdOut = new ByteArrayOutputStream () ;
51- private final ByteArrayOutputStream bufStdErr = new ByteArrayOutputStream () ;
55+ private final ByteArrayOutputStream bufStdOut ;
56+ private final ByteArrayOutputStream bufStdErr ;
5257
53- public ProcessRunner () {}
58+ public ProcessRunner () {
59+ this (-1 );
60+ }
61+
62+ public static ProcessRunner usingRingBuffersOfCapacity (int limit ) {
63+ return new ProcessRunner (limit );
64+ }
65+
66+ private ProcessRunner (int limitedBuffers ) {
67+ this .bufStdOut = limitedBuffers >= 0 ? new RingBufferByteArrayOutputStream (limitedBuffers ) : new ByteArrayOutputStream ();
68+ this .bufStdErr = limitedBuffers >= 0 ? new RingBufferByteArrayOutputStream (limitedBuffers ) : new ByteArrayOutputStream ();
69+ }
5470
5571 /** Executes the given shell command (using {@code cmd} on windows and {@code sh} on unix). */
5672 public Result shell (String cmd ) throws IOException , InterruptedException {
@@ -95,6 +111,36 @@ public Result exec(@Nullable byte[] stdin, List<String> args) throws IOException
95111
96112 /** Creates a process with the given arguments, the given byte array is written to stdin immediately. */
97113 public Result exec (@ Nullable File cwd , @ Nullable Map <String , String > environment , @ Nullable byte [] stdin , List <String > args ) throws IOException , InterruptedException {
114+ LongRunningProcess process = start (cwd , environment , stdin , args );
115+ try {
116+ // wait for the process to finish
117+ process .waitFor ();
118+ // collect the output
119+ return process .result ();
120+ } catch (ExecutionException e ) {
121+ throw ThrowingEx .asRuntime (e );
122+ }
123+ }
124+
125+ /**
126+ * Creates a process with the given arguments, the given byte array is written to stdin immediately.
127+ * <br>
128+ * Delegates to {@link #start(File, Map, byte[], boolean, List)} with {@code false} for {@code redirectErrorStream}.
129+ */
130+ public LongRunningProcess start (@ Nullable File cwd , @ Nullable Map <String , String > environment , @ Nullable byte [] stdin , List <String > args ) throws IOException {
131+ return start (cwd , environment , stdin , false , args );
132+ }
133+
134+ /**
135+ * Creates a process with the given arguments, the given byte array is written to stdin immediately.
136+ * <br>
137+ * The process is not waited for, so the caller is responsible for calling {@link LongRunningProcess#waitFor()} (if needed).
138+ * <br>
139+ * To dispose this {@code ProcessRunner} instance, either call {@link #close()} or {@link LongRunningProcess#close()}. After
140+ * {@link #close()} or {@link LongRunningProcess#close()} has been called, this {@code ProcessRunner} instance must not be used anymore.
141+ */
142+ public LongRunningProcess start (@ Nullable File cwd , @ Nullable Map <String , String > environment , @ Nullable byte [] stdin , boolean redirectErrorStream , List <String > args ) throws IOException {
143+ checkState ();
98144 ProcessBuilder builder = new ProcessBuilder (args );
99145 if (cwd != null ) {
100146 builder .directory (cwd );
@@ -105,20 +151,20 @@ public Result exec(@Nullable File cwd, @Nullable Map<String, String> environment
105151 if (stdin == null ) {
106152 stdin = new byte [0 ];
107153 }
154+ if (redirectErrorStream ) {
155+ builder .redirectErrorStream (true );
156+ }
157+
108158 Process process = builder .start ();
109159 Future <byte []> outputFut = threadStdOut .submit (() -> drainToBytes (process .getInputStream (), bufStdOut ));
110- Future <byte []> errorFut = threadStdErr .submit (() -> drainToBytes (process .getErrorStream (), bufStdErr ));
160+ Future <byte []> errorFut = null ;
161+ if (!redirectErrorStream ) {
162+ errorFut = threadStdErr .submit (() -> drainToBytes (process .getErrorStream (), bufStdErr ));
163+ }
111164 // write stdin
112165 process .getOutputStream ().write (stdin );
113166 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- }
167+ return new LongRunningProcess (process , args , outputFut , errorFut );
122168 }
123169
124170 private static void drain (InputStream input , OutputStream output ) throws IOException {
@@ -141,17 +187,24 @@ public void close() {
141187 threadStdErr .shutdown ();
142188 }
143189
190+ /** Checks if this {@code ProcessRunner} instance is still usable. */
191+ private void checkState () {
192+ if (threadStdOut .isShutdown () || threadStdErr .isShutdown ()) {
193+ throw new IllegalStateException ("ProcessRunner has been closed and must not be used anymore." );
194+ }
195+ }
196+
144197 @ SuppressFBWarnings ({"EI_EXPOSE_REP" , "EI_EXPOSE_REP2" })
145198 public static class Result {
146199 private final List <String > args ;
147200 private final int exitCode ;
148201 private final byte [] stdOut , stdErr ;
149202
150- public Result (List <String > args , int exitCode , byte [] stdOut , byte [] stdErr ) {
203+ public Result (@ Nonnull List <String > args , int exitCode , @ Nonnull byte [] stdOut , @ Nullable byte [] stdErr ) {
151204 this .args = args ;
152205 this .exitCode = exitCode ;
153206 this .stdOut = stdOut ;
154- this .stdErr = stdErr ;
207+ this .stdErr = ( stdErr == null ? new byte [ 0 ] : stdErr ) ;
155208 }
156209
157210 public List <String > args () {
@@ -222,8 +275,86 @@ public String toString() {
222275 }
223276 };
224277 perStream .accept (" stdout" , stdOut );
225- perStream .accept (" stderr" , stdErr );
278+ if (stdErr .length > 0 ) {
279+ perStream .accept (" stderr" , stdErr );
280+ }
226281 return builder .toString ();
227282 }
228283 }
284+
285+ /**
286+ * A long-running process that can be waited for.
287+ */
288+ public class LongRunningProcess extends Process implements AutoCloseable {
289+
290+ private final Process delegate ;
291+ private final List <String > args ;
292+ private final Future <byte []> outputFut ;
293+ private final Future <byte []> errorFut ;
294+
295+ public LongRunningProcess (@ Nonnull Process delegate , @ Nonnull List <String > args , @ Nonnull Future <byte []> outputFut , @ Nullable Future <byte []> errorFut ) {
296+ this .delegate = requireNonNull (delegate );
297+ this .args = args ;
298+ this .outputFut = outputFut ;
299+ this .errorFut = errorFut ;
300+ }
301+
302+ @ Override
303+ public OutputStream getOutputStream () {
304+ return delegate .getOutputStream ();
305+ }
306+
307+ @ Override
308+ public InputStream getInputStream () {
309+ return delegate .getInputStream ();
310+ }
311+
312+ @ Override
313+ public InputStream getErrorStream () {
314+ return delegate .getErrorStream ();
315+ }
316+
317+ @ Override
318+ public int waitFor () throws InterruptedException {
319+ return delegate .waitFor ();
320+ }
321+
322+ @ Override
323+ public boolean waitFor (long timeout , TimeUnit unit ) throws InterruptedException {
324+ return delegate .waitFor (timeout , unit );
325+ }
326+
327+ @ Override
328+ public int exitValue () {
329+ return delegate .exitValue ();
330+ }
331+
332+ @ Override
333+ public void destroy () {
334+ delegate .destroy ();
335+ }
336+
337+ @ Override
338+ public Process destroyForcibly () {
339+ return delegate .destroyForcibly ();
340+ }
341+
342+ @ Override
343+ public boolean isAlive () {
344+ return delegate .isAlive ();
345+ }
346+
347+ public Result result () throws ExecutionException , InterruptedException {
348+ int exitCode = waitFor ();
349+ return new Result (args , exitCode , this .outputFut .get (), (this .errorFut != null ? this .errorFut .get () : null ));
350+ }
351+
352+ @ Override
353+ public void close () {
354+ if (isAlive ()) {
355+ destroy ();
356+ }
357+ ProcessRunner .this .close ();
358+ }
359+ }
229360}
0 commit comments