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