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     */
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 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    }