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((Callable<Integer>) finalProcess::waitFor); 084 int exitCode; 085 if (timeoutMilliseconds < 0) { 086 exitCode = ft.get(); 087 } else { 088 exitCode = ft.get(timeoutMilliseconds, TimeUnit.MILLISECONDS); 089 } 090 waitUntilFinish(outputGobbler); 091 waitUntilFinish(errorGobbler); 092 verifyGobbler(command, outputGobbler, "stdOut"); 093 verifyGobbler(command, errorGobbler, "stdErr"); 094 return exitCode; 095 096 } catch (java.util.concurrent.TimeoutException te) { 097 throw new TimeoutException(command, "Timeout exceeded: " + timeoutMilliseconds + " ms", te); 098 099 } catch (CommandException e) { 100 throw e; 101 102 } catch (Exception e) { 103 throw new CommandException(command, e); 104 105 } finally { 106 if (process != null) { 107 process.destroy(); 108 } 109 waitUntilFinish(outputGobbler); 110 waitUntilFinish(errorGobbler); 111 closeStreams(process); 112 113 if (executorService != null) { 114 executorService.shutdown(); 115 } 116 } 117 } 118 119 private static void verifyGobbler(Command command, StreamGobbler gobbler, String type) { 120 if (gobbler.getException() != null) { 121 throw new CommandException(command, "Error inside " + type + " stream", gobbler.getException()); 122 } 123 } 124 125 /** 126 * Execute command and display error and output streams in log. 127 * Method {@link #execute(Command, StreamConsumer, StreamConsumer, long)} is preferable, 128 * when fine-grained control of output of command required. 129 * @param timeoutMilliseconds any negative value means no timeout. 130 * 131 * @throws CommandException 132 */ 133 public int execute(Command command, long timeoutMilliseconds) { 134 LOG.info("Executing command: " + command); 135 return execute(command, new DefaultConsumer(), new DefaultConsumer(), timeoutMilliseconds); 136 } 137 138 private static void closeStreams(@Nullable Process process) { 139 if (process != null) { 140 IOUtils.closeQuietly(process.getInputStream()); 141 IOUtils.closeQuietly(process.getOutputStream()); 142 IOUtils.closeQuietly(process.getErrorStream()); 143 } 144 } 145 146 private static void waitUntilFinish(@Nullable StreamGobbler thread) { 147 if (thread != null) { 148 try { 149 thread.join(); 150 } catch (InterruptedException e) { 151 LOG.error("InterruptedException while waiting finish of " + thread.toString(), e); 152 } 153 } 154 } 155 156 private static class StreamGobbler extends Thread { 157 private final InputStream is; 158 private final StreamConsumer consumer; 159 private volatile Exception exception; 160 161 StreamGobbler(InputStream is, StreamConsumer consumer) { 162 super("ProcessStreamGobbler"); 163 this.is = is; 164 this.consumer = consumer; 165 } 166 167 @Override 168 public void run() { 169 try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { 170 String line; 171 while ((line = br.readLine()) != null) { 172 consumeLine(line); 173 } 174 } catch (IOException ioe) { 175 exception = ioe; 176 } 177 } 178 179 private void consumeLine(String line) { 180 if (exception == null) { 181 try { 182 consumer.consumeLine(line); 183 } catch (Exception e) { 184 exception = e; 185 } 186 } 187 } 188 189 public Exception getException() { 190 return exception; 191 } 192 } 193 194 private static class DefaultConsumer implements StreamConsumer { 195 @Override 196 public void consumeLine(String line) { 197 LOG.info(line); 198 } 199 } 200}