class ExternalProcessRunner {

    public ProcessResult execute(String command, String... args) throws ProcessNotExecutableException,
            ProcessFailedException, InterruptedException {
        if (StringUtils.isEmpty(command)) {
            throw new IllegalArgumentException("command cannot be null");
        }
        List<String> commandWithArgs = new ArrayList<>();
        commandWithArgs.add(command);
        if (args != null && args.length > 0) {
            commandWithArgs.addAll(Arrays.asList(args));
        }
        String commandWithArgsString = String.join(" ", commandWithArgs);
        log.info("Running an external command: " + commandWithArgsString);
        Process process;
        ProcessBuilder pb = new ProcessBuilder(commandWithArgs);
        // redirect process errors into process's input stream
        pb.redirectErrorStream(true);
        try {
            process = pb.start();
        } catch (IOException e) {
            log.error("Error starting external process: " + command, e);
            throw new ProcessNotExecutableException(e);
        }

        // gather output into a list of string for returning
        List<String> processOutput = new ArrayList<>();
        try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            br.lines().forEach(processOutput::add);
        } catch (IOException e) {
            process.destroy();
            IOUtils.closeQuietly(process.getErrorStream());
            IOUtils.closeQuietly(process.getOutputStream());
            log.error("External command not executable: " + commandWithArgsString, e);
            throw new ProcessNotExecutableException(e);
        }

        int exitCode;

        try {
            boolean hasExited = process.waitFor(TIMEOUT, TimeUnit.MILLISECONDS);
            // exit value cannot be asked if the process is still running after timeout - instead throw and destroy
            if (!hasExited) {
                throw new ProcessFailedException("Process timed out");
            }
            exitCode = process.exitValue();
            log.info("External command finished with exit status {}", exitCode);
        } catch (InterruptedException e) {
            // retain the interrupted status
            Thread.currentThread().interrupt();
            throw e;
        } finally {
            // always destroy the process
            process.destroy();
            IOUtils.closeQuietly(process.getInputStream());
            IOUtils.closeQuietly(process.getErrorStream());
            IOUtils.closeQuietly(process.getOutputStream());
        }
        ProcessResult processResult = new ProcessResult(commandWithArgsString, exitCode, processOutput);
        return processResult;
    }

}