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