io.mvnpm.esbuild.Execute Maven / Gradle / Ivy
package io.mvnpm.esbuild;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import io.mvnpm.esbuild.model.EsBuildConfig;
import io.mvnpm.esbuild.model.ExecuteResult;
import io.mvnpm.esbuild.model.WatchBuildResult;
import io.mvnpm.esbuild.model.WatchStartResult;
public class Execute {
private static final ExecutorService EXECUTOR_STREAMER = Executors.newSingleThreadExecutor(r -> {
final Thread t = new Thread(r, "Process stdout streamer");
t.setDaemon(true);
return t;
});
private static final Logger logger = Logger.getLogger(Execute.class.getName());
private final Path workDir;
private final File esBuildExec;
private EsBuildConfig esBuildConfig;
private String[] args;
public Execute(Path workDir, File esBuildExec, EsBuildConfig esBuildConfig) {
this.workDir = workDir;
this.esBuildExec = esBuildExec;
this.esBuildConfig = esBuildConfig;
}
public Execute(Path workDir, File esBuildExec, String[] args) {
this.workDir = workDir;
this.esBuildExec = esBuildExec;
this.args = args;
}
public ExecuteResult executeAndWait() throws IOException {
final Process process = createProcess(getCommand(), Optional.empty());
try {
final int exitCode = process.waitFor();
final String content = readStream(process.getInputStream());
final String errors = readStream(process.getErrorStream());
if (exitCode != 0) {
throw new BundleException(errors.isEmpty() ? "Unexpected Error during bundling" : errors, content);
}
return new ExecuteResult(content);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
public WatchStartResult watch(BuildEventListener listener) throws IOException {
final Process process = createProcess(getCommand(), Optional.of(listener));
final ExecutorService executorStreamer = Executors
.newSingleThreadExecutor(r -> new Thread(r, "Esbuild watch stdout streamer"));
final ExecutorService executorBuild = Executors
.newSingleThreadExecutor(r -> new Thread(r, "Esbuild build listeners notify"));
final AtomicReference result = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
final WatchStartResult.WatchProcess watchProcess = new WatchStartResult.WatchProcess() {
@Override
public boolean isAlive() {
return process.isAlive();
}
@Override
public void close() throws IOException {
process.destroyForcibly();
executorStreamer.shutdownNow();
executorBuild.shutdownNow();
try {
process.waitFor();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
if (latch.getCount() == 1) {
latch.countDown();
}
}
};
try {
final InputStream processStream = process.getInputStream();
executorStreamer.execute(new Streamer(executorBuild, process::isAlive, processStream, (r) -> {
if (latch.getCount() == 1) {
result.set(r);
latch.countDown();
} else {
listener.onBuild(r);
}
}, r -> {
if (latch.getCount() == 1) {
result.set(r);
latch.countDown();
} else if (!r.isSuccess()) {
listener.onBuild(r);
}
}));
latch.await();
if (!process.isAlive() && !result.get().isSuccess()) {
throw result.get().bundleException();
}
return new WatchStartResult(result.get(), watchProcess);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
watchProcess.close();
throw new RuntimeException(e);
}
}
private String[] getCommand() {
String[] command = args != null ? getCommand(args) : getCommand(esBuildConfig);
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "running esbuild with flags: \n > `{0}`", String.join(" ", command));
}
return command;
}
private String[] getCommand(EsBuildConfig esBuildConfig) {
String[] params = esBuildConfig.toParams();
return getCommand(params);
}
private String[] getCommand(String[] args) {
List argList = new ArrayList<>(args.length + 1);
argList.add(esBuildExec.toString());
argList.addAll(Arrays.asList(args));
return argList.toArray(String[]::new);
}
public Process createProcess(final String[] command, final Optional listener) throws IOException {
return new ProcessBuilder().redirectErrorStream(listener.isPresent()).directory(workDir.toFile())
.command(command).start();
}
private record Streamer(ExecutorService executorBuild, BooleanSupplier isAlive, InputStream processStream,
BuildEventListener listener, Consumer onExit) implements Runnable {
@Override
public void run() {
final AtomicBoolean hasError = new AtomicBoolean();
final StringBuilder outputBuilder = new StringBuilder();
consumeStream(isAlive, processStream, l -> {
logger.fine(l);
outputBuilder.append("\n").append(l);
if (l.contains("build finished")) {
logger.fine("Build finished!");
final String output = outputBuilder.toString();
final boolean error = hasError.getAndSet(false);
outputBuilder.setLength(0);
executorBuild.execute(() -> {
if (!error) {
listener.onBuild(new WatchBuildResult(output));
} else {
listener.onBuild(
new WatchBuildResult(output, new BundleException("Error during bundling", output)));
}
});
} else if (l.contains("[ERROR]")) {
hasError.set(true);
}
});
if (!hasError.get()) {
onExit.accept(new WatchBuildResult(outputBuilder.toString()));
} else {
onExit.accept(new WatchBuildResult(outputBuilder.toString(),
new BundleException("Process exited with error", outputBuilder.toString())));
}
}
}
private static String readStream(InputStream stream) {
final StringBuilder s = new StringBuilder();
consumeStream(() -> true, stream, l -> s.append(l).append("\n"));
return s.toString();
}
private static void consumeStream(BooleanSupplier stayAlive, InputStream stream, Consumer newLineConsumer) {
try (
final InputStreamReader in = new InputStreamReader(stream, StandardCharsets.UTF_8);
final BufferedReader reader = new BufferedReader(in)) {
String line;
while ((line = reader.readLine()) != null) {
newLineConsumer.accept(line);
if (!stayAlive.getAsBoolean()) {
break;
}
}
} catch (IOException e) {
// ignore
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy