001package org.itsallcode.process;
002
003import java.time.Duration;
004import java.util.concurrent.TimeUnit;
005import java.util.logging.Logger;
006
007/**
008 * Provides control over native processes.
009 * 
010 * @param <T> type of stdout and stderr, e.g. {@link String}.
011 */
012public class SimpleProcess<T> {
013    private static final Logger LOG = Logger.getLogger(SimpleProcess.class.getName());
014    private final Process process;
015    private final String command;
016    private final ProcessOutputConsumer<T> consumer;
017
018    SimpleProcess(final Process process, final ProcessOutputConsumer<T> consumer, final String command) {
019        this.process = process;
020        this.consumer = consumer;
021        this.command = command;
022    }
023
024    /**
025     * Wait until the process has terminated.
026     * 
027     * @return exit code
028     * @see Process#waitFor()
029     */
030    public int waitForTermination() {
031        final int exitCode = waitForProcess();
032        consumer.waitForStreamsClosed();
033        return exitCode;
034    }
035
036    private int waitForProcess() {
037        try {
038            LOG.finest(() -> "Waiting for process %d (command '%s') to terminate...".formatted(
039                    pid(), command));
040            return process.waitFor();
041        } catch (final InterruptedException exception) {
042            Thread.currentThread().interrupt();
043            throw new IllegalStateException(
044                    "Interrupted while waiting for process %d (command '%s') to finish".formatted(pid(),
045                            command),
046                    exception);
047        }
048    }
049
050    /**
051     * Wait until the process terminates successfully with exit code {@code 0}.
052     * 
053     * @throws IllegalStateException if exit code is not equal to {@code 0}.
054     * @see #waitForTermination()
055     */
056    public void waitForSuccessfulTermination() {
057        waitForTermination(0);
058    }
059
060    /**
061     * Wait until the process terminates successfully with the given exit code.
062     * 
063     * @param expectedExitCode expected exit code
064     * @throws IllegalStateException if exit code is not equal to the given expected
065     *                               exit code.
066     * @see #waitForTermination(int)
067     */
068    public void waitForTermination(final int expectedExitCode) {
069        final int exitCode = waitForTermination();
070        if (exitCode != expectedExitCode) {
071            throw new IllegalStateException(
072                    "Expected process %d (command '%s') to terminate with exit code %d but was %d"
073                            .formatted(pid(), command, expectedExitCode, exitCode));
074        }
075    }
076
077    /**
078     * Wait until the process terminates with the given timeout.
079     * 
080     * @param timeout maximum time to wait for the termination
081     * @throws IllegalStateException if process does not exit within the given
082     *                               timeout.
083     * @see Process#waitFor(long, TimeUnit)
084     */
085    public void waitForTermination(final Duration timeout) {
086        waitForProcess(timeout);
087        LOG.finest(() -> "Process %d (command '%s') terminated with exit code %d".formatted(pid(), command,
088                exitValue()));
089        consumer.waitForStreamsClosed();
090    }
091
092    private void waitForProcess(final Duration timeout) {
093        try {
094            LOG.finest(() -> "Waiting %s for process %d (command '%s') to terminate...".formatted(timeout,
095                    pid(), command));
096            if (!process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
097                throw new IllegalStateException(
098                        "Timeout while waiting %s for process %d (command '%s')".formatted(timeout, pid(),
099                                command));
100            }
101        } catch (final InterruptedException exception) {
102            Thread.currentThread().interrupt();
103            throw new IllegalStateException(
104                    "Interrupted while waiting %s for process %d (command '%s') to finish".formatted(timeout,
105                            pid(), command),
106                    exception);
107        }
108    }
109
110    /**
111     * Get the standard output of the process.
112     * 
113     * @return standard output
114     */
115    public T getStdOut() {
116        return consumer.getStdOut();
117    }
118
119    /**
120     * Get the standard error of the process.
121     * 
122     * @return standard error
123     */
124    public T getStdErr() {
125        return consumer.getStdErr();
126    }
127
128    /**
129     * Check wether the process is alive.
130     * 
131     * @return {@code  true} if the process has not yet terminated
132     * @see Process#isAlive()
133     */
134    public boolean isAlive() {
135        return process.isAlive();
136    }
137
138    /**
139     * Get the exit value of the process.
140     * 
141     * @return exit value
142     * @see Process#exitValue()
143     */
144    public int exitValue() {
145        return process.exitValue();
146    }
147
148    /**
149     * Get the process ID.
150     * 
151     * @return process ID
152     * @see Process#pid()
153     */
154    public long pid() {
155        return process.pid();
156    }
157
158    /**
159     * Kill the process.
160     * 
161     * @see Process#destroy()
162     */
163    public void destroy() {
164        process.destroy();
165    }
166
167    /**
168     * Kill the process forcibly.
169     * 
170     * @see Process#destroyForcibly()
171     */
172    public void destroyForcibly() {
173        process.destroyForcibly();
174    }
175}