Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to launch processes with a working directory #741

Merged
merged 1 commit into from
Jun 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
sleberknight marked this conversation as resolved.
Show resolved Hide resolved
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);
}
}
}
}