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.*; 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 */ 038public 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}