001/* 002 * SonarQube 003 * Copyright (C) 2009-2016 SonarSource SA 004 * mailto:contact AT sonarsource DOT com 005 * 006 * This program 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 * This program 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 */ 020package org.sonar.api.utils.command; 021 022import java.io.BufferedReader; 023import java.io.IOException; 024import java.io.InputStream; 025import java.io.InputStreamReader; 026import java.nio.charset.StandardCharsets; 027import java.util.concurrent.Callable; 028import java.util.concurrent.ExecutorService; 029import java.util.concurrent.Executors; 030import java.util.concurrent.Future; 031import java.util.concurrent.TimeUnit; 032import javax.annotation.Nullable; 033import org.apache.commons.io.IOUtils; 034import org.sonar.api.utils.log.Logger; 035import org.sonar.api.utils.log.Loggers; 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 = Loggers.get(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 org.sonar.api.utils.command.TimeoutException on timeout, since 4.4 059 * @throws CommandException on any other error 060 * @param timeoutMilliseconds any negative value means no timeout. 061 * @since 3.0 062 */ 063 public int execute(Command command, StreamConsumer stdOut, StreamConsumer stdErr, long timeoutMilliseconds) { 064 ExecutorService executorService = null; 065 Process process = null; 066 StreamGobbler outputGobbler = null; 067 StreamGobbler errorGobbler = null; 068 try { 069 ProcessBuilder builder = new ProcessBuilder(command.toStrings(false)); 070 if (command.getDirectory() != null) { 071 builder.directory(command.getDirectory()); 072 } 073 builder.environment().putAll(command.getEnvironmentVariables()); 074 process = builder.start(); 075 076 outputGobbler = new StreamGobbler(process.getInputStream(), stdOut); 077 errorGobbler = new StreamGobbler(process.getErrorStream(), stdErr); 078 outputGobbler.start(); 079 errorGobbler.start(); 080 081 final Process finalProcess = process; 082 executorService = Executors.newSingleThreadExecutor(); 083 Future<Integer> ft = executorService.submit(new Callable<Integer>() { 084 @Override 085 public Integer call() throws Exception { 086 return finalProcess.waitFor(); 087 } 088 }); 089 int exitCode; 090 if (timeoutMilliseconds < 0) { 091 exitCode = ft.get(); 092 } else { 093 exitCode = ft.get(timeoutMilliseconds, TimeUnit.MILLISECONDS); 094 } 095 waitUntilFinish(outputGobbler); 096 waitUntilFinish(errorGobbler); 097 verifyGobbler(command, outputGobbler, "stdOut"); 098 verifyGobbler(command, errorGobbler, "stdErr"); 099 return exitCode; 100 101 } catch (java.util.concurrent.TimeoutException te) { 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 if (process != null) { 112 process.destroy(); 113 } 114 waitUntilFinish(outputGobbler); 115 waitUntilFinish(errorGobbler); 116 closeStreams(process); 117 118 if (executorService != null) { 119 executorService.shutdown(); 120 } 121 } 122 } 123 124 private static void verifyGobbler(Command command, StreamGobbler gobbler, String type) { 125 if (gobbler.getException() != null) { 126 throw new CommandException(command, "Error inside " + type + " stream", gobbler.getException()); 127 } 128 } 129 130 /** 131 * Execute command and display error and output streams in log. 132 * Method {@link #execute(Command, StreamConsumer, StreamConsumer, long)} is preferable, 133 * when fine-grained control of output of command required. 134 * @param timeoutMilliseconds any negative value means no timeout. 135 * 136 * @throws CommandException 137 */ 138 public int execute(Command command, long timeoutMilliseconds) { 139 LOG.info("Executing command: " + command); 140 return execute(command, new DefaultConsumer(), new DefaultConsumer(), timeoutMilliseconds); 141 } 142 143 private static void closeStreams(@Nullable Process process) { 144 if (process != null) { 145 IOUtils.closeQuietly(process.getInputStream()); 146 IOUtils.closeQuietly(process.getOutputStream()); 147 IOUtils.closeQuietly(process.getErrorStream()); 148 } 149 } 150 151 private static void waitUntilFinish(@Nullable StreamGobbler thread) { 152 if (thread != null) { 153 try { 154 thread.join(); 155 } catch (InterruptedException e) { 156 LOG.error("InterruptedException while waiting finish of " + thread.toString(), e); 157 } 158 } 159 } 160 161 private static class StreamGobbler extends Thread { 162 private final InputStream is; 163 private final StreamConsumer consumer; 164 private volatile Exception exception; 165 166 StreamGobbler(InputStream is, StreamConsumer consumer) { 167 super("ProcessStreamGobbler"); 168 this.is = is; 169 this.consumer = consumer; 170 } 171 172 @Override 173 public void run() { 174 try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { 175 String line; 176 while ((line = br.readLine()) != null) { 177 consumeLine(line); 178 } 179 } catch (IOException ioe) { 180 exception = ioe; 181 } 182 } 183 184 private void consumeLine(String line) { 185 if (exception == null) { 186 try { 187 consumer.consumeLine(line); 188 } catch (Exception e) { 189 exception = e; 190 } 191 } 192 } 193 194 public Exception getException() { 195 return exception; 196 } 197 } 198 199 private static class DefaultConsumer implements StreamConsumer { 200 @Override 201 public void consumeLine(String line) { 202 LOG.info(line); 203 } 204 } 205}