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