Skip to content

Commit

Permalink
Add ability to launch processes with a working directory (#741)
Browse files Browse the repository at this point in the history
* Add overloads of Processes#launch and ProcessHelper#launch that
  accept a File representing the working directory and a List that
  contains the command. Did not overload the launch methods that accept
  varargs because there can be ambiguity as to which method is called.
  For example, launch(null, "git", "status") is ambiguous since both
  launch(String...) and launch(File, String...) can apply and that
  would force ugly casts, etc.
* Update javadocs for Processes#waitForExit noting that we do not
  destroy the Process if the timeout occurs before the process exits

Closes #739
  • Loading branch information
sleberknight authored Jun 23, 2022
1 parent 3525938 commit a023f4e
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 12 deletions.
14 changes: 14 additions & 0 deletions src/main/java/org/kiwiproject/base/process/ProcessHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.tuple.Pair;

import javax.annotation.Nullable;
import java.io.File;
import java.io.UncheckedIOException;
import java.util.Collection;
import java.util.List;
Expand Down Expand Up @@ -61,6 +63,18 @@ public Process launch(List<String> command) {
return Processes.launch(command);
}

/**
* Launches a new process using the specified {@code workingDirectory} and {@code command}.
*
* @param workingDirectory the working directory to use
* @param command the list containing the program and its arguments
* @return the new {@link Process}
* @see Processes#launch(File, List)
*/
public Process launch(@Nullable File workingDirectory, List<String> command) {
return Processes.launch(workingDirectory, command);
}

/**
* Launches a new process using the specified {@code command}.
*
Expand Down
36 changes: 34 additions & 2 deletions src/main/java/org/kiwiproject/base/process/Processes.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.kiwiproject.base.UncheckedInterruptedException;

import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.List;
Expand Down Expand Up @@ -278,6 +279,8 @@ public static boolean isNonzeroExitCode(int exitCode) {

/**
* Waits up to {@link #DEFAULT_WAIT_FOR_EXIT_TIME_SECONDS} for the given process to exit.
* <p>
* Note that this method does <em>not</em> destroy the process if it times out waiting.
*
* @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
Expand All @@ -289,6 +292,8 @@ public static Optional<Integer> waitForExit(Process process) {

/**
* Waits up to the specified {@code timeout} for the given process to exit.
* <p>
* Note that this method does <em>not</em> destroy the process if it times out waiting.
*
* @param process the process to wait for
* @param timeout the value of the time to wait
Expand Down Expand Up @@ -321,8 +326,28 @@ public static Optional<Integer> waitForExit(Process process, long timeout, TimeU
* @see ProcessBuilder#start()
*/
public static Process launch(List<String> command) {
return launch(null, command);
}

/**
* Launches a new process using the specified {@code command} with the given working directory.
* This is just a convenience wrapper around creating a new {@link ProcessBuilder}, setting the
* {@link ProcessBuilder#directory(File) working directory}, and calling {@link ProcessBuilder#start()}.
* <p>
* This wrapper converts any thrown {@link IOException} to an {@link UncheckedIOException}.
* <p>
* <em>If you need more flexibility than provided in this simple wrapper, use {@link ProcessBuilder} directly.</em>
*
* @param workingDirectory the working directory to use
* @param command the list containing the program and its arguments
* @return the new {@link Process}
* @see ProcessBuilder#ProcessBuilder(List)
* @see ProcessBuilder#directory(File)
* @see ProcessBuilder#start()
*/
public static Process launch(@Nullable File workingDirectory, List<String> command) {
try {
return launchProcessInternal(command);
return launchProcessInternal(workingDirectory, command);
} catch (IOException e) {
throw new UncheckedIOException("Error launching command: " + command, e);
}
Expand Down Expand Up @@ -571,7 +596,14 @@ private static Process launchProcessInternal(String... commandLine) throws IOExc
}

private static Process launchProcessInternal(List<String> command) throws IOException {
return new ProcessBuilder(command).start();
return launchProcessInternal(null, command);
}

private static Process launchProcessInternal(@Nullable File workingDirectory,
List<String> command) throws IOException {
return new ProcessBuilder(command)
.directory(workingDirectory)
.start();
}

/**
Expand Down
73 changes: 63 additions & 10 deletions src/test/java/org/kiwiproject/base/process/ProcessHelperTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,15 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -86,14 +91,58 @@ void testWaitForExit_WhenInterruptedExceptionThrown() throws InterruptedExceptio
void testLaunch_UsingVarargs() {
var process = processes.launch("sleep", "11");

assertProcessAlive(process);
assertProcessIsAliveThenKill(process);
}

@Test
void testLaunch_UsingList() {
var process = processes.launch(List.of("sleep", "11"));

assertProcessAlive(process);
assertProcessIsAliveThenKill(process);
}

@Test
void shouldLaunch_UsingList_AndNullWorkingDirectory() throws InterruptedException {
var processWithNullDir = processes.launch(null, List.of("ls", "-t"));
var linesFromNullDir = readLinesFromInputStreamOf(processWithNullDir);
waitForSuccessfulExit(processWithNullDir);

var processWithDotDir = processes.launch(new File("."), List.of("ls", "-t"));
var linesFromDotDir = readLinesFromInputStreamOf(processWithDotDir);
waitForSuccessfulExit(processWithDotDir);

assertThat(linesFromNullDir)
.describedAs("files from ls -t should be the same using null working directory and '.' directory")
.isEqualTo(linesFromDotDir);
}

@Test
void shouldLaunch_UsingList_AndWorkingDirectory(@TempDir Path tempDirPath) throws InterruptedException {
writeFile(tempDirPath, "a.txt", "aaa");
writeFile(tempDirPath, "b.txt", "bbb bbb");

var tempDir = tempDirPath.toFile();
var process = processes.launch(tempDir, List.of("ls", "-S"));
var lines = readLinesFromInputStreamOf(process);
waitForSuccessfulExit(process);

assertThat(lines)
.describedAs("files from ls -S should be sorted by b.txt (larger) then a.txt (smaller)")
.containsExactly("b.txt", "a.txt");
}

private static void waitForSuccessfulExit(Process process) throws InterruptedException {
process.waitFor(1, TimeUnit.SECONDS);
assertThat(process.exitValue()).isEqualTo(Processes.SUCCESS_EXIT_CODE);
}

private static void writeFile(Path parentDir, String fileName, String content) {
var absParentDirPath = parentDir.toAbsolutePath().toString();
try {
Files.writeString(Path.of(absParentDirPath, fileName), content, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

@Test
Expand All @@ -106,9 +155,9 @@ void testLaunch_WithBadCommand() {
.withMessageContaining("arguments");
}

private void assertProcessAlive(Process process) {
private void assertProcessIsAliveThenKill(Process process) {
try {
assertThat(process.isAlive()).isTrue();
assertProcessIsAlive(process);
} finally {
if (nonNull(process)) {
long pid = process.pid();
Expand Down Expand Up @@ -292,7 +341,7 @@ private long createSleepingProcess(String seconds) {
@Test
void testKill_WithExplicitTimeout() throws IOException {
var process = new ProcessBuilder("sleep", "55").start();
assertThat(process.isAlive()).isTrue();
assertProcessIsAlive(process);

int exitCode = processes.kill(
process.pid(),
Expand All @@ -307,7 +356,7 @@ void testKill_WithExplicitTimeout() throws IOException {
@Test
void testKill_WithStringSignal_AndExplicitTimeout() throws IOException {
var process = new ProcessBuilder("sleep", "55").start();
assertThat(process.isAlive()).isTrue();
assertProcessIsAlive(process);

int exitCode = processes.kill(
process.pid(),
Expand All @@ -322,7 +371,7 @@ void testKill_WithStringSignal_AndExplicitTimeout() throws IOException {
@Test
void testKill_WithDefaultTimeout() throws IOException {
var process = new ProcessBuilder("sleep", "55").start();
assertThat(process.isAlive()).isTrue();
assertProcessIsAlive(process);

int exitCode = processes.kill(
process.pid(),
Expand All @@ -336,7 +385,7 @@ void testKill_WithDefaultTimeout() throws IOException {
@Test
void testKill_WithStringSignal_AndDefaultTimeout() throws IOException {
var process = new ProcessBuilder("sleep", "55").start();
assertThat(process.isAlive()).isTrue();
assertProcessIsAlive(process);

int exitCode = processes.kill(
process.pid(),
Expand All @@ -350,13 +399,17 @@ void testKill_WithStringSignal_AndDefaultTimeout() throws IOException {
@Test
void testKillForcibly() throws IOException, InterruptedException {
var process = new ProcessBuilder("sleep", "55").start();
assertThat(process.isAlive()).isTrue();
assertProcessIsAlive(process);

boolean killedBeforeTimeout = processes.killForcibly(process, 2500, TimeUnit.MILLISECONDS);

assertThat(killedBeforeTimeout).isTrue();
}

private void assertProcessIsAlive(Process process) {
assertThat(process.isAlive()).isTrue();
}

@Test
void testFindChildProcessId_WhenNoChildFound() {
Optional<Long> childProcessId = processes.findChildProcessId(-1L);
Expand Down Expand Up @@ -489,4 +542,4 @@ public int read() throws IOException {
throw new IOException(message);
}
}
}
}

0 comments on commit a023f4e

Please sign in to comment.