001/* 002 * SonarQube, open source software quality management tool. 003 * Copyright (C) 2008-2013 SonarSource 004 * mailto:contact AT sonarsource DOT com 005 * 006 * SonarQube is free software; you can redistribute it and/or 007 * modify it under the terms of the GNU Lesser General Public 008 * License as published by the Free Software Foundation; either 009 * version 3 of the License, or (at your option) any later version. 010 * 011 * SonarQube is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014 * Lesser General Public License for more details. 015 * 016 * You should have received a copy of the GNU Lesser General Public License 017 * along with this program; if not, write to the Free Software Foundation, 018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 019 */ 020package org.sonar.api.utils.command; 021 022import com.google.common.io.Closeables; 023import org.slf4j.Logger; 024import org.slf4j.LoggerFactory; 025 026import java.io.BufferedReader; 027import java.io.IOException; 028import java.io.InputStream; 029import java.io.InputStreamReader; 030import java.util.concurrent.*; 031 032/** 033 * Synchronously execute a native command line. It's much more limited than the Apache Commons Exec library. 034 * For example it does not allow to run asynchronously or to automatically quote command-line arguments. 035 * 036 * @since 2.7 037 */ 038public class CommandExecutor { 039 040 private static final Logger LOG = LoggerFactory.getLogger(CommandExecutor.class); 041 042 private static final CommandExecutor INSTANCE = new CommandExecutor(); 043 044 private CommandExecutor() { 045 } 046 047 public static CommandExecutor create() { 048 // stateless object, so a single singleton can be shared 049 return INSTANCE; 050 } 051 052 /** 053 * @throws CommandException 054 * @since 3.0 055 */ 056 public int execute(Command command, StreamConsumer stdOut, StreamConsumer stdErr, long timeoutMilliseconds) { 057 ExecutorService executorService = null; 058 Process process = null; 059 StreamGobbler outputGobbler = null; 060 StreamGobbler errorGobbler = null; 061 try { 062 ProcessBuilder builder = new ProcessBuilder(command.toStrings()); 063 if (command.getDirectory() != null) { 064 builder.directory(command.getDirectory()); 065 } 066 builder.environment().putAll(command.getEnvironmentVariables()); 067 process = builder.start(); 068 069 outputGobbler = new StreamGobbler(process.getInputStream(), stdOut); 070 errorGobbler = new StreamGobbler(process.getErrorStream(), stdErr); 071 outputGobbler.start(); 072 errorGobbler.start(); 073 074 final Process finalProcess = process; 075 executorService = Executors.newSingleThreadExecutor(); 076 Future<Integer> ft = executorService.submit(new Callable<Integer>() { 077 @Override 078 public Integer call() throws Exception { 079 return finalProcess.waitFor(); 080 } 081 }); 082 int exitCode = ft.get(timeoutMilliseconds, TimeUnit.MILLISECONDS); 083 waitUntilFinish(outputGobbler); 084 waitUntilFinish(errorGobbler); 085 verifyGobbler(command, outputGobbler, "stdOut"); 086 verifyGobbler(command, errorGobbler, "stdErr"); 087 return exitCode; 088 089 } catch (TimeoutException te) { 090 process.destroy(); 091 throw new CommandException(command, "Timeout exceeded: " + timeoutMilliseconds + " ms", te); 092 093 } catch (CommandException e) { 094 throw e; 095 096 } catch (Exception e) { 097 throw new CommandException(command, e); 098 099 } finally { 100 waitUntilFinish(outputGobbler); 101 waitUntilFinish(errorGobbler); 102 closeStreams(process); 103 104 if (executorService != null) { 105 executorService.shutdown(); 106 } 107 } 108 } 109 110 private void verifyGobbler(Command command, StreamGobbler gobbler, String type) { 111 if (gobbler.getException() != null) { 112 throw new CommandException(command, "Error inside " + type + " stream", gobbler.getException()); 113 } 114 } 115 116 /** 117 * Execute command and display error and output streams in log. 118 * Method {@link #execute(Command, StreamConsumer, StreamConsumer, long)} is preferable, 119 * when fine-grained control of output of command required. 120 * 121 * @throws CommandException 122 */ 123 public int execute(Command command, long timeoutMilliseconds) { 124 LOG.info("Executing command: " + command); 125 return execute(command, new DefaultConsumer(), new DefaultConsumer(), timeoutMilliseconds); 126 } 127 128 private void closeStreams(Process process) { 129 if (process != null) { 130 Closeables.closeQuietly(process.getInputStream()); 131 Closeables.closeQuietly(process.getInputStream()); 132 Closeables.closeQuietly(process.getOutputStream()); 133 Closeables.closeQuietly(process.getErrorStream()); 134 } 135 } 136 137 private void waitUntilFinish(StreamGobbler thread) { 138 if (thread != null) { 139 try { 140 thread.join(); 141 } catch (InterruptedException e) { 142 LOG.error("InterruptedException while waiting finish of " + thread.toString(), e); 143 } 144 } 145 } 146 147 private static class StreamGobbler extends Thread { 148 private final InputStream is; 149 private final StreamConsumer consumer; 150 private volatile Exception exception; 151 152 StreamGobbler(InputStream is, StreamConsumer consumer) { 153 super("ProcessStreamGobbler"); 154 this.is = is; 155 this.consumer = consumer; 156 } 157 158 @Override 159 public void run() { 160 InputStreamReader isr = new InputStreamReader(is); 161 BufferedReader br = new BufferedReader(isr); 162 try { 163 String line; 164 while ((line = br.readLine()) != null) { 165 consumeLine(line); 166 } 167 } catch (IOException ioe) { 168 exception = ioe; 169 170 } finally { 171 Closeables.closeQuietly(br); 172 Closeables.closeQuietly(isr); 173 } 174 } 175 176 private void consumeLine(String line) { 177 if (exception == null) { 178 try { 179 consumer.consumeLine(line); 180 } catch (Exception e) { 181 exception = e; 182 } 183 } 184 } 185 186 public Exception getException() { 187 return exception; 188 } 189 } 190 191 private static class DefaultConsumer implements StreamConsumer { 192 public void consumeLine(String line) { 193 LOG.info(line); 194 } 195 } 196}