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     */
020    package org.sonar.api.utils.command;
021    
022    import com.google.common.io.Closeables;
023    import org.slf4j.Logger;
024    import org.slf4j.LoggerFactory;
025    
026    import java.io.BufferedReader;
027    import java.io.IOException;
028    import java.io.InputStream;
029    import java.io.InputStreamReader;
030    import 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     */
038    public 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    }