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}