org.kiwiproject.base.process.ProcessHelper Maven / Gradle / Ivy
Show all versions of kiwi Show documentation
package org.kiwiproject.base.process;
import static java.util.stream.Collectors.toList;
import static org.kiwiproject.collect.KiwiLists.first;
import static org.kiwiproject.io.KiwiIO.readLinesFromInputStreamOf;
import static org.kiwiproject.io.KiwiIO.streamLinesFromInputStreamOf;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.tuple.Pair;
import java.io.UncheckedIOException;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
/**
* Wrapper class around the static utility functions in {@link Processes} that requires an instance, adn thus by using
* an instance of this class instead of {@link Processes} directly, it will make it much easier to test code that deals
* with processes.
*
* Note that most of the methods are intended only for use on Unix/Linux operating systems.
*/
public class ProcessHelper {
/**
* Waits up to {@link Processes#DEFAULT_WAIT_FOR_EXIT_TIME_SECONDS} for the given process to exit.
*
* @param process the process to wait for
* @return an {@link Optional} that will contain the exit code if the process exited before the timeout, or
* empty if the process did not exit before the timeout expired.
* @see Processes#waitForExit(Process)
*/
public Optional waitForExit(Process process) {
return Processes.waitForExit(process);
}
/**
* Waits up to the specified {@code timeout} for the given process to exit.
*
* @param process the process to wait for
* @param timeout the value of the time to wait
* @param unit the unit of time to wait
* @return an {@link Optional} that will contain the exit code if the process exited before the timeout, or
* empty if the process did not exit before the timeout expired.
* @see Processes#waitForExit(Process, long, TimeUnit)
*/
public Optional waitForExit(Process process, long timeout, TimeUnit unit) {
return Processes.waitForExit(process, timeout, unit);
}
/**
* Launches a new process using the specified {@code command}.
*
* @param command the list containing the program and its arguments
* @return the new {@link Process}
* @see Processes#launch(List)
*/
public Process launch(List command) {
return Processes.launch(command);
}
/**
* Launches a new process using the specified {@code command}.
*
* @param command a list containing the program and its arguments
* @return the new {@link Process}
* @see Processes#launch(String...)
*/
public Process launch(String... command) {
return Processes.launch(command);
}
/**
* Does a {@code pgrep} with the specified full command.
*
* @param commandLine the full command to match
* @return a list of matching process ids (pids)
* @see Processes#pgrep(String)
* @see Processes#wasPgrepFlagsCheckSuccessful()
* @see Processes#getPgrepFlags()
*/
public List pgrep(String commandLine) {
return Processes.pgrep(commandLine);
}
/**
* Does a {@code pgrep} with the specified full command.
*
* @param user the OS user (passed to the {@code -u} option)
* @param commandLine the full command to match
* @return list of matching process ids (pids)
* @see Processes#pgrep(String, String)
* @see Processes#wasPgrepFlagsCheckSuccessful()
* @see Processes#getPgrepFlags()
*/
public List pgrep(String user, String commandLine) {
return Processes.pgrep(user, commandLine);
}
/**
* Does a {@code pgrep} against the specified full command, expecting a single result, or no result.
*
* @param commandLine the full command line
* @return an optional either containing a process id, or an empty optional
* @see Processes#pgrepWithSingleResult(String)
* @see Processes#wasPgrepFlagsCheckSuccessful()
* @see Processes#getPgrepFlags()
*/
public Optional pgrepWithSingleResult(String commandLine) {
return Processes.pgrepWithSingleResult(commandLine);
}
/**
* Does a {@code pgrep} against the specified full command, expecting a single result for a specific user, or no result.
*
* @param user the OS user (passed to the {@code -u} option)
* @param commandLine the full command to match
* @return an optional either containing a process id, or an empty optional
* @see Processes#pgrepWithSingleResult(String, String)
* @see Processes#wasPgrepFlagsCheckSuccessful()
* @see Processes#getPgrepFlags()
*/
public Optional pgrepWithSingleResult(String user, String commandLine) {
return Processes.pgrepWithSingleResult(user, commandLine);
}
/**
* Does a {@code pgrep} with the specified full command.
*
* @param commandLine the full command line to match
* @return a list of pgrep output, with each line in format "{pid} {command}"
* @see Processes#pgrepList(String)
* @see Processes#wasPgrepFlagsCheckSuccessful()
* @see Processes#getPgrepFlags()
*/
public List pgrepList(String commandLine) {
return Processes.pgrepList(commandLine);
}
/**
* Does a {@code pgrep} with the specified full command.
*
* @param user the OS user (passed to the {@code -u} option)
* @param commandLine the full command line to match
* @return a list of pgrep output, with each line in format "{pid} {command}"
* @see Processes#pgrepList(String, String)
* @see Processes#wasPgrepFlagsCheckSuccessful()
* @see Processes#getPgrepFlags()
*/
public List pgrepList(String user, String commandLine) {
return Processes.pgrepList(user, commandLine);
}
/**
* Does a {@code pgrep} for the specified full command, returning a list of pairs containing the
* process id (pid) and the matched command line.
*
* @param commandLine the full command line to match
* @return a list of {@link Pair} objects; each pair contains the pid as a Long and the associated full command
* @see Processes#pgrepParsedList(String)
* @see Processes#wasPgrepFlagsCheckSuccessful()
* @see Processes#getPgrepFlags()
*/
public List> pgrepParsedList(String commandLine) {
return Processes.pgrepParsedList(commandLine);
}
/**
* Does a {@code pgrep} for the specified full command, returning a list of pairs containing the
* process id (pid) and the matched command line.
*
* @param user the OS user (passed to the {@code -u} option)
* @param commandLine the full command line to match
* @return a list of {@link Pair} objects; each pair contains the pid as a Long and the associated full command
* @see Processes#pgrepParsedList(String, String)
* @see Processes#wasPgrepFlagsCheckSuccessful()
* @see Processes#getPgrepFlags()
*/
public List> pgrepParsedList(String user, String commandLine) {
return Processes.pgrepParsedList(user, commandLine);
}
/**
* Kill a process, waiting up to {@link Processes#DEFAULT_KILL_TIMEOUT_SECONDS} seconds for it to terminate.
*
* @param processId the pid of the process to kill
* @param signal the kill signal; this could be the signal number (e.g. "1") or name (e.g. "SIGHUP")
* @param action the {@link KillTimeoutAction} to take if the process doesn't terminate within the allotted time
* @return the exit code from the {@code kill} command, or {@code -1} if {@code action} is
* @see Processes#kill(long, KillSignal, KillTimeoutAction)
*/
public int kill(long processId, KillSignal signal, KillTimeoutAction action) {
return Processes.kill(processId, signal, action);
}
/**
* Kill a process, waiting up to {@code timeout} in the specified {@link TimeUnit} for it to terminate.
*
* @param processId the pid of the process to kill
* @param signal the kill signal enum
* @param timeout the time to wait for the process to be killed
* @param unit the time unit associated with {@code timeout}
* @param action the {@link KillTimeoutAction} to take if the process doesn't terminate within the allotted time
* @return the exit code from the {@code kill} command, or {@code -1} if {@code action} is
* @see Processes#kill(long, KillSignal, long, TimeUnit, KillTimeoutAction)
*/
public int kill(long processId, KillSignal signal, long timeout, TimeUnit unit, KillTimeoutAction action) {
return Processes.kill(processId, signal, timeout, unit, action);
}
/**
* Kill a process, waiting up to {@link Processes#DEFAULT_KILL_TIMEOUT_SECONDS} seconds for it to terminate.
*
* @param processId the pid of the process to kill
* @param signal the kill signal; this could be the signal number (e.g. "1") or name (e.g. "SIGHUP")
* @param action the {@link KillTimeoutAction} to take if the process doesn't terminate within the allotted time
* @return the exit code from the {@code kill} command, or {@code -1} if {@code action} is
* {@link KillTimeoutAction#NO_OP} and the kill command times out
* @throws UncheckedIOException if an I/O error occurs killing the process
* @see Processes#kill(long, String, KillTimeoutAction)
*/
public int kill(long processId, String signal, KillTimeoutAction action) {
return Processes.kill(processId, signal, action);
}
/**
* Kill a process, waiting up to {@code timeout} in the specified {@link TimeUnit} for it to terminate.
*
* @param processId the pid of the process to kill
* @param signal the kill signal; this could be the signal number (e.g. "1") or name (e.g. "SIGHUP")
* @param timeout the time to wait for the process to be killed
* @param unit the time unit associated with {@code timeout}
* @param action the {@link KillTimeoutAction} to take if the process doesn't terminate within the allotted time
* @return the exit code from the {@code kill} command, or {@code -1} if {@code action} is
* {@link KillTimeoutAction#NO_OP} and the kill command times out
* @throws UncheckedIOException if an I/O error occurs killing the process
* @see Processes#kill(long, String, long, TimeUnit, KillTimeoutAction)
*/
public int kill(long processId, String signal, long timeout, TimeUnit unit, KillTimeoutAction action) {
return Processes.kill(processId, signal, timeout, unit, action);
}
/**
* Equivalent to a {@code kill -9} (i.e. a {@code SIGKILL}).
*
* @param process the process to kill forcibly
* @param timeout the time to wait for the process to be forcibly killed
* @param unit the time unit associated with the {@code timeout}
* @return {@code true} if {@code process} was killed before the timeout period elapsed; {@code false} otherwise
* @throws InterruptedException if the current thread is interrupted while waiting
* @see Processes#killForcibly(Process, long, TimeUnit)
*/
public boolean killForcibly(Process process, long timeout, TimeUnit unit) throws InterruptedException {
return Processes.killForcibly(process, timeout, unit);
}
/**
* For a given parent process id, find one and only one child process. There may be no child processes,
* in which case an empty {@link Optional} is returned.
*
* This method considers it an error if the process has more than one child process id.
*
* @param parentProcessId the parent process id
* @return optional containing the child process id if there is one; otherwise an empty optional
* @throws IllegalStateException if there is more than one child process found
*/
public Optional findChildProcessId(long parentProcessId) {
return findChildProcessIdInternal(parentProcessId, this);
}
/**
* This method is entirely for testing, specifically to be able to mock the Process object returned by
* calling launch on {@code processHelper} to perform a {@code pgrep -P}.
*
* @see #launchPgrepWithParentPidFlag(long, ProcessHelper)
*/
@VisibleForTesting
Optional findChildProcessIdInternal(long parentProcessId, ProcessHelper processHelper) {
var process = launchPgrepWithParentPidFlag(parentProcessId, processHelper);
List lines = readLinesFromInputStreamOf(process);
if (lines.isEmpty()) {
return Optional.empty();
} else if (lines.size() == 1) {
return Optional.of(Long.valueOf(first(lines)));
} else {
throw new IllegalStateException("More than one child process found for process ID " + parentProcessId);
}
}
/**
* For a given parent process id, find any child processes. There may be no child processes, in which case an
* empty collection is returned.
*
* @param parentProcessId the parent process id
* @return a collection of child pids, or an empty collection if there are no child processes
*/
public Collection findChildProcessIds(long parentProcessId) {
return findChildProcessIdsInternal(parentProcessId, this);
}
/**
* This method is entirely for testing, specifically to be able to mock the Process object returned by
* calling launch on {@code processHelper} to perform a {@code pgrep -P}.
*
* @see #launchPgrepWithParentPidFlag(long, ProcessHelper)
*/
@VisibleForTesting
Collection findChildProcessIdsInternal(long parentProcessId, ProcessHelper processHelper) {
var process = launchPgrepWithParentPidFlag(parentProcessId, processHelper);
Stream stream = streamLinesFromInputStreamOf(process);
return stream.map(Long::valueOf).collect(toList());
}
/**
* @implNote The "-P" flag causes pgrep to "Only match processes whose parent process ID is listed" per
* the man page. Also of note, "-P" is equivalent to the {@code --parent} long flag
*/
private Process launchPgrepWithParentPidFlag(long parentProcessId, ProcessHelper processHelper) {
return processHelper.launch("pgrep", "-P", String.valueOf(parentProcessId));
}
}