001 /*
002 * Sonar, open source software quality management tool.
003 * Copyright (C) 2008-2012 SonarSource
004 * mailto:contact AT sonarsource DOT com
005 *
006 * Sonar 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 * Sonar 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
017 * License along with Sonar; if not, write to the Free Software
018 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
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 final 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 CommandException
054 * @since 3.0
055 */
056 public int execute(Command command, StreamConsumer stdOut, StreamConsumer stdErr, long timeoutMilliseconds) {
057 ExecutorService executorService = null;
058 Process process = null;
059 StreamGobbler outputGobbler = null;
060 StreamGobbler errorGobbler = null;
061 try {
062 ProcessBuilder builder = new ProcessBuilder(command.toStrings());
063 if (command.getDirectory() != null) {
064 builder.directory(command.getDirectory());
065 }
066 process = builder.start();
067
068 outputGobbler = new StreamGobbler(process.getInputStream(), stdOut);
069 errorGobbler = new StreamGobbler(process.getErrorStream(), stdErr);
070 outputGobbler.start();
071 errorGobbler.start();
072
073 final Process finalProcess = process;
074 Callable<Integer> call = new Callable<Integer>() {
075 public Integer call() throws Exception {
076 return finalProcess.waitFor();
077 }
078 };
079
080 executorService = Executors.newSingleThreadExecutor();
081 Future<Integer> ft = executorService.submit(call);
082 int exitCode = ft.get(timeoutMilliseconds, TimeUnit.MILLISECONDS);
083 waitUntilFinish(outputGobbler);
084 waitUntilFinish(errorGobbler);
085 if (outputGobbler.getException() != null) {
086 throw new CommandException(command, "Error inside stdOut parser", outputGobbler.getException());
087 }
088 if (errorGobbler.getException() != null) {
089 throw new CommandException(command, "Error inside stdErr parser", errorGobbler.getException());
090 }
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 /**
115 * Execute command and display error and output streams in log.
116 * Method {@link #execute(Command, StreamConsumer, StreamConsumer, long)} is preferable,
117 * when fine-grained control of output of command required.
118 *
119 * @throws CommandException
120 */
121 public int execute(Command command, long timeoutMilliseconds) {
122 LOG.info("Executing command: " + command);
123 return execute(command, new DefaultConsumer(), new DefaultConsumer(), timeoutMilliseconds);
124 }
125
126 private void closeStreams(Process process) {
127 if (process != null) {
128 Closeables.closeQuietly(process.getInputStream());
129 Closeables.closeQuietly(process.getInputStream());
130 Closeables.closeQuietly(process.getOutputStream());
131 Closeables.closeQuietly(process.getErrorStream());
132 }
133 }
134
135 private void waitUntilFinish(StreamGobbler thread) {
136 if (thread != null) {
137 try {
138 thread.join();
139 } catch (InterruptedException e) {
140 LOG.error("InterruptedException while waiting finish of " + thread.toString(), e);
141 }
142 }
143 }
144
145 private static class StreamGobbler extends Thread {
146 private final InputStream is;
147 private final StreamConsumer consumer;
148 private volatile Exception exception;
149
150 StreamGobbler(InputStream is, StreamConsumer consumer) {
151 super("ProcessStreamGobbler");
152 this.is = is;
153 this.consumer = consumer;
154 }
155
156 @Override
157 public void run() {
158 InputStreamReader isr = new InputStreamReader(is);
159 BufferedReader br = new BufferedReader(isr);
160 try {
161 String line;
162 while ((line = br.readLine()) != null) {
163 consumeLine(line);
164 }
165 } catch (IOException ioe) {
166 exception = ioe;
167
168 } finally {
169 Closeables.closeQuietly(br);
170 Closeables.closeQuietly(isr);
171 }
172 }
173
174 private void consumeLine(String line) {
175 if (exception == null) {
176 try {
177 consumer.consumeLine(line);
178 } catch (Exception e) {
179 exception = e;
180 }
181 }
182 }
183
184 public Exception getException() {
185 return exception;
186 }
187 }
188
189 private static class DefaultConsumer implements StreamConsumer {
190 public void consumeLine(String line) {
191 LOG.info(line);
192 }
193 }
194 }