Skip to content

Commit bba246d

Browse files
committed
1416: use process runner to catch output
1 parent a9f5aac commit bba246d

File tree

2 files changed

+153
-27
lines changed

2 files changed

+153
-27
lines changed

lib/src/main/java/com/diffplug/spotless/ProcessRunner.java

+133-13
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package com.diffplug.spotless;
1717

18+
import static java.util.Objects.requireNonNull;
19+
1820
import java.io.ByteArrayOutputStream;
1921
import java.io.File;
2022
import java.io.IOException;
@@ -29,9 +31,12 @@
2931
import java.util.concurrent.ExecutorService;
3032
import java.util.concurrent.Executors;
3133
import java.util.concurrent.Future;
34+
import java.util.concurrent.TimeUnit;
3235
import java.util.function.BiConsumer;
3336

34-
import edu.umd.cs.findbugs.annotations.Nullable;
37+
import javax.annotation.Nonnull;
38+
import javax.annotation.Nullable;
39+
3540
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
3641

3742
/**
@@ -95,6 +100,36 @@ public Result exec(@Nullable byte[] stdin, List<String> args) throws IOException
95100

96101
/** Creates a process with the given arguments, the given byte array is written to stdin immediately. */
97102
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();
98133
ProcessBuilder builder = new ProcessBuilder(args);
99134
if (cwd != null) {
100135
builder.directory(cwd);
@@ -105,20 +140,20 @@ public Result exec(@Nullable File cwd, @Nullable Map<String, String> environment
105140
if (stdin == null) {
106141
stdin = new byte[0];
107142
}
143+
if (redirectErrorStream) {
144+
builder.redirectErrorStream(true);
145+
}
146+
108147
Process process = builder.start();
109148
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+
}
111153
// write stdin
112154
process.getOutputStream().write(stdin);
113155
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);
122157
}
123158

124159
private static void drain(InputStream input, OutputStream output) throws IOException {
@@ -141,17 +176,24 @@ public void close() {
141176
threadStdErr.shutdown();
142177
}
143178

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+
144186
@SuppressFBWarnings({"EI_EXPOSE_REP", "EI_EXPOSE_REP2"})
145187
public static class Result {
146188
private final List<String> args;
147189
private final int exitCode;
148190
private final byte[] stdOut, stdErr;
149191

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) {
151193
this.args = args;
152194
this.exitCode = exitCode;
153195
this.stdOut = stdOut;
154-
this.stdErr = stdErr;
196+
this.stdErr = (stdErr == null ? new byte[0] : stdErr);
155197
}
156198

157199
public List<String> args() {
@@ -222,8 +264,86 @@ public String toString() {
222264
}
223265
};
224266
perStream.accept(" stdout", stdOut);
225-
perStream.accept(" stderr", stdErr);
267+
if (stdErr.length > 0) {
268+
perStream.accept(" stderr", stdErr);
269+
}
226270
return builder.toString();
227271
}
228272
}
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+
}
229349
}

lib/src/main/java/com/diffplug/spotless/npm/NpmProcess.java

+20-14
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,15 @@
1919
import java.io.IOException;
2020
import java.util.ArrayList;
2121
import java.util.Arrays;
22+
import java.util.HashMap;
2223
import java.util.List;
24+
import java.util.Map;
25+
import java.util.concurrent.ExecutionException;
2326
import java.util.stream.Collectors;
2427

28+
import com.diffplug.spotless.ProcessRunner;
29+
import com.diffplug.spotless.ProcessRunner.LongRunningProcess;
30+
2531
class NpmProcess {
2632

2733
private final File workingDir;
@@ -30,10 +36,13 @@ class NpmProcess {
3036

3137
private final File nodeExecutable;
3238

39+
private final ProcessRunner processRunner;
40+
3341
NpmProcess(File workingDir, File npmExecutable, File nodeExecutable) {
3442
this.workingDir = workingDir;
3543
this.npmExecutable = npmExecutable;
3644
this.nodeExecutable = nodeExecutable;
45+
processRunner = new ProcessRunner();
3746
}
3847

3948
void install() {
@@ -44,32 +53,27 @@ void install() {
4453
"--prefer-offline");
4554
}
4655

47-
Process start() {
56+
LongRunningProcess start() {
4857
// adding --scripts-prepend-node-path=true due to https://github.com/diffplug/spotless/issues/619#issuecomment-648018679
4958
return npm("start", "--scripts-prepend-node-path=true");
5059
}
5160

5261
private void npmAwait(String... args) {
53-
final Process npmProcess = npm(args);
54-
55-
try {
62+
try (LongRunningProcess npmProcess = npm(args)) {
5663
if (npmProcess.waitFor() != 0) {
57-
throw new NpmProcessException("Running npm command '" + commandLine(args) + "' failed with exit code: " + npmProcess.exitValue());
64+
throw new NpmProcessException("Running npm command '" + commandLine(args) + "' failed with exit code: " + npmProcess.exitValue() + "\n\n" + npmProcess.result());
5865
}
5966
} catch (InterruptedException e) {
6067
throw new NpmProcessException("Running npm command '" + commandLine(args) + "' was interrupted.", e);
68+
} catch (ExecutionException e) {
69+
throw new NpmProcessException("Running npm command '" + commandLine(args) + "' failed.", e);
6170
}
6271
}
6372

64-
private Process npm(String... args) {
73+
private LongRunningProcess npm(String... args) {
6574
List<String> processCommand = processCommand(args);
6675
try {
67-
ProcessBuilder processBuilder = new ProcessBuilder()
68-
.inheritIO()
69-
.directory(this.workingDir)
70-
.command(processCommand);
71-
addEnvironmentVariables(processBuilder);
72-
return processBuilder.start();
76+
return processRunner.start(this.workingDir, environmentVariables(), null, true, processCommand);
7377
} catch (IOException e) {
7478
throw new NpmProcessException("Failed to launch npm command '" + commandLine(args) + "'.", e);
7579
}
@@ -82,8 +86,10 @@ private List<String> processCommand(String... args) {
8286
return command;
8387
}
8488

85-
private void addEnvironmentVariables(ProcessBuilder processBuilder) {
86-
processBuilder.environment().put("PATH", this.nodeExecutable.getParentFile().getAbsolutePath() + File.pathSeparator + System.getenv("PATH"));
89+
private Map<String, String> environmentVariables() {
90+
Map<String, String> environmentVariables = new HashMap<>();
91+
environmentVariables.put("PATH", this.nodeExecutable.getParentFile().getAbsolutePath() + File.pathSeparator + System.getenv("PATH"));
92+
return environmentVariables;
8793
}
8894

8995
private String commandLine(String... args) {

0 commit comments

Comments
 (0)