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.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    
036    /**
037     * Synchronously execute a native command line. It's much more limited than the Apache Commons Exec library.
038     * For example it does not allow to run asynchronously or to automatically quote command-line arguments.
039     *
040     * @since 2.7
041     */
042    public class CommandExecutor {
043    
044      private static final Logger LOG = LoggerFactory.getLogger(CommandExecutor.class);
045    
046      private static final CommandExecutor INSTANCE = new CommandExecutor();
047    
048      private CommandExecutor() {
049      }
050    
051      public static CommandExecutor create() {
052        // stateless object, so a single singleton can be shared
053        return INSTANCE;
054      }
055    
056      /**
057       * @throws org.sonar.api.utils.command.TimeoutException on timeout, since 4.4
058       * @throws CommandException on any other error
059       * @param timeoutMilliseconds any negative value means no timeout.
060       * @since 3.0
061       */
062      public int execute(Command command, StreamConsumer stdOut, StreamConsumer stdErr, long timeoutMilliseconds) {
063        ExecutorService executorService = null;
064        Process process = null;
065        StreamGobbler outputGobbler = null;
066        StreamGobbler errorGobbler = null;
067        try {
068          ProcessBuilder builder = new ProcessBuilder(command.toStrings(false));
069          if (command.getDirectory() != null) {
070            builder.directory(command.getDirectory());
071          }
072          builder.environment().putAll(command.getEnvironmentVariables());
073          process = builder.start();
074    
075          outputGobbler = new StreamGobbler(process.getInputStream(), stdOut);
076          errorGobbler = new StreamGobbler(process.getErrorStream(), stdErr);
077          outputGobbler.start();
078          errorGobbler.start();
079    
080          final Process finalProcess = process;
081          executorService = Executors.newSingleThreadExecutor();
082          Future<Integer> ft = executorService.submit(new Callable<Integer>() {
083            @Override
084            public Integer call() throws Exception {
085              return finalProcess.waitFor();
086            }
087          });
088          int exitCode;
089          if (timeoutMilliseconds < 0) {
090            exitCode = ft.get();
091          } else {
092            exitCode = ft.get(timeoutMilliseconds, TimeUnit.MILLISECONDS);
093          }
094          waitUntilFinish(outputGobbler);
095          waitUntilFinish(errorGobbler);
096          verifyGobbler(command, outputGobbler, "stdOut");
097          verifyGobbler(command, errorGobbler, "stdErr");
098          return exitCode;
099    
100        } catch (java.util.concurrent.TimeoutException te) {
101          process.destroy();
102          throw new TimeoutException(command, "Timeout exceeded: " + timeoutMilliseconds + " ms", te);
103    
104        } catch (CommandException e) {
105          throw e;
106    
107        } catch (Exception e) {
108          throw new CommandException(command, e);
109    
110        } finally {
111          waitUntilFinish(outputGobbler);
112          waitUntilFinish(errorGobbler);
113          closeStreams(process);
114    
115          if (executorService != null) {
116            executorService.shutdown();
117          }
118        }
119      }
120    
121      private void verifyGobbler(Command command, StreamGobbler gobbler, String type) {
122        if (gobbler.getException() != null) {
123          throw new CommandException(command, "Error inside " + type + " stream", gobbler.getException());
124        }
125      }
126    
127      /**
128       * Execute command and display error and output streams in log.
129       * Method {@link #execute(Command, StreamConsumer, StreamConsumer, long)} is preferable,
130       * when fine-grained control of output of command required.
131       * @param timeoutMilliseconds any negative value means no timeout.
132       *
133       * @throws CommandException
134       */
135      public int execute(Command command, long timeoutMilliseconds) {
136        LOG.info("Executing command: " + command);
137        return execute(command, new DefaultConsumer(), new DefaultConsumer(), timeoutMilliseconds);
138      }
139    
140      private void closeStreams(Process process) {
141        if (process != null) {
142          Closeables.closeQuietly(process.getInputStream());
143          Closeables.closeQuietly(process.getInputStream());
144          Closeables.closeQuietly(process.getOutputStream());
145          Closeables.closeQuietly(process.getErrorStream());
146        }
147      }
148    
149      private void waitUntilFinish(StreamGobbler thread) {
150        if (thread != null) {
151          try {
152            thread.join();
153          } catch (InterruptedException e) {
154            LOG.error("InterruptedException while waiting finish of " + thread.toString(), e);
155          }
156        }
157      }
158    
159      private static class StreamGobbler extends Thread {
160        private final InputStream is;
161        private final StreamConsumer consumer;
162        private volatile Exception exception;
163    
164        StreamGobbler(InputStream is, StreamConsumer consumer) {
165          super("ProcessStreamGobbler");
166          this.is = is;
167          this.consumer = consumer;
168        }
169    
170        @Override
171        public void run() {
172          InputStreamReader isr = new InputStreamReader(is);
173          BufferedReader br = new BufferedReader(isr);
174          try {
175            String line;
176            while ((line = br.readLine()) != null) {
177              consumeLine(line);
178            }
179          } catch (IOException ioe) {
180            exception = ioe;
181    
182          } finally {
183            Closeables.closeQuietly(br);
184            Closeables.closeQuietly(isr);
185          }
186        }
187    
188        private void consumeLine(String line) {
189          if (exception == null) {
190            try {
191              consumer.consumeLine(line);
192            } catch (Exception e) {
193              exception = e;
194            }
195          }
196        }
197    
198        public Exception getException() {
199          return exception;
200        }
201      }
202    
203      private static class DefaultConsumer implements StreamConsumer {
204        @Override
205        public void consumeLine(String line) {
206          LOG.info(line);
207        }
208      }
209    }