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