/* * Copyright (C) 2013 RoboVM AB * * 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 2 * 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/gpl-2.0.html>. */ package org.robovm.compiler.util; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.exec.CommandLine; import org.apache.commons.exec.DefaultExecutor; import org.apache.commons.exec.ExecuteException; import org.apache.commons.exec.ExecuteStreamHandler; import org.apache.commons.exec.PumpStreamHandler; import org.apache.commons.exec.environment.EnvironmentUtils; import org.apache.commons.exec.util.StringUtils; import org.robovm.compiler.log.ErrorOutputStream; import org.robovm.compiler.log.InfoOutputStream; import org.robovm.compiler.log.Logger; import org.robovm.compiler.target.Launcher; import org.robovm.compiler.util.io.NeverCloseOutputStream; /** * Builder style wrapper around <code>commons-exec</code> which also adds support for asynchronous * execution. */ public class Executor implements Launcher { private final String cmd; private final Logger logger; private List<String> args = new ArrayList<String>(); private Map<String, String> env = new HashMap<String, String>(); private boolean inheritEnv = true; private File wd; private OutputStream out; private OutputStream err; private InputStream in; private boolean closeOutputStreams = false; private ExecuteStreamHandler streamHandler = null; /** * Creates a new instance which will execute the specified command. * * @param logger {@link Logger} used by this {@link Executor}. * @param cmd the command to be executed. Either the full path to an executable or the name of * an executable which will be searched for in the search paths specified by the * <code>PATH</code> environment variable. */ public Executor(Logger logger, String cmd) { this.logger = logger; this.cmd = cmd; } /** * Creates a new instance which will execute the specified command. * * @param logger {@link Logger} used by this {@link Executor}. * @param cmd the command to be executed. */ public Executor(Logger logger, File cmd) { this.logger = logger; this.cmd = cmd.getAbsolutePath(); } /** * Adds arguments from the specified {@link Collection}. {@link File} arguments will be * converted to absolute paths using {@link File#getAbsolutePath()}. All other types of args * will be converted to {@link String}s using {@link Object#toString()}. * * @param args the arguments to add. * @return this {@link Executor}. */ public Executor args(Collection<Object> args) { if (!args.isEmpty()) { return args(args.toArray(new Object[args.size()])); } return this; } /** * Adds one or more argument. {@link File} arguments will be converted to absolute paths using * {@link File#getAbsolutePath()}. All other types of args will be converted to {@link String}s * using {@link Object#toString()}. * * @param args the argument(s) to add. * @return this {@link Executor}. */ @SuppressWarnings("unchecked") public Executor args(Object ... args) { for (Object a : args) { if (a instanceof Collection) { args((Collection<Object>) a); } else if (a instanceof Object[]) { args((Object[]) a); } else { this.args.add(a instanceof File ? ((File) a).getAbsolutePath() : a.toString()); } } return this; } /** * Sets the environment variables for the child process. * * @param env the environment variables. * @return this {@link Executor}. */ public Executor env(Map<String, String> env) { this.env = env; return this; } /** * Adds a single environment variable. * * @param env the environment variables. * @return this {@link Executor}. */ public Executor addEnv(String name, String value) { this.env.put(name, value); return this; } /** * Sets whether the parent's environment variables should be inherited by the child process. * Defaults to <code>true</code>. * * @param b <code>true</code> or <code>false</code>. * @return this {@link Executor}. */ public Executor inheritEnv(boolean b) { this.inheritEnv = b; return this; } /** * Sets the working directory of the child process. If not set the working directory will be * the same as the parent's. * * @param wd the working directory. * @return this {@link Executor}. */ public Executor wd(File wd) { this.wd = wd; return this; } /** * Redirects the stdout and stderr streams of the child process to the specified * {@link OutputStream}. If not specified stdout and stderr will be inherited from the * parent process. * * @param out the {@link OutputStream}. * @return this {@link Executor}. */ public Executor errOut(OutputStream out) { this.out = out; this.err = out; return this; } /** * Redirects the stdout stream of the child process to the specified * {@link OutputStream}. If not specified stdout will be inherited from the * parent process. * * @param out the {@link OutputStream}. * @return this {@link Executor}. */ public Executor out(OutputStream out) { this.out = out; return this; } /** * Redirects the stderr stream of the child process to the specified * {@link OutputStream}. If not specified stderr will be inherited from the * parent process. * * @param err the {@link OutputStream}. * @return this {@link Executor}. */ public Executor err(OutputStream err) { this.err = err; return this; } /** * Uses the specified {@link InputStream} as the stdin stream for the child process. * * @param in the {@link InputStream}. * @return this {@link Executor}. */ public Executor in(InputStream in) { this.in = in; return this; } /** * Sets the {@link ExecuteStreamHandler} to be used by the underlying * {@link org.apache.commons.exec.Executor}. If set any streams set by {@link #out(OutputStream)}, * {@link #err(OutputStream)}, {@link #errOut(OutputStream)} or {@link #in(InputStream)} will be * ignored. * * @param streamHandler the {@link ExecuteStreamHandler} to be used. * @return this {@link Executor}. */ public Executor streamHandler(ExecuteStreamHandler streamHandler) { this.streamHandler = streamHandler; return this; } /** * Sets whether the stdout and stderr {@link OutputStream}s should be closed after the command * has finished. * * @param b <code>true</code> or <code>false</code>. */ public Executor closeOutputStreams(boolean b) { this.closeOutputStreams = b; return this; } private CommandLine generateCommandLine() { CommandLine commandLine = new CommandLine(cmd); for (String arg : args) { commandLine.addArgument(arg, false); } return commandLine; } @SuppressWarnings("unchecked") private Map<String, String> generateEnv() throws IOException { Map<String, String> mergedEnv = new HashMap<String, String>(); if (inheritEnv) { mergedEnv.putAll(EnvironmentUtils.getProcEnvironment()); } mergedEnv.putAll(env); return mergedEnv; } private <T extends org.apache.commons.exec.Executor> T initExecutor(T executor) { if (streamHandler == null) { OutputStream pumpOut = null; OutputStream pumpErr = null; InputStream pumpIn = null; if (out != null) { pumpOut = out; } else { pumpOut = new InfoOutputStream(logger); } if (err != null) { pumpErr = err; } else { pumpErr = new ErrorOutputStream(logger); } if (in != null) { pumpIn = in; } if (pumpOut == System.out) { pumpOut = new NeverCloseOutputStream(pumpOut); } if (pumpErr == System.err) { pumpErr = new NeverCloseOutputStream(pumpErr); } executor.setStreamHandler(new PumpStreamHandler(pumpOut, pumpErr, pumpIn) { @Override protected Thread createPump(InputStream is, OutputStream os, boolean closeWhenExhausted) { return super.createPump(is, os, closeOutputStreams ? true : closeWhenExhausted); } }); } else { executor.setStreamHandler(streamHandler); } if (wd != null) { executor.setWorkingDirectory(wd); } executor.setExitValue(0); return executor; } private void logCommandLine(CommandLine commandLine) { if (logger == null) { return; } String[] args = commandLine.getArguments(); if (args.length == 0) { logger.info(commandLine.toString()); return; } StringBuilder result = new StringBuilder(); result.append(StringUtils.quoteArgument(commandLine.getExecutable())); result.append(' '); boolean first = true; for (int i = 0; i < args.length; i++) { String currArgument = args[i]; if( StringUtils.isQuoted(currArgument)) { result.append(currArgument); } else { result.append(StringUtils.quoteArgument(currArgument)); } if (i<args.length-1) { result.append(' '); } if (i == args.length - 1 || result.length() > 2048) { logger.info((first ? "" : " ") + result.toString()); result.delete(0, result.length()); first = false; } } } public int exec() throws ExecuteException, IOException { CommandLine commandLine = generateCommandLine(); logCommandLine(commandLine); try { return initExecutor(new DefaultExecutor()).execute(commandLine, generateEnv()); } catch (ExecuteException e) { ExecuteException ex = new ExecuteException("Command '" + commandLine + "' failed ", e.getExitValue()); ex.setStackTrace(e.getStackTrace()); throw ex; } } public Process execAsync() throws IOException { CommandLine commandLine = generateCommandLine(); logCommandLine(commandLine); return initExecutor(new AsyncExecutor()).executeAsync(commandLine, generateEnv()); } public String execCapture() throws IOException { ExecuteStreamHandler oldStreamHandler = streamHandler; ByteArrayOutputStream baos = new ByteArrayOutputStream(); CommandLine commandLine = generateCommandLine(); try { streamHandler(new PumpStreamHandler(baos)); logCommandLine(commandLine); DefaultExecutor executor = initExecutor(new DefaultExecutor()); executor.execute(commandLine, generateEnv()); return new String(baos.toByteArray()).trim(); } catch (ExecuteException e) { String output = new String(baos.toByteArray()).trim(); if (output.length() > 0 && e.getMessage().startsWith("Process exited with an error")) { StackTraceElement[] origStackTrace = e.getStackTrace(); e = new ExecuteException("Command '" + commandLine + "' failed with output: " + output + " ", e.getExitValue()); e.setStackTrace(origStackTrace); } throw e; } finally { streamHandler = oldStreamHandler; } } }