org.kiwiproject.base.process.Processes Maven / Gradle / Ivy
Show all versions of kiwi Show documentation
package org.kiwiproject.base.process;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static java.util.stream.Collectors.toList;
import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotNull;
import static org.kiwiproject.base.KiwiStrings.SPACE;
import static org.kiwiproject.base.KiwiStrings.format;
import static org.kiwiproject.base.KiwiStrings.splitToList;
import static org.kiwiproject.collect.KiwiLists.first;
import static org.kiwiproject.collect.KiwiLists.second;
import static org.kiwiproject.io.KiwiIO.readLinesFromErrorStreamOf;
import static org.kiwiproject.io.KiwiIO.readLinesFromInputStreamOf;
import static org.kiwiproject.io.KiwiIO.streamLinesFromInputStreamOf;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.kiwiproject.base.UncheckedInterruptedException;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.List;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.concurrent.TimeUnit;
/**
* Utility class for working with operating system processes.
*
* Note that most of the methods are intended only for use on Unix/Linux operating systems.
*
* @see ProcessHelper
*/
@UtilityClass
@Slf4j
public class Processes {
/**
* Exit code that indicates a {@link Process} terminated normally.
*
* @see Process#exitValue()
*/
public static final int SUCCESS_EXIT_CODE = 0;
/**
* Default number of seconds to wait for termination of a process.
*
* @see #kill(long, KillSignal, KillTimeoutAction)
* @see #kill(long, String, KillTimeoutAction)
*/
public static final int DEFAULT_KILL_TIMEOUT_SECONDS = 5;
/**
* Default number of seconds to wait for a process to exit.
*
* @see #waitForExit(Process)
*/
public static final long DEFAULT_WAIT_FOR_EXIT_TIME_SECONDS = 5;
/**
* The command flags to use with the {@code pgrep} command for matching and printing the full command line.
* For example, to find the "sleep 25" process with pid 32332 we want pgrep to return results in the
* format: "[pid] [full command]". For this example the expected result is: {@code 32332 sleep 25}.
*
* However, there are differences in pgrep command line arguments between BSD-based systems (including macOS) and
* Linux systems. There are also differences between older and newer Linux distributions such as
* CentOS/Red Hat 6 and 7. Specifically, some systems require {@code -fl} to match against and print out the full
* command line with {@code pgrep}. Other implementations require {@code -fa} (or {@code -f --list-full}).
*
* See for example Linux pgrep and
* BSD pgrep for more information.
*/
private static final String PGREP_FULL_COMMAND_MATCH_AND_PRINT_FLAGS;
private static final boolean PGREP_CHECK_SUCCESSFUL;
static {
var result = choosePgrepFlags();
PGREP_FULL_COMMAND_MATCH_AND_PRINT_FLAGS = result.getLeft();
PGREP_CHECK_SUCCESSFUL = result.getRight();
}
private static final String PGREP_COMMAND = "pgrep";
private static Pair choosePgrepFlags() {
String flagsOrNull = findPgrepFlags().orElse(null);
return choosePgrepFlags(flagsOrNull);
}
@VisibleForTesting
static Pair choosePgrepFlags(@Nullable String flagsOrNull) {
if (isNull(flagsOrNull)) {
logPgrepFlagWarnings();
return Pair.of("-fa", false);
}
return Pair.of(flagsOrNull, true);
}
private static Optional findPgrepFlags() {
return tryPgrepForSleepCommand("-fa", "123")
.or(() -> tryPgrepForSleepCommand("-fl", "124"));
}
private static Optional tryPgrepForSleepCommand(String flags, String sleepTime) {
Process sleeperProc = null;
try {
sleeperProc = launch("sleep", sleepTime);
var pid = String.valueOf(sleeperProc.pid());
var pgrepCheckerProc = launch(PGREP_COMMAND, flags, "sleep");
var stdOutLines = readLinesFromInputStreamOf(pgrepCheckerProc);
var stdErrLines = readLinesFromErrorStreamOf(pgrepCheckerProc);
var expectedCommand = "sleep " + sleepTime;
logPgrepCheckInfo(flags, pid, stdOutLines, stdErrLines, expectedCommand);
if (linesContainPidAndFullCommand(stdOutLines, pid, expectedCommand)) {
LOG.info("Will use [{}] as flags for pgrep full-command listing", flags);
return Optional.of(flags);
}
LOG.trace("Flags [{}] did not produce pgrep full-command listing", flags);
return Optional.empty();
} catch (Exception e) {
LOG.error("Error checking pgrep flags. pgrep calls may fail in unexpected ways!", e);
return Optional.empty();
} finally {
if (nonNull(sleeperProc)) {
killSilently(sleeperProc.pid());
}
}
}
@VisibleForTesting
static void logPgrepCheckInfo(String flags,
String pid,
List stdOutLines,
List stdErrLines,
String expectedCommand) {
LOG.trace("Checking pgrep flags [{}] for command [{}] with pid {}", flags, expectedCommand, pid);
LOG.trace("pid {} stdOut: {}", pid, stdOutLines);
if (stdErrLines.isEmpty()) {
LOG.trace("pid {} stdErr: {}", pid, stdErrLines);
} else {
LOG.warn("stdErr checking pgrep flags for pid {} (command: {}): {}",
pid, expectedCommand, stdErrLines);
}
}
private static boolean linesContainPidAndFullCommand(List lines, String pid, String expectedCommand) {
return lines.stream().anyMatch(line -> line.contains(pid) && line.contains(expectedCommand));
}
private static void killSilently(long processId) {
try {
LOG.trace("Killing sleeper process ({}) used to determine pgrep flags", processId);
kill(processId, KillSignal.SIGTERM, KillTimeoutAction.NO_OP);
} catch (Exception e) {
LOG.warn("Error killing sleeper process ({}) used to determine pgrep flags", processId, e);
}
}
@VisibleForTesting
static void logPgrepFlagWarnings() {
LOG.warn("Neither -fa nor -fl flags produced PID and full command line, so pgrep commands will behave (or fail) in unexpected ways!");
LOG.warn("If you see this warning, DO NOT use any of the pgrep-related methods in Processes or ProcessHelper and submit a bug report.");
LOG.warn("Turn on TRACE-level logging to see standard output and error for pgrep commands");
LOG.warn("We will use -fa even though we know this will not work, instead of throwing an exception");
}
/**
* Use this method to determine if calling any of the pgrep methods in this class will work as expected.
*
* @return true if the pgrep check to determine the flags to use for full command matching was successful; false
* otherwise. If false you should NOT use any of the pgrep methods.
* @see #getPgrepFlags()
*/
public static boolean wasPgrepFlagsCheckSuccessful() {
return PGREP_CHECK_SUCCESSFUL;
}
/**
* Returns the pgrep flags that {@link Processes} will use in all pgrep methods.
* Use {@link #wasPgrepFlagsCheckSuccessful()} to check if the flags were chosen successfully
* to ensure that pgrep commands will work as expected on your OS.
*
* @return the flags that will be used in pgrep commands
*/
public static String getPgrepFlags() {
return PGREP_FULL_COMMAND_MATCH_AND_PRINT_FLAGS;
}
/**
* Get a process id, or "pid".
*
* Earlier versions of Kiwi used to perform some nasty things (checking if the actual process class was
* {@code java.lang.UNIXProcess}, then using reflection to get the value of the private {@code pid} field)
* to obtain Process pids, but since JDK 9 offers the {@link Process#pid()} method, that is no longer necessary.
* This method is now a simple delegation to {@link Process#pid()} and you should now prefer that method.
*
* @param process the process to obtain the process id (pid) from
* @return the process id of {@code process}
* @throws UnsupportedOperationException if the Process implementation does not support getting the pid
* @see Process#pid()
*/
public static long processId(Process process) {
checkArgumentNotNull(process);
return process.pid();
}
/**
* Get a process id, or "pid", if it is available from the {@link Process} implementation, wrapped inside
* an OptionalLong. If the pid is not available for whatever reason, return an empty OptionalLong.
*
* @param process the process to obtain the process id (pid) from
* @return an OptionalLong containing the process if of {@code process} or an empty OptionalLong if the
* {@link Process} implementation does not support getting the pid for whatever reason.
* @implNote the {@link Process#pid()} method says it can throw {@link UnsupportedOperationException} if the
* "implementation does not support this operation" but does not specify under what circumstances that can
* happen, and I have not been able to find this information using Google, Bing, or DuckDuckGo. This method
* logs a warning along with the exception, so if this occurs check your logs for the possible reason.
*/
public static OptionalLong processIdOrEmpty(Process process) {
checkArgumentNotNull(process);
try {
return OptionalLong.of(process.pid());
} catch (UnsupportedOperationException e) {
LOG.warn("The JDK cannot get the PID of the given Process. Check stack trace for a possible reason", e);
return OptionalLong.empty();
}
}
/**
* Check if the given {@link Process} has an exit code representing successful termination.
*
* @param process the Process, assumed to have exited
* @return true if the process terminated successfully
* @see #isSuccessfulExitCode(int)
* @see #SUCCESS_EXIT_CODE
* @see Process#exitValue()
*/
public static boolean hasSuccessfulExitCode(Process process) {
return isSuccessfulExitCode(process.exitValue());
}
/**
* Check if the given exit code represents successful termination.
*
* @param exitCode the exit code to check
* @return true if the exit code represents success
* @see #SUCCESS_EXIT_CODE
* @see Process#exitValue()
*/
public static boolean isSuccessfulExitCode(int exitCode) {
return exitCode == SUCCESS_EXIT_CODE;
}
/**
* Waits up to {@link #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.
*/
public static Optional waitForExit(Process process) {
return waitForExit(process, DEFAULT_WAIT_FOR_EXIT_TIME_SECONDS, TimeUnit.SECONDS);
}
/**
* 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.
*/
public static Optional waitForExit(Process process, long timeout, TimeUnit unit) {
try {
boolean exited = process.waitFor(timeout, unit);
return exited ? Optional.of(process.exitValue()) : Optional.empty();
} catch (InterruptedException e) {
LOG.warn("Interrupted waiting for process to exit", e);
Thread.currentThread().interrupt();
return Optional.empty();
}
}
/**
* Launches a new process using the specified {@code command}. This is just a convenience wrapper around
* creating a new {@link ProcessBuilder} and calling {@link ProcessBuilder#start()}.
*
* This wrapper also converts any thrown {@link IOException} to an {@link UncheckedIOException}.
*
* If you need more flexibility than provided in this simple wrapper, use {@link ProcessBuilder} directly.
*
* @param command the list containing the program and its arguments
* @return the new {@link Process}
* @see ProcessBuilder#ProcessBuilder(List)
* @see ProcessBuilder#start()
*/
public static Process launch(List command) {
try {
return launchProcessInternal(command);
} catch (IOException e) {
throw new UncheckedIOException("Error launching command: " + command, e);
}
}
/**
* 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 #launch(List)
*/
public static Process launch(String... command) {
return launch(Lists.newArrayList(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 #pgrep(String, String)
* @see #wasPgrepFlagsCheckSuccessful()
* @see #getPgrepFlags()
*/
public static List pgrep(String commandLine) {
return pgrep(null, 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 #wasPgrepFlagsCheckSuccessful()
* @see #getPgrepFlags()
*/
public static List pgrep(String user, String commandLine) {
try {
List command = buildPgrepCommand(user, commandLine);
var process = launchProcessInternal(command);
return streamLinesFromInputStreamOf(process)
.map(Long::valueOf)
.collect(toList());
} catch (IOException e) {
throw new UncheckedIOException(
format("Error executing pgrep with user [%s] and command [%s]", user, commandLine), e);
}
}
/**
* 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 #wasPgrepFlagsCheckSuccessful()
* @see #getPgrepFlags()
*/
public static Optional pgrepWithSingleResult(String commandLine) {
return pgrepWithSingleResult(null, 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 #wasPgrepFlagsCheckSuccessful()
* @see #getPgrepFlags()
*/
public static Optional pgrepWithSingleResult(String user, String commandLine) {
List pids = pgrep(user, commandLine);
if (pids.isEmpty()) {
return Optional.empty();
}
checkState(pids.size() == 1, "Expecting exactly one result pid for command [%s], but received %s: %s",
commandLine, pids.size(), pids);
return Optional.of(first(pids));
}
/**
* 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 #pgrepList(String, String)
* @see #wasPgrepFlagsCheckSuccessful()
* @see #getPgrepFlags()
*/
public static List pgrepList(String commandLine) {
return pgrepList(null, 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 #wasPgrepFlagsCheckSuccessful()
* @see #getPgrepFlags()
*/
public static List pgrepList(String user, String commandLine) {
try {
List command = buildPgrepListCommand(user, commandLine);
var process = launchProcessInternal(command);
return readLinesFromInputStreamOf(process);
} catch (IOException e) {
throw new UncheckedIOException(
format("Error executing pgrep with user [%s] and command [%s]", user, commandLine), e);
}
}
/**
* 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 #pgrepParsedList(String, String)
* @see #wasPgrepFlagsCheckSuccessful()
* @see #getPgrepFlags()
*/
public static List> pgrepParsedList(String commandLine) {
return pgrepParsedList(null, 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 #wasPgrepFlagsCheckSuccessful()
* @see #getPgrepFlags()
*/
public static List> pgrepParsedList(String user, String commandLine) {
List lines = pgrepList(user, commandLine);
return lines.stream().map(Processes::pairFromPgrepLine).collect(toList());
}
private static Pair pairFromPgrepLine(String line) {
List splat = splitToList(line, SPACE, 2);
return Pair.of(Long.valueOf(first(splat)), second(splat));
}
private static List buildPgrepCommand(String user, String commandLine) {
return buildPgrepCommand(user, commandLine, "-f");
}
private static List buildPgrepListCommand(String user, String commandLine) {
return buildPgrepCommand(user, commandLine, PGREP_FULL_COMMAND_MATCH_AND_PRINT_FLAGS);
}
private static List buildPgrepCommand(String user, String commandLine, String flags) {
checkArgument(doesNotContainWhitespace(flags),
"Currently only short flags specified together with no whitespace" +
" are supported, e.g. -fl and NOT -f -l. Offending flags: %s", flags);
if (StringUtils.isBlank(user)) {
return Lists.newArrayList(PGREP_COMMAND, flags, commandLine);
}
return Lists.newArrayList(PGREP_COMMAND, "-u", user, flags, commandLine);
}
private static boolean doesNotContainWhitespace(String string) {
return !string.contains(" ");
}
/**
* Kill a process, waiting up to {@link #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 #kill(long, String, long, TimeUnit, KillTimeoutAction)
*/
public static int kill(long processId, KillSignal signal, KillTimeoutAction action) {
return kill(processId, signal.number(), action);
}
/**
* Kill a process, waiting up to {@link #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
*/
public static int kill(long processId, String signal, KillTimeoutAction action) {
return kill(processId, signal, DEFAULT_KILL_TIMEOUT_SECONDS, TimeUnit.SECONDS, 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 #kill(long, String, long, TimeUnit, KillTimeoutAction)
*/
public static int kill(long processId, KillSignal signal, long timeout, TimeUnit unit, KillTimeoutAction action) {
return kill(processId, signal.number(), timeout, unit, 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
*/
public static int kill(long processId, String signal, long timeout, TimeUnit unit, KillTimeoutAction action) {
try {
var killProcess = launchProcessInternal("kill", KillSignal.withLeadingDash(signal), String.valueOf(processId));
return killInternal(processId, killProcess, timeout, unit, action);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static Process launchProcessInternal(String... commandLine) throws IOException {
return launchProcessInternal(Lists.newArrayList(commandLine));
}
private static Process launchProcessInternal(List command) throws IOException {
return new ProcessBuilder(command).start();
}
/**
* 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 Process#destroyForcibly()
* @see Process#waitFor(long, TimeUnit)
*/
public static boolean killForcibly(Process process, long timeout, TimeUnit unit) throws InterruptedException {
return process.destroyForcibly().waitFor(timeout, unit);
}
@VisibleForTesting
static int killInternal(long processId, Process killProcess, long timeout, TimeUnit unit, KillTimeoutAction action) {
try {
boolean exitedBeforeWaitTimeout = killProcess.waitFor(timeout, unit);
if (exitedBeforeWaitTimeout) {
return killProcess.exitValue();
}
return doTimeoutAction(action, killProcess, processId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new UncheckedInterruptedException(e);
}
}
private static int doTimeoutAction(KillTimeoutAction action, Process killProcess, long processId)
throws InterruptedException {
switch (action) {
case FORCE_KILL:
boolean killedBeforeWaitTimeout = killForcibly(killProcess, 1L, TimeUnit.SECONDS);
validateKilledBeforeTimeout(processId, killedBeforeWaitTimeout);
return killProcess.exitValue();
case THROW_EXCEPTION:
throw new IllegalStateException(
format("Process %s did not end before timeout (and exception was requested)", processId));
case NO_OP:
LOG.warn("Process {} did not end before timeout and no-op action requested, so doing nothing", processId);
return -1;
default:
throw new IllegalStateException("Unaccounted for action: " + action);
}
}
private static void validateKilledBeforeTimeout(long processId, boolean killedBeforeWaitTimeout) {
if (!killedBeforeWaitTimeout) {
throw new IllegalStateException(
format("Process %s was not killed before 1 second timeout expired", processId));
}
}
}