/*************************************************************************
* *
* This file is part of the 20n/act project. *
* 20n/act enables DNA prediction for synthetic biology/bioengineering. *
* Copyright (C) 2017 20n Labs, Inc. *
* *
* Please direct all queries to act@20n.com. *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*************************************************************************/
package com.act.utils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* A simple class to run child processes and log their output.
* TODO: is there a way we can hook into `ShellJob` without having to run inside a workflow?
*/
public class ProcessRunner {
private static final Logger LOGGER = LogManager.getFormatterLogger(ProcessRunner.class);
/**
* Run's a child process using the specified command and arguments.
* @param command The process to run.
* @param args The arguments to pass to that process.
* @return The exit code of the child process.
* @throws InterruptedException
* @throws IOException
*/
public static int runProcess(String command, List<String> args) throws InterruptedException, IOException {
return runProcess(command, args, null);
}
/**
* Run's a child process using the specified command and arguments, timing out after a specified number of seconds
* if the process does not terminate on its own in that time.
* @param command The process to run.
* @param args The arguments to pass to that process.
* @param timeoutInSeconds A timeout to impose on the child process; an InterruptedException is likely to occur
* when the child process times out.
* @return The exit code of the child process.
* @throws InterruptedException
* @throws IOException
*/
public static int runProcess(String command, List<String> args, Long timeoutInSeconds)
throws InterruptedException, IOException {
/* The ProcessBuilder doesn't differentiate the command from its arguments, but we do in our API to ensure the user
* doesn't just pass us a single string command, which invokes the shell and can cause all sorts of bugs and
* security issues. */
List<String> commandAndArgs = new ArrayList<String>(args.size() + 1) {{
add(command);
addAll(args);
}};
ProcessBuilder processBuilder = new ProcessBuilder(commandAndArgs);
LOGGER.info("Running child process: %s", StringUtils.join(commandAndArgs, " "));
Process p = processBuilder.start();
// Log whatever the child writes.
new StreamLogger(p.getInputStream(), l -> LOGGER.info("[child STDOUT]: %s", l)).run();
new StreamLogger(p.getErrorStream(), l -> LOGGER.warn("[child STDERR]: %s", l)).run();
// Wait for the child process to exit, timing out if it takes to long to finish.
if (timeoutInSeconds != null) {
p.waitFor(timeoutInSeconds, TimeUnit.SECONDS);
} else {
p.waitFor();
}
// 0 is the default success exit code in *nix land.
if (p.exitValue() != 0) {
LOGGER.error("Child process exited with non-zero status code: %d", p.exitValue());
}
return p.exitValue();
}
private static class StreamLogger implements Runnable {
/* With help from http://stackoverflow.com/questions/14165517/processbuilder-forwarding-stdout-and-stderr-of-started-processes-without-blocki
* Asynchronously read and log a child process's stdout or stderr. */
private InputStream stream;
private Consumer<String> consumer;
public StreamLogger(InputStream stream, Consumer<String> consumer) {
this.stream = stream;
this.consumer = consumer;
}
@Override
public void run() {
new BufferedReader(new InputStreamReader(stream)).lines().forEach(consumer);
try {
stream.close();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
}