001/*-------------------------------------------------------------------------+ 002| | 003| Copyright 2005-2011 The ConQAT Project | 004| | 005| Licensed under the Apache License, Version 2.0 (the "License"); | 006| you may not use this file except in compliance with the License. | 007| You may obtain a copy of the License at | 008| | 009| http://www.apache.org/licenses/LICENSE-2.0 | 010| | 011| Unless required by applicable law or agreed to in writing, software | 012| distributed under the License is distributed on an "AS IS" BASIS, | 013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 014| See the License for the specific language governing permissions and | 015| limitations under the License. | 016+-------------------------------------------------------------------------*/ 017package org.conqat.lib.commons.io; 018 019import java.io.IOException; 020import java.io.OutputStreamWriter; 021import java.io.Writer; 022import java.nio.charset.Charset; 023import java.nio.charset.StandardCharsets; 024import java.util.Arrays; 025import java.util.Timer; 026import java.util.function.BiFunction; 027import java.util.regex.Matcher; 028import java.util.regex.Pattern; 029 030import org.conqat.lib.commons.concurrent.InterruptTimerTask; 031import org.conqat.lib.commons.string.StringUtils; 032import org.conqat.lib.commons.system.SystemUtils; 033 034/** 035 * Executes a system process. Takes care of reading stdout and stderr of the 036 * process in separate threads to avoid blocking. 037 */ 038public class ProcessUtils { 039 040 /** 041 * Command (path or name) to mono, to execute .NET programs on non-windows 042 * machines. 043 */ 044 public static final String MONO_COMMAND = "mono"; 045 046 /** Character set used for process I/O. Important on Windows. */ 047 public static Charset CONSOLE_CHARSET = StandardCharsets.UTF_8; 048 049 static { 050 if (SystemUtils.isWindows()) { 051 try { 052 ExecutionResult result = execute(new String[] { "chcp.com" }); 053 Matcher matcher = Pattern.compile("\\d+").matcher(result.getStdout()); 054 if (matcher.find()) { 055 CONSOLE_CHARSET = Charset.forName("Cp" + matcher.group()); 056 } 057 } catch (IOException | IllegalArgumentException e) { 058 // Keep default. 059 } 060 } 061 } 062 063 /** 064 * Creates a {@link ProcessBuilder} for executing a .net assembly. If the 065 * current OS is not Windows mono will be used for executing the assembly. 066 */ 067 public static ProcessBuilder createDotNetProcessBuilder(String... arguments) { 068 ProcessBuilder builder = new ProcessBuilder(); 069 070 if (!SystemUtils.isWindows()) { 071 builder.command().add(MONO_COMMAND); 072 } 073 074 builder.command().addAll(Arrays.asList(arguments)); 075 076 return builder; 077 } 078 079 /** 080 * Executes a .net program in a thread-safe way. 081 */ 082 public static ExecutionResult executeDotNet(String[] completeArguments) throws IOException { 083 return execute(createDotNetProcessBuilder(completeArguments)); 084 } 085 086 /** 087 * Executes a process in a thread-safe way. 088 * 089 * @param completeArguments 090 * Array of command line arguments to start the process 091 * 092 * @return result of the execution 093 */ 094 public static ExecutionResult execute(String[] completeArguments) throws IOException { 095 return execute(completeArguments, null); 096 } 097 098 /** 099 * Executes a process in a thread-safe way. 100 * 101 * @param completeArguments 102 * Array of command line arguments to start the process 103 * @param input 104 * String that gets written to stdin 105 * 106 * @return result of the execution 107 */ 108 public static ExecutionResult execute(String[] completeArguments, String input) throws IOException { 109 ProcessBuilder builder = new ProcessBuilder(completeArguments); 110 return execute(builder, input); 111 } 112 113 /** 114 * Executes a process in a thread-safe way. 115 * 116 * @param builder 117 * builder that gets executed 118 * @return result of the execution 119 */ 120 public static ExecutionResult execute(ProcessBuilder builder) throws IOException { 121 return execute(builder, null); 122 } 123 124 /** 125 * Executes a process in a thread-safe way. 126 * 127 * @param builder 128 * builder that gets executed 129 * @param input 130 * String that gets written to stdin 131 * @return result of the execution 132 */ 133 public static ExecutionResult execute(ProcessBuilder builder, String input) throws IOException { 134 return execute(builder, input, -1); 135 } 136 137 /** 138 * Executes a process in a thread-safe way. 139 * 140 * @param builder 141 * builder that gets executed 142 * @param input 143 * String that gets written to stdin (may be null). 144 * @param timeOut 145 * the number to seconds to wait for the process. If this runs 146 * longer, the process is killed. Passing a value of 0 or less makes 147 * the method wait forever (until the process finishes normally). To 148 * find out whether the process was killed, query 149 * {@link ExecutionResult#isNormalTermination()}. 150 * 151 * @return result of the execution 152 */ 153 public static ExecutionResult execute(ProcessBuilder builder, String input, int timeOut) throws IOException { 154 return execute(builder, input, timeOut, true); 155 } 156 157 /** 158 * Executes a process in a thread-safe way. 159 * 160 * @param builder 161 * builder that gets executed 162 * @param input 163 * String that gets written to stdin (may be null). 164 * @param timeOut 165 * the number to seconds to wait for the process. If this runs 166 * longer, the process is killed. Passing a value of 0 or less makes 167 * the method wait forever (until the process finishes normally). To 168 * find out whether the process was killed, query 169 * {@link ExecutionResult#isNormalTermination()}. 170 * @param collectOutputStreamContent 171 * boolean that indicates if the content from the stderr and stdout 172 * shall be collected. False is useful if the content is not needed 173 * and / or if the content would cause an OutOfMemoryException. 174 * 175 * @return result of the execution 176 */ 177 public static ExecutionResult execute(ProcessBuilder builder, String input, int timeOut, 178 boolean collectOutputStreamContent) throws IOException { 179 // start process 180 Process process = builder.start(); 181 182 // read error for later use 183 StreamReaderThread stderrReader = new StreamReaderThread(process.getErrorStream(), CONSOLE_CHARSET, 184 collectOutputStreamContent); 185 StreamReaderThread stdoutReader = new StreamReaderThread(process.getInputStream(), CONSOLE_CHARSET, 186 collectOutputStreamContent); 187 188 // write input to process 189 if (input != null) { 190 Writer stdIn = new OutputStreamWriter(process.getOutputStream()); 191 stdIn.write(input); 192 stdIn.close(); 193 } 194 195 // wait for process 196 boolean normalTermination = waitForProcess(process, timeOut); 197 int exitValue = -1; 198 if (normalTermination) { 199 exitValue = process.exitValue(); 200 } 201 202 try { 203 // It is important to wait for the threads, so the output is 204 // completely stored. 205 stderrReader.join(); 206 stdoutReader.join(); 207 } catch (InterruptedException e) { 208 // ignore this one 209 } 210 211 return new ExecutionResult(stdoutReader.getContent(), stderrReader.getContent(), exitValue, normalTermination); 212 } 213 214 /** 215 * Waits for the process to end or terminates it if it hits the timeout. The 216 * return value indicated whether the process terminated (true) or was killed by 217 * the timeout (false). 218 * 219 * @param maxRuntimeSeconds 220 * is this is non-positive, this method waits until the process 221 * terminates (without timeout). 222 */ 223 private static boolean waitForProcess(Process process, int maxRuntimeSeconds) { 224 // Stopping a running process after a given time is not well supported 225 // by the Java API. See the following links for the gory details: 226 // * http://kylecartmell.com/?p=9 227 // * http://www.kylecartmell.com/public_files/ProcessTimeoutExample.java 228 Timer timer = new Timer(true); 229 230 if (maxRuntimeSeconds > 0) { 231 timer.schedule(new InterruptTimerTask(Thread.currentThread()), maxRuntimeSeconds * 1000); 232 } 233 try { 234 process.waitFor(); 235 return true; 236 } catch (InterruptedException e) { 237 process.destroy(); 238 return false; 239 } finally { 240 // stop timer if still running (relevant if process terminates "in 241 // time") 242 timer.cancel(); 243 // clear the interrupt flag (see the links above for details) 244 Thread.interrupted(); 245 } 246 } 247 248 /** 249 * Parameter object that encapsulates the result of a process execution. This 250 * object is immutable. 251 */ 252 public static class ExecutionResult { 253 254 /** Output on stdout of the process */ 255 private final String stdout; 256 257 /** Output on stderr of the process */ 258 private final String stderr; 259 260 /** Return code of the process */ 261 private final int returnCode; 262 263 /** Whether termination was normal (not timeout). */ 264 private final boolean normalTermination; 265 266 /** Constructor */ 267 private ExecutionResult(String stdout, String stderr, int returnCode, boolean normalTermination) { 268 this.stdout = stdout; 269 this.stderr = stderr; 270 this.returnCode = returnCode; 271 this.normalTermination = normalTermination; 272 } 273 274 /** Returns stdout. */ 275 public String getStdout() { 276 return stdout; 277 } 278 279 /** Returns stderr. */ 280 public String getStderr() { 281 return stderr; 282 } 283 284 /** Returns returnCode. */ 285 public int getReturnCode() { 286 return returnCode; 287 } 288 289 /** Returns whether this was a normal termination (not a timeout). */ 290 public boolean isNormalTermination() { 291 return normalTermination; 292 } 293 } 294 295 /** 296 * Runs the given process builder with the given input. If creating the process 297 * fails, the process times out or the process exits with a non-zero exit code, 298 * an exception is thrown 299 * 300 * @param builder 301 * The process builder to execute. 302 * @param input 303 * The input to the program. 304 * @param timeout 305 * How many seconds to wait before terminating the process. If this 306 * is less than 1, no timeout is applied. 307 * @param exceptionConstructor 308 * Constructs the exception that is thrown in case something goes 309 * wrong. The first argument is a descriptive message, the second is 310 * either <code>null</code> (in case of a timeout or non-zero exit 311 * code) or the exception that caused the failure. This parameter is 312 * compatible with most exception constructors, e.g. 313 * <code>ConQATException::new</code>. 314 * @return the execution result of the successfully run process. 315 * @throws T 316 * An exception constructed with the given constructor. Thrown if 317 * the process fails to run and return a zero exit code. 318 */ 319 public static <T extends Exception> ExecutionResult executeOrThrow(ProcessBuilder builder, String input, 320 int timeout, BiFunction<String, Throwable, T> exceptionConstructor) throws T { 321 String commandString = StringUtils.concat(builder.command(), StringUtils.SPACE); 322 try { 323 ExecutionResult result = execute(builder, input, timeout); 324 if (!result.isNormalTermination()) { 325 throw exceptionConstructor.apply("Process " + commandString + " timed out.", null); 326 } 327 if (result.getReturnCode() != 0) { 328 throw exceptionConstructor.apply("Process " + commandString + " failed with non-zero exit code " 329 + result.getReturnCode() + ". Standard output: '" + result.getStdout() + "', Error output: '" 330 + result.getStderr() + "'", null); 331 } 332 return result; 333 } catch (IOException e) { 334 throw exceptionConstructor.apply("Failed to execute " + commandString, e); 335 } 336 } 337}