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 }