-
Notifications
You must be signed in to change notification settings - Fork 3
Fix #863: add ProcessRunner utility to capabilities-common #89
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| package ai.wanaku.capabilities.sdk.common; | ||
|
|
||
| import java.io.BufferedReader; | ||
| import java.io.File; | ||
| import java.io.IOException; | ||
| import java.io.InputStreamReader; | ||
| import java.util.Map; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import ai.wanaku.capabilities.sdk.api.exceptions.WanakuException; | ||
|
|
||
| /** | ||
| * Convenience utility for running external processes. This is intended for use by capabilities and CLI code. | ||
| * It MUST NOT be used in router code. | ||
| */ | ||
| public class ProcessRunner { | ||
| private static final Logger LOG = LoggerFactory.getLogger(ProcessRunner.class); | ||
|
|
||
| private ProcessRunner() {} | ||
|
|
||
| public static String runWithOutput(String... command) { | ||
| try { | ||
| ProcessBuilder processBuilder = new ProcessBuilder(command); | ||
| // Redirect output and error streams to a pipe | ||
| processBuilder.redirectOutput(ProcessBuilder.Redirect.PIPE); | ||
| processBuilder.redirectError(ProcessBuilder.Redirect.PIPE); | ||
|
|
||
| final Process process = processBuilder.start(); | ||
| LOG.info("Waiting for process to finish..."); | ||
|
|
||
| String output = readOutput(process); | ||
|
|
||
| waitForExit(process); | ||
| return output; | ||
| } catch (IOException e) { | ||
| LOG.error("I/O Error: {}", e.getMessage(), e); | ||
| throw new WanakuException(e); | ||
| } catch (InterruptedException e) { | ||
| LOG.error("Interrupted: {}", e.getMessage(), e); | ||
| throw new WanakuException(e); | ||
| } | ||
| } | ||
|
|
||
| private static String readOutput(Process process) throws IOException { | ||
| // Read the output from the process | ||
| BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); | ||
| StringBuilder output = new StringBuilder(); | ||
| String line; | ||
| while ((line = reader.readLine()) != null) { | ||
|
Comment on lines
+44
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (bug_risk): Close the BufferedReader/InputStream via try-with-resources to avoid leaking OS resources. The try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
StringBuilder output = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
return output.toString();
} |
||
| output.append(line).append("\n"); | ||
| } | ||
| return output.toString(); | ||
| } | ||
|
|
||
| public static void run(File directory, String... command) { | ||
| run(directory, null, command); | ||
| } | ||
|
|
||
| public static void run(File directory, Map<String, String> environmentVariables, String... command) { | ||
| try { | ||
| LOG.info("About to run command: {}", String.join(" ", command)); | ||
| ProcessBuilder processBuilder = new ProcessBuilder(); | ||
| processBuilder.command(command).inheritIO().directory(directory); | ||
|
|
||
| if (environmentVariables != null) { | ||
| processBuilder.environment().putAll(environmentVariables); | ||
| } | ||
|
|
||
| final Process process = processBuilder.start(); | ||
|
|
||
| LOG.info("Waiting for process to finish..."); | ||
| waitForExit(process); | ||
| } catch (IOException e) { | ||
| LOG.error("I/O Error: {}", e.getMessage(), e); | ||
| throw new WanakuException(e); | ||
| } catch (InterruptedException e) { | ||
| LOG.error("Interrupted: {}", e.getMessage(), e); | ||
| throw new WanakuException(e); | ||
| } | ||
| } | ||
|
|
||
| private static void waitForExit(Process process) throws InterruptedException { | ||
| Thread thread = new Thread(() -> { | ||
| if (process.isAlive()) { | ||
| process.destroy(); | ||
| } | ||
| }); | ||
|
|
||
| try { | ||
| Runtime.getRuntime().addShutdownHook(thread); | ||
|
|
||
| final int ret = process.waitFor(); | ||
| if (ret != 0) { | ||
| LOG.warn("Process did not execute successfully: (return status {})", ret); | ||
| } | ||
| } finally { | ||
| if (!process.isAlive()) { | ||
| Runtime.getRuntime().removeShutdownHook(thread); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,18 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| package ai.wanaku.capabilities.sdk.common; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.junit.jupiter.api.Test; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import static org.junit.jupiter.api.Assertions.assertFalse; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import static org.junit.jupiter.api.Assertions.assertNotNull; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class ProcessRunnerTest { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| void runWithOutput_returnsCommandOutput() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (testing): Add a test for Because |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| String output = ProcessRunner.runWithOutput("echo", "hello"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertNotNull(output, "Output should not be null"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertFalse(output.isEmpty(), "Output should not be empty"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertTrue(output.contains("hello"), "Output should contain 'hello'"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (testing): Consider adding a test that covers stderr handling in Currently we only cover a stdout-only command. Since Suggested implementation: import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertFalse;
import org.junit.jupiter.api.Test;
public class ProcessRunnerTest {
@Test
void runWithOutput_returnsCommandOutput() {
String output = ProcessRunner.runWithOutput("echo", "hello");
assertNotNull(output, "Output should not be null");
assertFalse(output.isEmpty(), "Output should not be empty");
assertTrue(output.contains("hello"), "Output should contain 'hello'");
}
@Test
void runWithOutput_ignoresStderrAndReturnsOnlyStdout() {
String output = ProcessRunner.runWithOutput(
"sh",
"-c",
"echo stdout && echo stderr 1>&2"
);
assertNotNull(output, "Output should not be null");
assertFalse(output.isEmpty(), "Output should not be empty");
assertTrue(output.contains("stdout"), "Output should contain only stdout content");
assertFalse(output.contains("stderr"), "Output should not contain stderr content");
}
}If your test environment does not guarantee a POSIX-compatible |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+10
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (testing): The test command may not be fully portable across platforms Using
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): Reading only stdout while piping stderr risks deadlock for verbose error output.
In
runWithOutput, both stdout and stderr are piped but only stdout is read. If the subprocess writes heavily to stderr, its buffer can fill and block the process sowaitFor()never returns. Consider either usingredirectErrorStream(true)and reading a single stream, or consuming stderr concurrently (e.g., separate thread) / redirecting it to inheritIO or logging instead of a pipe.