SimpleProcess.java

package org.itsallcode.process;

import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

/**
 * Provides control over native processes.
 * 
 * @param <T> type of stdout and stderr, e.g. {@link String}.
 */
public class SimpleProcess<T> {
    private static final Logger LOG = Logger.getLogger(SimpleProcess.class.getName());
    private final Process process;
    private final String command;
    private final ProcessOutputConsumer<T> consumer;

    SimpleProcess(final Process process, final ProcessOutputConsumer<T> consumer, final String command) {
        this.process = process;
        this.consumer = consumer;
        this.command = command;
    }

    /**
     * Wait until the process has terminated.
     * 
     * @return exit code
     * @see Process#waitFor()
     */
    public int waitForTermination() {
        final int exitCode = waitForProcess();
        consumer.waitForStreamsClosed();
        return exitCode;
    }

    private int waitForProcess() {
        try {
            LOG.finest(() -> "Waiting for process %d (command '%s') to terminate...".formatted(
                    pid(), command));
            return process.waitFor();
        } catch (final InterruptedException exception) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException(
                    "Interrupted while waiting for process %d (command '%s') to finish".formatted(pid(),
                            command),
                    exception);
        }
    }

    /**
     * Wait until the process terminates successfully with exit code {@code 0}.
     * 
     * @throws IllegalStateException if exit code is not equal to {@code 0}.
     * @see #waitForTermination()
     */
    public void waitForSuccessfulTermination() {
        waitForTermination(0);
    }

    /**
     * Wait until the process terminates successfully with the given exit code.
     * 
     * @param expectedExitCode expected exit code
     * @throws IllegalStateException if exit code is not equal to the given expected
     *                               exit code.
     * @see #waitForTermination(int)
     */
    public void waitForTermination(final int expectedExitCode) {
        final int exitCode = waitForTermination();
        if (exitCode != expectedExitCode) {
            throw new IllegalStateException(
                    "Expected process %d (command '%s') to terminate with exit code %d but was %d"
                            .formatted(pid(), command, expectedExitCode, exitCode));
        }
    }

    /**
     * Wait until the process terminates with the given timeout.
     * 
     * @param timeout maximum time to wait for the termination
     * @throws IllegalStateException if process does not exit within the given
     *                               timeout.
     * @see Process#waitFor(long, TimeUnit)
     */
    public void waitForTermination(final Duration timeout) {
        waitForProcess(timeout);
        LOG.finest(() -> "Process %d (command '%s') terminated with exit code %d".formatted(pid(), command,
                exitValue()));
        consumer.waitForStreamsClosed();
    }

    private void waitForProcess(final Duration timeout) {
        try {
            LOG.finest(() -> "Waiting %s for process %d (command '%s') to terminate...".formatted(timeout,
                    pid(), command));
            if (!process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
                throw new IllegalStateException(
                        "Timeout while waiting %s for process %d (command '%s')".formatted(timeout, pid(),
                                command));
            }
        } catch (final InterruptedException exception) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException(
                    "Interrupted while waiting %s for process %d (command '%s') to finish".formatted(timeout,
                            pid(), command),
                    exception);
        }
    }

    /**
     * Get the standard output of the process.
     * 
     * @return standard output
     */
    public T getStdOut() {
        return consumer.getStdOut();
    }

    /**
     * Get the standard error of the process.
     * 
     * @return standard error
     */
    public T getStdErr() {
        return consumer.getStdErr();
    }

    /**
     * Check wether the process is alive.
     * 
     * @return {@code  true} if the process has not yet terminated
     * @see Process#isAlive()
     */
    public boolean isAlive() {
        return process.isAlive();
    }

    /**
     * Get the exit value of the process.
     * 
     * @return exit value
     * @see Process#exitValue()
     */
    public int exitValue() {
        return process.exitValue();
    }

    /**
     * Get the process ID.
     * 
     * @return process ID
     * @see Process#pid()
     */
    public long pid() {
        return process.pid();
    }

    /**
     * Kill the process.
     * 
     * @see Process#destroy()
     */
    public void destroy() {
        process.destroy();
    }

    /**
     * Kill the process forcibly.
     * 
     * @see Process#destroyForcibly()
     */
    public void destroyForcibly() {
        process.destroyForcibly();
    }
}