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