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