001/* 002 * SonarQube 003 * Copyright (C) 2009-2017 SonarSource SA 004 * mailto:info 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 executorService = Executors.newSingleThreadExecutor(); 082 Future<Integer> ft = executorService.submit((Callable<Integer>) process::waitFor); 083 int exitCode; 084 if (timeoutMilliseconds < 0) { 085 exitCode = ft.get(); 086 } else { 087 exitCode = ft.get(timeoutMilliseconds, TimeUnit.MILLISECONDS); 088 } 089 waitUntilFinish(outputGobbler); 090 waitUntilFinish(errorGobbler); 091 verifyGobbler(command, outputGobbler, "stdOut"); 092 verifyGobbler(command, errorGobbler, "stdErr"); 093 return exitCode; 094 095 } catch (java.util.concurrent.TimeoutException te) { 096 throw new TimeoutException(command, "Timeout exceeded: " + timeoutMilliseconds + " ms", te); 097 098 } catch (CommandException e) { 099 throw e; 100 101 } catch (Exception e) { 102 throw new CommandException(command, e); 103 104 } finally { 105 if (process != null) { 106 process.destroy(); 107 } 108 waitUntilFinish(outputGobbler); 109 waitUntilFinish(errorGobbler); 110 closeStreams(process); 111 112 if (executorService != null) { 113 executorService.shutdown(); 114 } 115 } 116 } 117 118 private static void verifyGobbler(Command command, StreamGobbler gobbler, String type) { 119 if (gobbler.getException() != null) { 120 throw new CommandException(command, "Error inside " + type + " stream", gobbler.getException()); 121 } 122 } 123 124 /** 125 * Execute command and display error and output streams in log. 126 * Method {@link #execute(Command, StreamConsumer, StreamConsumer, long)} is preferable, 127 * when fine-grained control of output of command required. 128 * @param timeoutMilliseconds any negative value means no timeout. 129 * 130 * @throws CommandException 131 */ 132 public int execute(Command command, long timeoutMilliseconds) { 133 LOG.info("Executing command: " + command); 134 return execute(command, new DefaultConsumer(), new DefaultConsumer(), timeoutMilliseconds); 135 } 136 137 private static void closeStreams(@Nullable Process process) { 138 if (process != null) { 139 IOUtils.closeQuietly(process.getInputStream()); 140 IOUtils.closeQuietly(process.getOutputStream()); 141 IOUtils.closeQuietly(process.getErrorStream()); 142 } 143 } 144 145 private static void waitUntilFinish(@Nullable StreamGobbler thread) { 146 if (thread != null) { 147 try { 148 thread.join(); 149 } catch (InterruptedException e) { 150 // considered as finished, restore the interrupted flag 151 Thread.currentThread().interrupt(); 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}