001 /* 002 * SonarQube, open source software quality management tool. 003 * Copyright (C) 2008-2014 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 */ 020 package org.sonar.api.utils.command; 021 022 import com.google.common.io.Closeables; 023 import org.slf4j.Logger; 024 import org.slf4j.LoggerFactory; 025 026 import java.io.BufferedReader; 027 import java.io.IOException; 028 import java.io.InputStream; 029 import java.io.InputStreamReader; 030 import java.util.concurrent.Callable; 031 import java.util.concurrent.ExecutorService; 032 import java.util.concurrent.Executors; 033 import java.util.concurrent.Future; 034 import java.util.concurrent.TimeUnit; 035 036 /** 037 * Synchronously execute a native command line. It's much more limited than the Apache Commons Exec library. 038 * For example it does not allow to run asynchronously or to automatically quote command-line arguments. 039 * 040 * @since 2.7 041 */ 042 public class CommandExecutor { 043 044 private static final Logger LOG = LoggerFactory.getLogger(CommandExecutor.class); 045 046 private static final CommandExecutor INSTANCE = new CommandExecutor(); 047 048 private CommandExecutor() { 049 } 050 051 public static CommandExecutor create() { 052 // stateless object, so a single singleton can be shared 053 return INSTANCE; 054 } 055 056 /** 057 * @throws org.sonar.api.utils.command.TimeoutException on timeout, since 4.4 058 * @throws CommandException on any other error 059 * @param timeoutMilliseconds any negative value means no timeout. 060 * @since 3.0 061 */ 062 public int execute(Command command, StreamConsumer stdOut, StreamConsumer stdErr, long timeoutMilliseconds) { 063 ExecutorService executorService = null; 064 Process process = null; 065 StreamGobbler outputGobbler = null; 066 StreamGobbler errorGobbler = null; 067 try { 068 ProcessBuilder builder = new ProcessBuilder(command.toStrings(false)); 069 if (command.getDirectory() != null) { 070 builder.directory(command.getDirectory()); 071 } 072 builder.environment().putAll(command.getEnvironmentVariables()); 073 process = builder.start(); 074 075 outputGobbler = new StreamGobbler(process.getInputStream(), stdOut); 076 errorGobbler = new StreamGobbler(process.getErrorStream(), stdErr); 077 outputGobbler.start(); 078 errorGobbler.start(); 079 080 final Process finalProcess = process; 081 executorService = Executors.newSingleThreadExecutor(); 082 Future<Integer> ft = executorService.submit(new Callable<Integer>() { 083 @Override 084 public Integer call() throws Exception { 085 return finalProcess.waitFor(); 086 } 087 }); 088 int exitCode; 089 if (timeoutMilliseconds < 0) { 090 exitCode = ft.get(); 091 } else { 092 exitCode = ft.get(timeoutMilliseconds, TimeUnit.MILLISECONDS); 093 } 094 waitUntilFinish(outputGobbler); 095 waitUntilFinish(errorGobbler); 096 verifyGobbler(command, outputGobbler, "stdOut"); 097 verifyGobbler(command, errorGobbler, "stdErr"); 098 return exitCode; 099 100 } catch (java.util.concurrent.TimeoutException te) { 101 process.destroy(); 102 throw new TimeoutException(command, "Timeout exceeded: " + timeoutMilliseconds + " ms", te); 103 104 } catch (CommandException e) { 105 throw e; 106 107 } catch (Exception e) { 108 throw new CommandException(command, e); 109 110 } finally { 111 waitUntilFinish(outputGobbler); 112 waitUntilFinish(errorGobbler); 113 closeStreams(process); 114 115 if (executorService != null) { 116 executorService.shutdown(); 117 } 118 } 119 } 120 121 private void verifyGobbler(Command command, StreamGobbler gobbler, String type) { 122 if (gobbler.getException() != null) { 123 throw new CommandException(command, "Error inside " + type + " stream", gobbler.getException()); 124 } 125 } 126 127 /** 128 * Execute command and display error and output streams in log. 129 * Method {@link #execute(Command, StreamConsumer, StreamConsumer, long)} is preferable, 130 * when fine-grained control of output of command required. 131 * @param timeoutMilliseconds any negative value means no timeout. 132 * 133 * @throws CommandException 134 */ 135 public int execute(Command command, long timeoutMilliseconds) { 136 LOG.info("Executing command: " + command); 137 return execute(command, new DefaultConsumer(), new DefaultConsumer(), timeoutMilliseconds); 138 } 139 140 private void closeStreams(Process process) { 141 if (process != null) { 142 Closeables.closeQuietly(process.getInputStream()); 143 Closeables.closeQuietly(process.getInputStream()); 144 Closeables.closeQuietly(process.getOutputStream()); 145 Closeables.closeQuietly(process.getErrorStream()); 146 } 147 } 148 149 private void waitUntilFinish(StreamGobbler thread) { 150 if (thread != null) { 151 try { 152 thread.join(); 153 } catch (InterruptedException e) { 154 LOG.error("InterruptedException while waiting finish of " + thread.toString(), e); 155 } 156 } 157 } 158 159 private static class StreamGobbler extends Thread { 160 private final InputStream is; 161 private final StreamConsumer consumer; 162 private volatile Exception exception; 163 164 StreamGobbler(InputStream is, StreamConsumer consumer) { 165 super("ProcessStreamGobbler"); 166 this.is = is; 167 this.consumer = consumer; 168 } 169 170 @Override 171 public void run() { 172 InputStreamReader isr = new InputStreamReader(is); 173 BufferedReader br = new BufferedReader(isr); 174 try { 175 String line; 176 while ((line = br.readLine()) != null) { 177 consumeLine(line); 178 } 179 } catch (IOException ioe) { 180 exception = ioe; 181 182 } finally { 183 Closeables.closeQuietly(br); 184 Closeables.closeQuietly(isr); 185 } 186 } 187 188 private void consumeLine(String line) { 189 if (exception == null) { 190 try { 191 consumer.consumeLine(line); 192 } catch (Exception e) { 193 exception = e; 194 } 195 } 196 } 197 198 public Exception getException() { 199 return exception; 200 } 201 } 202 203 private static class DefaultConsumer implements StreamConsumer { 204 @Override 205 public void consumeLine(String line) { 206 LOG.info(line); 207 } 208 } 209 }