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.Callable; 031import java.util.concurrent.ExecutorService; 032import java.util.concurrent.Executors; 033import java.util.concurrent.Future; 034import java.util.concurrent.TimeUnit; 035import 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 */ 043public 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}