Skip to content

Instantly share code, notes, and snippets.

@sanyarnd
Created July 18, 2019 14:13
Show Gist options
  • Select an option

  • Save sanyarnd/c28aac4d7293df77c2429ac9b4a4032a to your computer and use it in GitHub Desktop.

Select an option

Save sanyarnd/c28aac4d7293df77c2429ac9b4a4032a to your computer and use it in GitHub Desktop.
Execute native binary and glob stderr/stdout output on JVM
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.checkerframework.checker.nullness.qual.NonNull;
import lombok.extern.slf4j.Slf4j;
/**
* Native binary executor.
*
* @author Alexander Biryukov
*/
@Slf4j
public final class ProcessExecutor {
@NonNull
private final Path executable;
/**
* Construct native executor.
*
* @param pathToExecutable the path to the executable.
*/
public ProcessExecutor(@NonNull final Path pathToExecutable) {
this.executable = pathToExecutable;
}
/**
* Run the executable and collect stderr/stdout outputs with return code.
*
* @param arguments the list of arguments.
* @return the process output, stdout, stderr and exit code.
* @throws InterruptedException if {@link Process#waitFor()} or {@link Thread#join()} was interrupted.
* @throws IOException if any error happens during process execution.
*/
@NonNull
public ProcessOutput execute(@NonNull final List<String> arguments) throws InterruptedException, IOException {
// build command line query
final List<String> cmd = new ArrayList<>(arguments);
cmd.add(0, executable.toAbsolutePath().toString());
final File workingDir = executable.getParent().toFile();
final ProcessBuilder processBuilder = new ProcessBuilder(cmd).directory(workingDir);
final List<String> cmdFull = processBuilder.command();
log.debug("Executing {}", cmdFull);
final StringBuilder stderr = new StringBuilder();
final StringBuilder stdout = new StringBuilder();
final Process process = processBuilder.start();
// we start two parallel threads because:
// if there is no error, stderr BufferedReader#readLine will block
// if there is no output, stdout BufferedReader#readLine will block
// code is single-threaded, we are stuck
final Path binaryFilename = executable.getFileName();
final Thread threadErr = buildProcessOutputThread(process.getErrorStream(), stderr,
"%s: stderr",
"Process execution stderr error " + binaryFilename
);
final Thread threadOut = buildProcessOutputThread(process.getInputStream(), stdout,
"%s: stdout",
"Process execution stdout error " + binaryFilename
);
threadErr.setDaemon(true);
threadOut.setDaemon(true);
threadErr.start();
threadOut.start();
threadErr.join();
threadOut.join();
int exitCode = process.waitFor();
log.debug("Execution finished {}", cmdFull);
ProcessOutput output = new ProcessOutput(stdout.toString(), stderr.toString(), exitCode);
if (!output.getStderr().isEmpty()) {
log.info("stderr is not empty {}: {}", cmdFull, output.getStderr());
}
return output;
}
@NonNull
private Thread buildProcessOutputThread(@NonNull final InputStream inputStream, @NonNull final StringBuilder sb,
@NonNull final String threadName, @NonNull final String errorLogMessage) {
return new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
while (true) {
String line = reader.readLine();
if (line == null) break;
sb.append(line);
}
} catch (IOException ex) {
log.error(errorLogMessage, ex);
}
}, String.format(threadName, executable.getFileName()));
}
}
import org.checkerframework.checker.nullness.qual.NonNull;
import lombok.Value;
/**
* Captured result of execution.
*
* @author Alexander Biryukov
*/
@Value
public final class ProcessOutput {
@NonNull
private String stdout;
@NonNull
private String stderr;
private int exitCode;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment