It’s time for a rewrite of my old post https://thilosdevblog.wordpress.com/2011/11/21/proper-handling-of-the-processbuilder, because since Java 9 there are some new nice features, which make things easier.
There are many topics around the ProcessBuilder. In this post the focus is how to handle the output of executed commands correctly!
The following two things are important:
- Consuming STDOUT and STDERR is necessary to avoid freezing!
- If the process writes a large amount of output, it must be consumed.
This can be done by calling #redirectErrorStream, which redirects STDERR to STDOUT!
Have look at my ProcessBuilderWrapper.
/**
* GPL
*/
public class ProcessBuilderWrapper {
private String[] command;
private File workingDirectory;
// exit value of the process
private int status;
private boolean redirectToStdout;
private boolean redirectErrorStream;
private ByteArrayOutputStream outStd = new ByteArrayOutputStream();
private ByteArrayOutputStream outError = new ByteArrayOutputStream();
// environment variables, that are visible to the process
private Map<String, String> environment = null;
public ProcessBuilderWrapper(String... command) {
this.command = command;
}
public void run() throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder(command);
if(workingDirectory != null)
pb.directory(workingDirectory);
if(environment != null && environment.size() > 0)
pb.environment().putAll(environment);
pb.redirectErrorStream(redirectErrorStream);
Process process = pb.start();
try (var infoStream = process.getInputStream(); var errorStream = process.getErrorStream()) {
if(redirectToStdout) {
infoStream.transferTo(System.out);
errorStream.transferTo(System.out);
} else {
infoStream.transferTo(this.outStd);
errorStream.transferTo(this.outError);
}
}
status = process.waitFor();
}
public void redirectToStdOut() {
this.redirectToStdout = true;
}
public void redirectErrorStream() {
this.redirectErrorStream = true;
}
public void setWorkingDirectory(String dir) {
this.workingDirectory = Paths.get(dir).toFile();
}
public void setEnvironment(Map<String, String> environment) {
this.environment = environment;
}
public int getStatus() {
return status;
}
public String getOutput() {
return (redirectToStdout) ? "n/a" : outStd.toString();
}
public String getError() {
return (redirectToStdout) ? "n/a" : outError.toString();
}
public boolean hasErrors() {
return getStatus() != 0;
}
}
Usage for regular commands
To run a regular command, you just have to do something like this:
ProcessBuilderWrapper pbw = new ProcessBuilderWrapper("ls", "-al");
pbw.setWorkingDirectory("/tmp");
pbw.redirectErrorStream();
pbw.run();
if(!pbw.hasErrors())
System.out.println(pbw.getOutput());
Time consuming commands
For time consuming commands it could be important to see some progress information or output of the command. In this case, the inputstreams of the Process can be redirected to standard out.
To test my ProcessBuilderWrapper, I used the following time consuming bash script (sleep.sh):
#!/bin/bash
for i in {1..10} ; do
echo "Step: ${i}"
sleep 3s
done
The call of the ProcessBuilderWrapper:
ProcessBuilderWrapper pbw = new ProcessBuilderWrapper("bash", "sleep.sh");
pbw.setWorkingDirectory([path_to_the_script]);
pbw.redirectToStdOut();
pbw.run();
if(!pbw.hasErrors())
System.out.println(pbw.getOutput());
Error case
To test the error case, I used the following bash script (error.sh):
#!/bin/bash
echo "Text on Stdtout"
echo "Text on StdtErr1" >/dev/stderr
echo "Text on StdtErr2" >&2
exit 1
And the call of the ProcessBuilderWrapper:
ProcessBuilderWrapper pbw = new ProcessBuilderWrapper("bash", "error.sh");
pbw.setWorkingDirectory([path_to_the_script]);
pbw.redirectToStdOut();
pbw.run();
if(!pbw.hasErrors())
System.out.println(pbw.getOutput());
It’s tested with mascOS and Linux.