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