How to properly handle the output of the ProcessBuilder

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.

,

  1. Leave a comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: