/************************************************************************* * * * 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); } } } }