4

How can I obtain a process' output while setting a timeout value?

I am currently using Apache Commons IO utils to create a string from the process' standard and error outputs.

The code below, as is (with the comments), works fine for processes that terminate. However, if the process doesn't terminate, the main thread doesn't terminate either!

If I uncomment out the commented code and instead comment out process.waitfor(), the method will properly destroy non terminating processes. However, for terminating processes, the output isn't properly obtained. It appears that once waitFor is completed, I cannot get the process' input and error streams?

Finally, if I attempt to move the commented section to where process.waitFor() currently is, remove process.waitFor() and uncomment the commented section, then for non terminating processes, the main thread also won't stop. This is because the process.waitFor(15, ...) will never be reached.

private static Outputs runProcess(String command) throws Exception {
    Process process = Runtime.getRuntime().exec(command);

    // if (!process.waitFor(15, TimeUnit.SECONDS)) {
    // System.out.println("Destroy");
    // process.destroy();
    // }

    // Run and collect the results from the standard output and error output
    String stdStr = IOUtils.toString(process.getInputStream());
    String errStr = IOUtils.toString(process.getErrorStream());

    process.waitFor();

    return new Outputs(stdStr, errStr);
}
Lii
  • 11,553
  • 8
  • 64
  • 88
waylonion
  • 6,866
  • 8
  • 51
  • 92

2 Answers2

8

As @EJP suggested, You can use different threads to capture the streams or use ProcessBuilder or redirect to a file from your command.
Here are 3 approaches that I feel you can use.

  1. Using different threads for Streams.

    Process process = Runtime.getRuntime().exec("cat ");
    
    ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(2);
    
    Future<String> output = newFixedThreadPool.submit(() -> {
        return IOUtils.toString(process.getInputStream());
    });
    Future<String> error = newFixedThreadPool.submit(() -> {
        return IOUtils.toString(process.getErrorStream());
    });
    
    newFixedThreadPool.shutdown();
    
    // process.waitFor();
    if (!process.waitFor(3, TimeUnit.SECONDS)) {
        System.out.println("Destroy");
        process.destroy();
    }
    
    System.out.println(output.get());
    System.out.println(error.get());
    
  2. Using ProcessBuilder

    ProcessBuilder processBuilder = new ProcessBuilder("cat")
            .redirectError(new File("error"))
            .redirectOutput(new File("output"));
    
    Process process = processBuilder.start();
    
    // process.waitFor();
    if (!process.waitFor(3, TimeUnit.SECONDS)) {
        System.out.println("Destroy");
        process.destroy();
    }
    
    System.out.println(FileUtils.readFileToString(new File("output")));
    System.out.println(FileUtils.readFileToString(new File("error")));
    
  3. Use a redirection operator in your command to redirect Output & Error to a file and then Read from File.

Here is very good blog which explains different ways of handling Runtime.Exec

Kishore Bandi
  • 5,537
  • 2
  • 31
  • 52
  • In your first example. you use threadPool.shutdown(). The javadoc is a little bit confusing. _Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted_ vs _This method does not wait for previously submitted tasks to complete execution. Use awaitTermination to do that._ So does this terminte the thread early? – lrxw Jun 30 '20 at 04:58
  • 1
    @Irxw `Shutdown` will only tell the executor to stop accepting new requests. However existing/Submitted task will continue running without interruption. Also, Calling `Shutdown` will NOT block your call to wait for all Submitted Tasks to finish. To ensure you want to wait for all Tasks to be finished, Invoke `Shutdown` and then Invoke `AwaitTermination`. There is `ShutdownNow` which does this on Best-effort basis by mostly interrupting waiting/running threads, but even that doesn't guarantee the submitted tasks will be killed/interrupted. – Kishore Bandi Jun 30 '20 at 09:01
  • Thx, I understood "does not wait" as "it kills it". But it's just saying its not blocking and waiting for it to complete. But it's still graceful. – lrxw Jun 30 '20 at 14:58
0

This is a slightly adjusted version of Kishore Bandi's first solution which uses separate thread to capture output.

It has been simplified, uses no external libraries and have more robust termination code.

Process process = new ProcessBuilder("cat", "file.txt")
    .redirectErrorStream(true)
    .start();

System.out.println("Output: " + waitForOuput(process, Duration.ofSeconds(10)));
/**
 * Waits {@code timeout} time for the output of
 * {@code process.getInputStream()}. Returns when the process is terminated.
 * Throws on non-zero exit value.
 */
public static String waitForOuput(Process process, Duration timeout) throws InterruptedException, TimeoutException {
    ExecutorService pool = Executors.newFixedThreadPool(1);
    Future<String> outputFuture = pool.submit(() -> new String(process.getInputStream().readAllBytes()));
    pool.shutdown();

    try {
        String output = outputFuture.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
        if (process.exitValue() != 0) {
            throw new IllegalStateException("Command " + process.info().command()
                + " returned exit value " + process.exitValue());
        }
        return output;
    } catch (ExecutionException | TimeoutException | InterruptedException ex) {
        process.destroyForcibly();
        outputFuture.cancel(true);
        process.waitFor(1, TimeUnit.SECONDS); // Give process time to die

        if (ex instanceof InterruptedException intEx) {
            throw intEx;
        } else if (ex instanceof TimeoutException timeEx) {
            throw timeEx;
        } else {
            throw new IllegalStateException(ex);
        }
    }
}
Lii
  • 11,553
  • 8
  • 64
  • 88