All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nl.vpro.util.CommandExecutor Maven / Gradle / Ivy

There is a newer version: 5.3.1
Show newest version
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: *

    *
  1. {@link #execute(InputStream, OutputStream, OutputStream, String...)} To synchronously execute and return exit code.
  2. *
  3. {@link #submit(java.io.InputStream, java.io.OutputStream, java.io.OutputStream, java.util.function.IntConsumer, java.lang.String...)} For asynchronous execution
  4. *
* 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. *
    *
  1. {@link #lines(InputStream, OutputStream, String...)} For synchronous execution and returing the output as a stream of strings.
  2. *
* @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); } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy