Skip to content

Commit 2353037

Browse files
authored
suggest prettier plugins as appropriate (#1511)
2 parents 1095ab4 + c849de8 commit 2353037

File tree

10 files changed

+636
-31
lines changed

10 files changed

+636
-31
lines changed

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
1212
## [Unreleased]
1313
### Added
1414
* `ProcessRunner` has added some convenience methods so it can be used for maven testing. ([#1496](https://github.com/diffplug/spotless/pull/1496))
15+
* `ProcessRunner` allows to limit captured output to a certain number of bytes. ([#1511](https://github.com/diffplug/spotless/pull/1511))
16+
* `ProcessRunner` is now capable of handling long-running tasks where waiting for exit is delegated to the caller. ([#1511](https://github.com/diffplug/spotless/pull/1511))
1517
* Allow to specify node executable for node-based formatters using `nodeExecutable` parameter ([#1500](https://github.com/diffplug/spotless/pull/1500))
1618
### Fixed
1719
* The default list of type annotations used by `formatAnnotations` has had 8 more annotations from the Checker Framework added [#1494](https://github.com/diffplug/spotless/pull/1494)

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

Lines changed: 147 additions & 16 deletions
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
/**
@@ -47,10 +52,21 @@
4752
public 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
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright 2023 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless;
17+
18+
import java.io.ByteArrayOutputStream;
19+
import java.io.IOException;
20+
import java.io.OutputStream;
21+
import java.io.UnsupportedEncodingException;
22+
23+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
24+
25+
class RingBufferByteArrayOutputStream extends ByteArrayOutputStream {
26+
27+
private final int limit;
28+
29+
private int zeroIndexPointer = 0;
30+
31+
private boolean isOverLimit = false;
32+
33+
public RingBufferByteArrayOutputStream(int limit) {
34+
this(limit, 32);
35+
}
36+
37+
public RingBufferByteArrayOutputStream(int limit, int initialCapacity) {
38+
super(initialCapacity);
39+
if (limit < initialCapacity) {
40+
throw new IllegalArgumentException("Limit must be greater than initial capacity. Limit: " + limit + ", initial capacity: " + initialCapacity);
41+
}
42+
if (limit < 2) {
43+
throw new IllegalArgumentException("Limit must be greater than or equal to 2 but is " + limit);
44+
}
45+
if (limit % 2 != 0) {
46+
throw new IllegalArgumentException("Limit must be an even number but is " + limit); // to fit 16 bit unicode chars
47+
}
48+
this.limit = limit;
49+
}
50+
51+
// ---- writing
52+
@Override
53+
public synchronized void write(int b) {
54+
if (count < limit) {
55+
super.write(b);
56+
return;
57+
}
58+
isOverLimit = true;
59+
buf[zeroIndexPointer] = (byte) b;
60+
zeroIndexPointer = (zeroIndexPointer + 1) % limit;
61+
}
62+
63+
@Override
64+
public synchronized void write(byte[] b, int off, int len) {
65+
int remaining = limit - count;
66+
if (remaining >= len) {
67+
super.write(b, off, len);
68+
return;
69+
}
70+
if (remaining > 0) {
71+
// write what we can "normally"
72+
super.write(b, off, remaining);
73+
// rest delegated
74+
write(b, off + remaining, len - remaining);
75+
return;
76+
}
77+
// we are over the limit
78+
isOverLimit = true;
79+
// write till limit is reached
80+
int writeTillLimit = Math.min(len, limit - zeroIndexPointer);
81+
System.arraycopy(b, off, buf, zeroIndexPointer, writeTillLimit);
82+
zeroIndexPointer = (zeroIndexPointer + writeTillLimit) % limit;
83+
if (writeTillLimit < len) {
84+
// write rest
85+
write(b, off + writeTillLimit, len - writeTillLimit);
86+
}
87+
}
88+
89+
@Override
90+
public synchronized void reset() {
91+
super.reset();
92+
zeroIndexPointer = 0;
93+
isOverLimit = false;
94+
}
95+
96+
// ---- output
97+
@Override
98+
public synchronized void writeTo(OutputStream out) throws IOException {
99+
if (!isOverLimit) {
100+
super.writeTo(out);
101+
return;
102+
}
103+
out.write(buf, zeroIndexPointer, limit - zeroIndexPointer);
104+
out.write(buf, 0, zeroIndexPointer);
105+
}
106+
107+
@Override
108+
public synchronized byte[] toByteArray() {
109+
if (!isOverLimit) {
110+
return super.toByteArray();
111+
}
112+
byte[] result = new byte[limit];
113+
System.arraycopy(buf, zeroIndexPointer, result, 0, limit - zeroIndexPointer);
114+
System.arraycopy(buf, 0, result, limit - zeroIndexPointer, zeroIndexPointer);
115+
return result;
116+
}
117+
118+
@SuppressFBWarnings(value = "DM_DEFAULT_ENCODING", justification = "We want to use the default encoding here since this is contract on ByteArrayOutputStream")
119+
@Override
120+
public synchronized String toString() {
121+
if (!isOverLimit) {
122+
return super.toString();
123+
}
124+
return new String(buf, zeroIndexPointer, limit - zeroIndexPointer) + new String(buf, 0, zeroIndexPointer);
125+
}
126+
127+
@Override
128+
public synchronized String toString(String charsetName) throws UnsupportedEncodingException {
129+
if (!isOverLimit) {
130+
return super.toString(charsetName);
131+
}
132+
return new String(buf, zeroIndexPointer, limit - zeroIndexPointer, charsetName) + new String(buf, 0, zeroIndexPointer, charsetName);
133+
}
134+
135+
}

0 commit comments

Comments
 (0)