package io.arivera.oss.embedded.rabbitmq.bin; import io.arivera.oss.embedded.rabbitmq.EmbeddedRabbitMq; import io.arivera.oss.embedded.rabbitmq.EmbeddedRabbitMqConfig; import io.arivera.oss.embedded.rabbitmq.apache.commons.lang3.SystemUtils; import io.arivera.oss.embedded.rabbitmq.util.StringUtils; import org.apache.commons.io.output.NullOutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.zeroturnaround.exec.ProcessExecutor; import org.zeroturnaround.exec.StartedProcess; import org.zeroturnaround.exec.listener.ProcessListener; import org.zeroturnaround.exec.stream.slf4j.Level; import org.zeroturnaround.exec.stream.slf4j.Slf4jStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; /** * A generic way of executing any of the commands found under the {@code sbin} folder of the RabbitMQ installation. * <p> * Example usage: * <pre> <code> RabbitMqCommand command = new RabbitMqCommand(config, "rabbitmq-server", "-detached"); * StartedProcess process = command.call(); * // ... * </code> * </pre> * <p> * To read the output as it happens, use the {@link #writeOutputTo(OutputStream)} method. * <p> * To be notified of the process ending without blocking for the result of the finished process, use * {@link #listenToEvents(ProcessListener)} method. * * @see RabbitMqCtl * @see RabbitMqServer */ public class RabbitMqCommand implements Callable<StartedProcess> { static final String BINARIES_FOLDER = "sbin"; private static final String LOGGER_TEMPLATE = "%s.Process.%s"; private static final ProcessListener NULL_LISTENER = new NullProcessListener(); private static final NullOutputStream NULL_OUTPUT_STREAM = new NullOutputStream(); private static final boolean IS_WIN = SystemUtils.IS_OS_WINDOWS; private static final String WIN_EXT = ".bat"; private static final String UNIT_EXT = ""; private final String command; private final File executableFile; private final List<String> arguments; private final ProcessExecutorFactory processExecutorFactory; private final File appFolder; private final Map<String, String> envVars; private Logger processOutputLogger; private OutputStream outputStream; private OutputStream errorOutputStream; private ProcessListener eventsListener; private boolean storeOutput; private Level stdOutLogLevel; private Level stdErrLogLevel; /** * Constructs a new instance this class to execute arbitrary RabbitMQ commands with arbitrary arguments. * * By default: * <ul> * <li> * the resulting processes's output will be logged using a Logger with a name matching the command. * See {@link #logWith(Logger)} to use another Logger * </li> * <li> * the output from STDOUT will be logged as {@code INFO} * </li> * <li> * the output from STDERR will e logged as {@code WARN} * </li> * <li> * the output can be programmatically accessed by retrieving the {@link org.zeroturnaround.exec.ProcessResult} * from the resulting {@link #call()} execution. To disable storing the output see {@link #storeOutput(boolean)} * <p> * To obtain the output as a stream as it's being produced, * see {@link #writeOutputTo(OutputStream)} and {@link #writeErrorOutputTo(OutputStream)}. * </li> * <li> * the process' events will be ignored. See {@link #listenToEvents(ProcessListener)} to define a listener. * </li> * </ul> * * @param config the configuration information used to launch the process with the correct context. * @param command command name, without any path or extension. For example, for a command like * "{@code rabbitmq-plugins.bat list}", use "{@code rabbitmq-plugins}" as value * @param arguments list of arguments to pass to the executable. For example, for a command like * "{@code ./rabbitmq-plugins enable foo}", utilize {@code ["enable", "foo"]} as value */ public RabbitMqCommand(EmbeddedRabbitMqConfig config, String command, String... arguments) { this(config.getProcessExecutorFactory(), config.getEnvVars(), config.getAppFolder(), command, arguments); } /** * An alternative constructor that allows for more control. */ public RabbitMqCommand(ProcessExecutorFactory processExecutorFactory, Map<String, String> envVars, File appFolder, String command, String... arguments) { this.processExecutorFactory = processExecutorFactory; this.command = command + getCommandExtension(); this.envVars = envVars; this.appFolder = appFolder; this.executableFile = new File(new File(this.appFolder, BINARIES_FOLDER), this.command); if (!(executableFile.exists())) { throw new IllegalArgumentException("The given command could not be found using the path: " + executableFile); } this.arguments = Arrays.asList(arguments); this.processOutputLogger = LoggerFactory.getLogger( String.format(LOGGER_TEMPLATE, EmbeddedRabbitMq.class.getName(), command)); this.outputStream = NULL_OUTPUT_STREAM; this.errorOutputStream = NULL_OUTPUT_STREAM; this.eventsListener = NULL_LISTENER; this.storeOutput = true; this.stdOutLogLevel = Level.INFO; this.stdErrLogLevel = Level.WARN; } static String getCommandExtension() { return IS_WIN ? WIN_EXT : UNIT_EXT; } /** * Output from the process will be written here as it happens. * * @see #writeErrorOutputTo(OutputStream) */ public RabbitMqCommand writeOutputTo(OutputStream outputStream) { this.outputStream = outputStream; return this; } /** * Error output from the process will be written here as it happens. * * @see #writeOutputTo(OutputStream) */ public RabbitMqCommand writeErrorOutputTo(OutputStream outputStream) { this.errorOutputStream = outputStream; return this; } /** * Defines which SLF4J logger to use log the process output as it would have been dumped to STDOUT and STDERR. */ public RabbitMqCommand logWith(Logger logger) { this.processOutputLogger = logger; return this; } /** * Registers a unique listener to be notified of process events, such as start and finish. */ public RabbitMqCommand listenToEvents(ProcessListener listener) { this.eventsListener = listener; return this; } /** * Used to define if the output of the process should be stored for retrieval after the ProcessResult future is * completed. * <p> * Default is {@code true} */ public RabbitMqCommand storeOutput(boolean storeOutput) { this.storeOutput = storeOutput; return this; } /** * Defines which logging level to use for the process' standard output. * <p> * Default is {@code INFO} */ public RabbitMqCommand logStandardOutputAs(Level level) { this.stdOutLogLevel = level; return this; } /** * Defines which logging level to use for the processes' standard error output. * <p> * Default is {@code WARN} */ public RabbitMqCommand logStandardErrorOutputAs(Level level) { this.stdErrLogLevel = level; return this; } @Override public StartedProcess call() throws RabbitMqCommandException { List<String> fullCommand = new ArrayList<>(arguments); fullCommand.add(0, executableFile.toString()); Slf4jStream loggingStream = Slf4jStream.of(processOutputLogger); LoggingProcessListener loggingListener = new LoggingProcessListener(processOutputLogger); ProcessExecutor processExecutor = processExecutorFactory.createInstance() .environment(envVars) .directory(appFolder) .command(fullCommand) .destroyOnExit() .addListener(loggingListener) // Logs process events (like start, stop...) .addListener(eventsListener) // Notifies asynchronously of process events (start/finish/stop) .redirectError(loggingStream.as(stdErrLogLevel)) // Logging for output made to STDERR .redirectOutput(loggingStream.as(stdOutLogLevel)) // Logging for output made to STDOUT .redirectOutputAlsoTo(outputStream) // Pipe stdout to this stream for the application to process .redirectErrorAlsoTo(errorOutputStream) // Pipe stderr to this stream for the application to process .readOutput(storeOutput); // Store the output in the ProcessResult as well. try { return processExecutor.start(); } catch (IOException e) { throw new RabbitMqCommandException("Failed to execute: " + StringUtils.join(fullCommand, " "), e); } } public static class ProcessExecutorFactory { public ProcessExecutor createInstance() { return new ProcessExecutor(); } } private static class NullProcessListener extends ProcessListener { } }