001    /*
002     * SonarQube, open source software quality management tool.
003     * Copyright (C) 2008-2013 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.Callable;
031    import java.util.concurrent.ExecutorService;
032    import java.util.concurrent.Executors;
033    import java.util.concurrent.Future;
034    import java.util.concurrent.TimeUnit;
035    import 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     */
043    public 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    }