nl.vpro.util.CommandExecutor Maven / Gradle / Ivy
package nl.vpro.util;
import lombok.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.function.*;
import java.util.stream.Stream;
import org.apache.commons.io.output.WriterOutputStream;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.slf4j.LoggerFactory;
import nl.vpro.logging.LoggerOutputStream;
import nl.vpro.logging.simple.*;
/**
* Executor for external commands.
*
* Three types of methods:
*
* - {@link #execute(InputStream, OutputStream, OutputStream, String...)} To synchronously execute and return exit code.
* - {@link #submit(java.io.InputStream, java.io.OutputStream, java.io.OutputStream, java.util.function.IntConsumer, java.lang.String...)} For asynchronous execution
*
* These two also have versions with a {@link Parameters} (or {@link Parameters.Builder} or {@code Consumer}) argument, so you can use the builder pattern to fill in parameters.
*
* - {@link #lines(InputStream, OutputStream, String...)} For synchronous execution and returing the output as a stream of strings.
*
* @author Michiel Meeuwissen
* @since 1.6
*/
public interface CommandExecutor {
/**
* Executes with the given arguments. The command itself is supposed to be a member of the implementation, so you
* would have a CommandExecutor instance for every external program you'd like to wrap.
* The version with no outputstream argument logs the output.
* @return the exit code
*/
int execute(String... args);
/**
* Executes the command, defaulting version of {@link #execute(java.io.InputStream, java.io.OutputStream, java.io.OutputStream, java.lang.String...)}, where the first argument is {@code null}
*
* @param out Stdout of the command will be written to this.
* @param errors Stderr of the command will be written to this. To log errors use {@link nl.vpro.logging.LoggerOutputStream#error(org.slf4j.Logger)}
* @param args The command and its arguments to be executed on the remote server
* @return The exit code
*/
default int execute(OutputStream out, OutputStream errors, String... args) {
return execute(null, out, errors, args);
}
/**
* Executes the command with given arguments. Stderr will be logged via slf4j. There will be no stdin supplied.
* @param out Stdout will be written to this.
* @return the exit code
*/
default int execute(OutputStream out, String... args) {
return execute(out, LoggerOutputStream.error(LoggerFactory.getLogger(getClass()), true), args);
}
/**
* Defaulting version of {@link #execute(OutputStream, String...)}, where the output of the command will be interpreted
* as a UTF-stream, and be written to the supplied {@link Writer}.
*
* @param out Stdout will be written to this.
* @return the exit code
*/
@SneakyThrows
default int execute(Writer out, String... args) {
return execute(
WriterOutputStream.builder()
.setWriter(out)
.setCharset(StandardCharsets.UTF_8)
.get(),
LoggerOutputStream.error(LoggerFactory.getLogger(getClass()), true), args);
}
/**
* Executes the command
*
* @param in Stdin of the command will be taken from this. This may be {@code null} for no stdin at all.
* @param out Stdout of the command will be written to this.
* @param error Stder of the command will be written to this.
* @return The exit code
*/
default int execute(@Nullable InputStream in, OutputStream out, OutputStream error, String... args) {
return execute(Parameters.builder()
.in(in)
.out(out)
.errors(error)
.args(args)
.build());
}
/**
* Executes the command .
* @param parameters The parameters for doing this wrapped in a {@link Parameters} object.
*/
int execute(Parameters parameters);
/**
* This defaulting version of {@link #execute(Parameters)} eliminates the need to call {@link Parameters.Builder#build()}.
*/
default int execute(Parameters.Builder parameters) {
return execute(parameters.build());
}
/**
* Executes the command in the background.
* @param callback will be called when ready.
* @return A future producing the result code.
*/
default CompletableFuture submit(
InputStream in,
OutputStream out,
OutputStream error,
IntConsumer callback,
String... args) {
return submit(callback, Parameters.builder()
.in(in)
.out(out)
.errors(error)
.args(args)
);
}
default CompletableFuture submit(IntConsumer callback, Parameters.Builder parameters) {
return submit(callback, parameters.build());
}
/**
*
*/
default CompletableFuture submit(IntConsumer callback, Parameters parameters) {
final int[] result = {-1};
return CompletableFuture.supplyAsync(() -> {
result[0] = execute(parameters);
return result[0];
}).whenComplete((i, t) -> {
if (callback != null) {
getLogger().debug("Calling back {}", callback);
synchronized (callback) {
callback.accept(result[0]);
callback.notifyAll();
}
}
});
}
default CompletableFuture submit(Parameters parameters) {
return submit((i) -> {}, parameters);
}
default CompletableFuture submit(Parameters.Builder parameters) {
return submit((i) -> {}, parameters.build());
}
default CompletableFuture submit(Consumer parameters) {
Parameters.Builder builder = parameters();
parameters.accept(builder);
return submit(builder);
}
default CompletableFuture submit(InputStream in, OutputStream out, OutputStream error, String... args) {
return submit(in, out, error, null, args);
}
default CompletableFuture submit(OutputStream out, OutputStream error, String... args) {
return submit(null, out, error, args);
}
default CompletableFuture submit(OutputStream out, String... args) {
return submit(out, LoggerOutputStream.error(LoggerFactory.getLogger(getClass())), args);
}
default CompletableFuture submit(Consumer callback, String... args) {
return CompletableFuture.supplyAsync(() -> {
Integer result = null;
try {
result = execute(args);
return result;
} finally {
if (callback != null) {
callback.accept(result);
}
}
});
}
default CompletableFuture submit(String... args) {
return submit((OutputStream) null, args);
}
/**
* Executes the command streamingly. Stdout is converted to a stream of string (one for each line).
* E.g.
* *
* CommandExecutorImpl env = new CommandExecutorImpl("/usr/bin/env");
* long running = env.lines("ps", "u").filter(s -> s.contains("amara_poms_publisher")).count();
*
* @return The exit code
*/
default Stream lines(InputStream in, OutputStream errors, String... args) {
try {
final PipedInputStream reader = new PipedInputStream();
final PipedOutputStream writer = new PipedOutputStream(reader);
final BufferedReader result = new BufferedReader(new InputStreamReader(reader));
final CompletableFuture submit = submit(in, writer, errors, (i) -> {
try {
writer.flush();
writer.close();
} catch (IOException ignored) {
}
}, args);
submit.whenComplete((i, t) -> {
if (t != null && !(t instanceof CancellationException)) {
if (t.getMessage() != null) {
getLogger().error(t.getMessage());
} else {
getLogger().error(t.getClass().getSimpleName());
}
}
getLogger().debug("Ready with {}", i);
});
return result.lines().onClose(() -> {
submit.cancel(true);
CloseableIterator.closeQuietly(writer);
});
} catch (IOException e) {
getLogger().error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
default Stream lines(String... args) {
return lines(null,
LoggerOutputStream.error(LoggerFactory.getLogger(getClass()), true), args
);
}
static boolean isBrokenPipe(Throwable ioe) {
return ioe.getCause() != null && ioe.getCause().getMessage().equalsIgnoreCase("broken pipe");
}
SimpleLogger getLogger();
class BrokenPipe extends RuntimeException {
@Serial
private static final long serialVersionUID = 973250365216289481L;
public BrokenPipe(IOException e) {
super(e);
}
}
@Getter
class ExitCodeException extends RuntimeException {
@Serial
private static final long serialVersionUID = 6103943329292512702L;
final int exitCode;
public ExitCodeException(String message, int exitCode) {
super(message);
this.exitCode = exitCode;
}
@Override
public String toString() {
return super.toString() + " exitcode: " + exitCode;
}
}
static Parameters.Builder parameters() {
return Parameters.builder();
}
/**
* The binary associated with the CommandExecutor.
*/
Supplier getBinary();
/**
* The parameters of {@link #submit(IntConsumer, Parameters)}, in other words, an object representing the one time parameters of a call to a {@link CommandExecutor}.
*/
@Getter
class Parameters {
/**
* InputStream (optional)
*/
final InputStream in;
final OutputStream out;
final OutputStream errors;
final Consumer onProcessCreation;
/**
* (Extra) arguments for the external command
*/
final String[] args;
/**
* @param in The input stream for the external command
* @param out The input stream for the external command
* @param onProcessCreation As soon as the {@link Process} is created this is called.
*/
@lombok.Builder(builderClassName = "Builder")
private Parameters(
InputStream in,
OutputStream out,
OutputStream errors,
Consumer onProcessCreation,
@Singular List listArgs) {
this.in = in;
this.out = out;
this.errors = errors == null ?
LoggerOutputStream.error(LoggerFactory.getLogger(getClass()), true) : errors;
this.onProcessCreation = onProcessCreation == null ? (p) -> {} : onProcessCreation;
this.args = listArgs == null ? new String[0] : listArgs.toArray(new String[0]);
}
public static class Builder {
public Builder args(String... args) {
return listArgs(Arrays.asList(args));
}
public Builder arg(String... args) {
for (String a : args) {
listArg(a);
}
return this;
}
public CompletableFuture submit(IntConsumer exitCode, CommandExecutor executor) {
return executor.submit(exitCode, this);
}
public CompletableFuture submit(CommandExecutor executor) {
return submit((exitCode) -> {
}, executor);
}
public int execute(CommandExecutor executor) {
return executor.execute(this);
}
/**
*
* Sets up input and errors stream (unless they are set already) so they can be
* used via a {@link Consumer} of {@link Event}s.
*
*
* Lines on stdin are fed to the consumer as Events on level {@link Level#INFO}
*
*
* Lines on stderr are fed to the consumer as Events on level {@link Level#ERROR}
*
* Empty lines are ignored.
*/
public Builder outputConsumer(Consumer outputConsumer) {
if (outputConsumer != null) {
if (out != null && errors != null) {
throw new IllegalArgumentException();
}
EventSimpleLogger eventLogger = EventSimpleLogger.of(outputConsumer);
if (out == null) {
out = SimpleLoggerOutputStream.info(eventLogger, true);
}
if (errors == null) {
errors = SimpleLoggerOutputStream.error(eventLogger, true);
}
}
return this;
}
/**
* @deprecated Use {@link #onProcessCreation(Consumer)}
*/
@Deprecated
public Builder consumer(Consumer consumer) {
return onProcessCreation(consumer);
}
}
}
}