Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

suggest prettier plugins as appropriate #1511

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
### Added
* `ProcessRunner` has added some convenience methods so it can be used for maven testing. ([#1496](https://github.com/diffplug/spotless/pull/1496))
* `ProcessRunner` allows to limit captured output to a certain number of bytes. ([#1511](https://github.com/diffplug/spotless/pull/1511))
* `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))
* Allow to specify node executable for node-based formatters using `nodeExecutable` parameter ([#1500](https://github.com/diffplug/spotless/pull/1500))
### Fixed
* 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)
Expand Down
163 changes: 147 additions & 16 deletions lib/src/main/java/com/diffplug/spotless/ProcessRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package com.diffplug.spotless;

import static java.util.Objects.requireNonNull;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
Expand All @@ -29,9 +31,12 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;

import edu.umd.cs.findbugs.annotations.Nullable;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
Expand All @@ -47,10 +52,21 @@
public class ProcessRunner implements AutoCloseable {
private final ExecutorService threadStdOut = Executors.newSingleThreadExecutor();
private final ExecutorService threadStdErr = Executors.newSingleThreadExecutor();
private final ByteArrayOutputStream bufStdOut = new ByteArrayOutputStream();
private final ByteArrayOutputStream bufStdErr = new ByteArrayOutputStream();
private final ByteArrayOutputStream bufStdOut;
private final ByteArrayOutputStream bufStdErr;

public ProcessRunner() {}
public ProcessRunner() {
this(-1);
}

public static ProcessRunner usingRingBuffersOfCapacity(int limit) {
return new ProcessRunner(limit);
}

private ProcessRunner(int limitedBuffers) {
this.bufStdOut = limitedBuffers >= 0 ? new RingBufferByteArrayOutputStream(limitedBuffers) : new ByteArrayOutputStream();
this.bufStdErr = limitedBuffers >= 0 ? new RingBufferByteArrayOutputStream(limitedBuffers) : new ByteArrayOutputStream();
}

/** Executes the given shell command (using {@code cmd} on windows and {@code sh} on unix). */
public Result shell(String cmd) throws IOException, InterruptedException {
Expand Down Expand Up @@ -95,6 +111,36 @@ public Result exec(@Nullable byte[] stdin, List<String> args) throws IOException

/** Creates a process with the given arguments, the given byte array is written to stdin immediately. */
public Result exec(@Nullable File cwd, @Nullable Map<String, String> environment, @Nullable byte[] stdin, List<String> args) throws IOException, InterruptedException {
LongRunningProcess process = start(cwd, environment, stdin, args);
try {
// wait for the process to finish
process.waitFor();
// collect the output
return process.result();
} catch (ExecutionException e) {
throw ThrowingEx.asRuntime(e);
}
}

/**
* Creates a process with the given arguments, the given byte array is written to stdin immediately.
* <br>
* Delegates to {@link #start(File, Map, byte[], boolean, List)} with {@code false} for {@code redirectErrorStream}.
*/
public LongRunningProcess start(@Nullable File cwd, @Nullable Map<String, String> environment, @Nullable byte[] stdin, List<String> args) throws IOException {
return start(cwd, environment, stdin, false, args);
}

/**
* Creates a process with the given arguments, the given byte array is written to stdin immediately.
* <br>
* The process is not waited for, so the caller is responsible for calling {@link LongRunningProcess#waitFor()} (if needed).
* <br>
* To dispose this {@code ProcessRunner} instance, either call {@link #close()} or {@link LongRunningProcess#close()}. After
* {@link #close()} or {@link LongRunningProcess#close()} has been called, this {@code ProcessRunner} instance must not be used anymore.
*/
public LongRunningProcess start(@Nullable File cwd, @Nullable Map<String, String> environment, @Nullable byte[] stdin, boolean redirectErrorStream, List<String> args) throws IOException {
checkState();
ProcessBuilder builder = new ProcessBuilder(args);
if (cwd != null) {
builder.directory(cwd);
Expand All @@ -105,20 +151,20 @@ public Result exec(@Nullable File cwd, @Nullable Map<String, String> environment
if (stdin == null) {
stdin = new byte[0];
}
if (redirectErrorStream) {
builder.redirectErrorStream(true);
}

Process process = builder.start();
Future<byte[]> outputFut = threadStdOut.submit(() -> drainToBytes(process.getInputStream(), bufStdOut));
Future<byte[]> errorFut = threadStdErr.submit(() -> drainToBytes(process.getErrorStream(), bufStdErr));
Future<byte[]> errorFut = null;
if (!redirectErrorStream) {
errorFut = threadStdErr.submit(() -> drainToBytes(process.getErrorStream(), bufStdErr));
}
// write stdin
process.getOutputStream().write(stdin);
process.getOutputStream().close();
// wait for the process to finish
int exitCode = process.waitFor();
try {
// collect the output
return new Result(args, exitCode, outputFut.get(), errorFut.get());
} catch (ExecutionException e) {
throw ThrowingEx.asRuntime(e);
}
return new LongRunningProcess(process, args, outputFut, errorFut);
}

private static void drain(InputStream input, OutputStream output) throws IOException {
Expand All @@ -141,17 +187,24 @@ public void close() {
threadStdErr.shutdown();
}

/** Checks if this {@code ProcessRunner} instance is still usable. */
private void checkState() {
if (threadStdOut.isShutdown() || threadStdErr.isShutdown()) {
throw new IllegalStateException("ProcessRunner has been closed and must not be used anymore.");
}
}

@SuppressFBWarnings({"EI_EXPOSE_REP", "EI_EXPOSE_REP2"})
public static class Result {
private final List<String> args;
private final int exitCode;
private final byte[] stdOut, stdErr;

public Result(List<String> args, int exitCode, byte[] stdOut, byte[] stdErr) {
public Result(@Nonnull List<String> args, int exitCode, @Nonnull byte[] stdOut, @Nullable byte[] stdErr) {
this.args = args;
this.exitCode = exitCode;
this.stdOut = stdOut;
this.stdErr = stdErr;
this.stdErr = (stdErr == null ? new byte[0] : stdErr);
}

public List<String> args() {
Expand Down Expand Up @@ -222,8 +275,86 @@ public String toString() {
}
};
perStream.accept(" stdout", stdOut);
perStream.accept(" stderr", stdErr);
if (stdErr.length > 0) {
perStream.accept(" stderr", stdErr);
}
return builder.toString();
}
}

/**
* A long-running process that can be waited for.
*/
public class LongRunningProcess extends Process implements AutoCloseable {

private final Process delegate;
private final List<String> args;
private final Future<byte[]> outputFut;
private final Future<byte[]> errorFut;

public LongRunningProcess(@Nonnull Process delegate, @Nonnull List<String> args, @Nonnull Future<byte[]> outputFut, @Nullable Future<byte[]> errorFut) {
this.delegate = requireNonNull(delegate);
this.args = args;
this.outputFut = outputFut;
this.errorFut = errorFut;
}

@Override
public OutputStream getOutputStream() {
return delegate.getOutputStream();
}

@Override
public InputStream getInputStream() {
return delegate.getInputStream();
}

@Override
public InputStream getErrorStream() {
return delegate.getErrorStream();
}

@Override
public int waitFor() throws InterruptedException {
return delegate.waitFor();
}

@Override
public boolean waitFor(long timeout, TimeUnit unit) throws InterruptedException {
return delegate.waitFor(timeout, unit);
}

@Override
public int exitValue() {
return delegate.exitValue();
}

@Override
public void destroy() {
delegate.destroy();
}

@Override
public Process destroyForcibly() {
return delegate.destroyForcibly();
}

@Override
public boolean isAlive() {
return delegate.isAlive();
}

public Result result() throws ExecutionException, InterruptedException {
int exitCode = waitFor();
return new Result(args, exitCode, this.outputFut.get(), (this.errorFut != null ? this.errorFut.get() : null));
}

@Override
public void close() {
if (isAlive()) {
destroy();
}
ProcessRunner.this.close();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright 2023 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.spotless;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

class RingBufferByteArrayOutputStream extends ByteArrayOutputStream {

private final int limit;

private int zeroIndexPointer = 0;

private boolean isOverLimit = false;

public RingBufferByteArrayOutputStream(int limit) {
this(limit, 32);
}

public RingBufferByteArrayOutputStream(int limit, int initialCapacity) {
super(initialCapacity);
if (limit < initialCapacity) {
throw new IllegalArgumentException("Limit must be greater than initial capacity. Limit: " + limit + ", initial capacity: " + initialCapacity);
}
if (limit < 2) {
throw new IllegalArgumentException("Limit must be greater than or equal to 2 but is " + limit);
}
if (limit % 2 != 0) {
throw new IllegalArgumentException("Limit must be an even number but is " + limit); // to fit 16 bit unicode chars
}
this.limit = limit;
}

// ---- writing
@Override
public synchronized void write(int b) {
if (count < limit) {
super.write(b);
return;
}
isOverLimit = true;
buf[zeroIndexPointer] = (byte) b;
zeroIndexPointer = (zeroIndexPointer + 1) % limit;
}

@Override
public synchronized void write(byte[] b, int off, int len) {
int remaining = limit - count;
if (remaining >= len) {
super.write(b, off, len);
return;
}
if (remaining > 0) {
// write what we can "normally"
super.write(b, off, remaining);
// rest delegated
write(b, off + remaining, len - remaining);
return;
}
// we are over the limit
isOverLimit = true;
// write till limit is reached
int writeTillLimit = Math.min(len, limit - zeroIndexPointer);
System.arraycopy(b, off, buf, zeroIndexPointer, writeTillLimit);
zeroIndexPointer = (zeroIndexPointer + writeTillLimit) % limit;
if (writeTillLimit < len) {
// write rest
write(b, off + writeTillLimit, len - writeTillLimit);
}
}

@Override
public synchronized void reset() {
super.reset();
zeroIndexPointer = 0;
isOverLimit = false;
}

// ---- output
@Override
public synchronized void writeTo(OutputStream out) throws IOException {
if (!isOverLimit) {
super.writeTo(out);
return;
}
out.write(buf, zeroIndexPointer, limit - zeroIndexPointer);
out.write(buf, 0, zeroIndexPointer);
}

@Override
public synchronized byte[] toByteArray() {
if (!isOverLimit) {
return super.toByteArray();
}
byte[] result = new byte[limit];
System.arraycopy(buf, zeroIndexPointer, result, 0, limit - zeroIndexPointer);
System.arraycopy(buf, 0, result, limit - zeroIndexPointer, zeroIndexPointer);
return result;
}

@SuppressFBWarnings(value = "DM_DEFAULT_ENCODING", justification = "We want to use the default encoding here since this is contract on ByteArrayOutputStream")
@Override
public synchronized String toString() {
if (!isOverLimit) {
return super.toString();
}
return new String(buf, zeroIndexPointer, limit - zeroIndexPointer) + new String(buf, 0, zeroIndexPointer);
}

@Override
public synchronized String toString(String charsetName) throws UnsupportedEncodingException {
if (!isOverLimit) {
return super.toString(charsetName);
}
return new String(buf, zeroIndexPointer, limit - zeroIndexPointer, charsetName) + new String(buf, 0, zeroIndexPointer, charsetName);
}

}
Loading