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 */
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.Callable;
031import java.util.concurrent.ExecutorService;
032import java.util.concurrent.Executors;
033import java.util.concurrent.Future;
034import java.util.concurrent.TimeUnit;
035import java.util.concurrent.TimeoutException;
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 = LoggerFactory.getLogger(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 CommandException
059   * @since 3.0
060   */
061  public int execute(Command command, StreamConsumer stdOut, StreamConsumer stdErr, long timeoutMilliseconds) {
062    ExecutorService executorService = null;
063    Process process = null;
064    StreamGobbler outputGobbler = null;
065    StreamGobbler errorGobbler = null;
066    try {
067      ProcessBuilder builder = new ProcessBuilder(command.toStrings());
068      if (command.getDirectory() != null) {
069        builder.directory(command.getDirectory());
070      }
071      builder.environment().putAll(command.getEnvironmentVariables());
072      process = builder.start();
073
074      outputGobbler = new StreamGobbler(process.getInputStream(), stdOut);
075      errorGobbler = new StreamGobbler(process.getErrorStream(), stdErr);
076      outputGobbler.start();
077      errorGobbler.start();
078
079      final Process finalProcess = process;
080      executorService = Executors.newSingleThreadExecutor();
081      Future<Integer> ft = executorService.submit(new Callable<Integer>() {
082        public Integer call() throws Exception {
083          return finalProcess.waitFor();
084        }
085      });
086      int exitCode = ft.get(timeoutMilliseconds, TimeUnit.MILLISECONDS);
087      waitUntilFinish(outputGobbler);
088      waitUntilFinish(errorGobbler);
089      verifyGobbler(command, outputGobbler, "stdOut");
090      verifyGobbler(command, errorGobbler, "stdErr");
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  private void verifyGobbler(Command command, StreamGobbler gobbler, String type) {
115    if (gobbler.getException() != null) {
116      throw new CommandException(command, "Error inside " + type + " stream", gobbler.getException());
117    }
118  }
119
120  /**
121   * Execute command and display error and output streams in log.
122   * Method {@link #execute(Command, StreamConsumer, StreamConsumer, long)} is preferable,
123   * when fine-grained control of output of command required.
124   *
125   * @throws CommandException
126   */
127  public int execute(Command command, long timeoutMilliseconds) {
128    LOG.info("Executing command: " + command);
129    return execute(command, new DefaultConsumer(), new DefaultConsumer(), timeoutMilliseconds);
130  }
131
132  private void closeStreams(Process process) {
133    if (process != null) {
134      Closeables.closeQuietly(process.getInputStream());
135      Closeables.closeQuietly(process.getInputStream());
136      Closeables.closeQuietly(process.getOutputStream());
137      Closeables.closeQuietly(process.getErrorStream());
138    }
139  }
140
141  private void waitUntilFinish(StreamGobbler thread) {
142    if (thread != null) {
143      try {
144        thread.join();
145      } catch (InterruptedException e) {
146        LOG.error("InterruptedException while waiting finish of " + thread.toString(), e);
147      }
148    }
149  }
150
151  private static class StreamGobbler extends Thread {
152    private final InputStream is;
153    private final StreamConsumer consumer;
154    private volatile Exception exception;
155
156    StreamGobbler(InputStream is, StreamConsumer consumer) {
157      super("ProcessStreamGobbler");
158      this.is = is;
159      this.consumer = consumer;
160    }
161
162    @Override
163    public void run() {
164      InputStreamReader isr = new InputStreamReader(is);
165      BufferedReader br = new BufferedReader(isr);
166      try {
167        String line;
168        while ((line = br.readLine()) != null) {
169          consumeLine(line);
170        }
171      } catch (IOException ioe) {
172        exception = ioe;
173
174      } finally {
175        Closeables.closeQuietly(br);
176        Closeables.closeQuietly(isr);
177      }
178    }
179
180    private void consumeLine(String line) {
181      if (exception == null) {
182        try {
183          consumer.consumeLine(line);
184        } catch (Exception e) {
185          exception = e;
186        }
187      }
188    }
189
190    public Exception getException() {
191      return exception;
192    }
193  }
194
195  private static class DefaultConsumer implements StreamConsumer {
196    public void consumeLine(String line) {
197      LOG.info(line);
198    }
199  }
200}