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.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 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}