001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. 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.apache.commons.exec; 018 019import java.io.Closeable; 020import java.io.File; 021import java.io.IOException; 022import java.util.Map; 023import java.util.concurrent.Executors; 024import java.util.concurrent.ThreadFactory; 025import java.util.function.Supplier; 026 027import org.apache.commons.exec.launcher.CommandLauncher; 028import org.apache.commons.exec.launcher.CommandLauncherFactory; 029 030/** 031 * The default class to start a subprocess. The implementation allows to 032 * <ul> 033 * <li>set a current working directory for the subprocess</li> 034 * <li>provide a set of environment variables passed to the subprocess</li> 035 * <li>capture the subprocess output of stdout and stderr using an ExecuteStreamHandler</li> 036 * <li>kill long-running processes using an ExecuteWatchdog</li> 037 * <li>define a set of expected exit values</li> 038 * <li>terminate any started processes when the main process is terminating using a ProcessDestroyer</li> 039 * </ul> 040 * 041 * The following example shows the basic usage: 042 * 043 * <pre> 044 * Executor exec = DefaultExecutor.builder().get(); 045 * CommandLine cl = new CommandLine("ls -l"); 046 * int exitvalue = exec.execute(cl); 047 * </pre> 048 */ 049public class DefaultExecutor implements Executor { 050 051 /** 052 * Constructs a new builder. 053 * 054 * @param <T> The builder type. 055 * @since 1.4.0 056 */ 057 public static class Builder<T extends Builder<T>> implements Supplier<DefaultExecutor> { 058 059 private ThreadFactory threadFactory; 060 private ExecuteStreamHandler executeStreamHandler; 061 private File workingDirectory; 062 063 @SuppressWarnings("unchecked") 064 T asThis() { 065 return (T) this; 066 } 067 068 /** 069 * Creates a new configured DefaultExecutor. 070 * 071 * @return a new configured DefaultExecutor. 072 */ 073 @Override 074 public DefaultExecutor get() { 075 return new DefaultExecutor(threadFactory, executeStreamHandler, workingDirectory); 076 } 077 078 ExecuteStreamHandler getExecuteStreamHandler() { 079 return executeStreamHandler; 080 } 081 082 ThreadFactory getThreadFactory() { 083 return threadFactory; 084 } 085 086 File getWorkingDirectory() { 087 return workingDirectory; 088 } 089 090 /** 091 * Sets the PumpStreamHandler. 092 * 093 * @param executeStreamHandler the ExecuteStreamHandler, null resets to the default. 094 * @return this. 095 */ 096 public T setExecuteStreamHandler(final ExecuteStreamHandler executeStreamHandler) { 097 this.executeStreamHandler = executeStreamHandler; 098 return asThis(); 099 } 100 101 /** 102 * Sets the ThreadFactory. 103 * 104 * @param threadFactory the ThreadFactory, null resets to the default. 105 * @return this. 106 */ 107 public T setThreadFactory(final ThreadFactory threadFactory) { 108 this.threadFactory = threadFactory; 109 return asThis(); 110 } 111 112 /** 113 * Sets the working directory. 114 * 115 * @param workingDirectory the working directory., null resets to the default. 116 * @return this. 117 */ 118 public T setWorkingDirectory(final File workingDirectory) { 119 this.workingDirectory = workingDirectory; 120 return asThis(); 121 } 122 123 } 124 125 /** 126 * Creates a new builder. 127 * 128 * @return a new builder. 129 * @since 1.4.0 130 */ 131 public static Builder<?> builder() { 132 return new Builder<>(); 133 } 134 135 /** Taking care of output and error stream. */ 136 private ExecuteStreamHandler executeStreamHandler; 137 138 /** The working directory of the process. */ 139 private File workingDirectory; 140 141 /** Monitoring of long running processes. */ 142 private ExecuteWatchdog watchdog; 143 144 /** The exit values considered to be successful. */ 145 private int[] exitValues; 146 147 /** Launches the command in a new process. */ 148 private final CommandLauncher launcher; 149 150 /** Optional cleanup of started processes. */ 151 private ProcessDestroyer processDestroyer; 152 153 /** Worker thread for asynchronous execution. */ 154 private Thread executorThread; 155 156 /** The first exception being caught to be thrown to the caller. */ 157 private IOException exceptionCaught; 158 159 /** 160 * The thread factory. 161 */ 162 private final ThreadFactory threadFactory; 163 164 /** 165 * Constructs a default {@code PumpStreamHandler} and sets the working directory of the subprocess to the current working directory. 166 * 167 * The {@code PumpStreamHandler} pumps the output of the subprocess into our {@code System.out} and {@code System.err} to avoid into our {@code System.out} 168 * and {@code System.err} to avoid a blocked or deadlocked subprocess (see {@link Process Process}). 169 * 170 * @deprecated Use {@link Builder#get()}. 171 */ 172 @Deprecated 173 public DefaultExecutor() { 174 this(Executors.defaultThreadFactory(), new PumpStreamHandler(), new File(".")); 175 } 176 177 DefaultExecutor(final ThreadFactory threadFactory, final ExecuteStreamHandler executeStreamHandler, final File workingDirectory) { 178 this.threadFactory = threadFactory != null ? threadFactory : Executors.defaultThreadFactory(); 179 this.executeStreamHandler = executeStreamHandler != null ? executeStreamHandler : new PumpStreamHandler(); 180 this.workingDirectory = workingDirectory != null ? workingDirectory : new File("."); 181 this.launcher = CommandLauncherFactory.createVMLauncher(); 182 this.exitValues = new int[0]; 183 } 184 185 private void checkWorkingDirectory() throws IOException { 186 checkWorkingDirectory(workingDirectory); 187 } 188 189 private void checkWorkingDirectory(final File directory) throws IOException { 190 if (directory != null && !directory.exists()) { 191 throw new IOException(directory + " doesn't exist."); 192 } 193 } 194 195 /** 196 * Closes the Closeable, remembering any exception. 197 * 198 * @param closeable the {@link Closeable} to close. 199 */ 200 private void closeCatch(final Closeable closeable) { 201 try { 202 closeable.close(); 203 } catch (final IOException e) { 204 setExceptionCaught(e); 205 } 206 } 207 208 /** 209 * Closes the streams belonging to the given Process. 210 * 211 * @param process the {@link Process}. 212 */ 213 @SuppressWarnings("resource") 214 private void closeProcessStreams(final Process process) { 215 closeCatch(process.getInputStream()); 216 closeCatch(process.getOutputStream()); 217 closeCatch(process.getErrorStream()); 218 } 219 220 /** 221 * Creates a thread waiting for the result of an asynchronous execution. 222 * 223 * @param runnable the runnable passed to the thread. 224 * @param name the name of the thread. 225 * @return the thread 226 */ 227 protected Thread createThread(final Runnable runnable, final String name) { 228 return ThreadUtil.newThread(threadFactory, runnable, name, false); 229 } 230 231 /** 232 * @see org.apache.commons.exec.Executor#execute(CommandLine) 233 */ 234 @Override 235 public int execute(final CommandLine command) throws ExecuteException, IOException { 236 return execute(command, (Map<String, String>) null); 237 } 238 239 /** 240 * @see org.apache.commons.exec.Executor#execute(CommandLine, org.apache.commons.exec.ExecuteResultHandler) 241 */ 242 @Override 243 public void execute(final CommandLine command, final ExecuteResultHandler handler) throws ExecuteException, IOException { 244 execute(command, null, handler); 245 } 246 247 /** 248 * @see org.apache.commons.exec.Executor#execute(CommandLine, java.util.Map) 249 */ 250 @Override 251 public int execute(final CommandLine command, final Map<String, String> environment) throws ExecuteException, IOException { 252 checkWorkingDirectory(); 253 return executeInternal(command, environment, workingDirectory, executeStreamHandler); 254 } 255 256 /** 257 * @see org.apache.commons.exec.Executor#execute(CommandLine, java.util.Map, org.apache.commons.exec.ExecuteResultHandler) 258 */ 259 @Override 260 public void execute(final CommandLine command, final Map<String, String> environment, final ExecuteResultHandler handler) 261 throws ExecuteException, IOException { 262 checkWorkingDirectory(); 263 if (watchdog != null) { 264 watchdog.setProcessNotStarted(); 265 } 266 executorThread = createThread(() -> { 267 int exitValue = Executor.INVALID_EXITVALUE; 268 try { 269 exitValue = executeInternal(command, environment, workingDirectory, executeStreamHandler); 270 handler.onProcessComplete(exitValue); 271 } catch (final ExecuteException e) { 272 handler.onProcessFailed(e); 273 } catch (final Exception e) { 274 handler.onProcessFailed(new ExecuteException("Execution failed", exitValue, e)); 275 } 276 }, "CommonsExecDefaultExecutor"); 277 getExecutorThread().start(); 278 } 279 280 /** 281 * Execute an internal process. If the executing thread is interrupted while waiting for the child process to return the child process will be killed. 282 * 283 * @param command the command to execute. 284 * @param environment the execution environment. 285 * @param workingDirectory the working directory. 286 * @param streams process the streams (in, out, err) of the process. 287 * @return the exit code of the process. 288 * @throws IOException executing the process failed. 289 */ 290 private int executeInternal(final CommandLine command, final Map<String, String> environment, final File workingDirectory, 291 final ExecuteStreamHandler streams) throws IOException { 292 final Process process; 293 exceptionCaught = null; 294 try { 295 process = launch(command, environment, workingDirectory); 296 } catch (final IOException e) { 297 if (watchdog != null) { 298 watchdog.failedToStart(e); 299 } 300 throw e; 301 } 302 try { 303 setStreams(streams, process); 304 } catch (final IOException e) { 305 process.destroy(); 306 if (watchdog != null) { 307 watchdog.failedToStart(e); 308 } 309 throw e; 310 } 311 streams.start(); 312 try { 313 // add the process to the list of those to destroy if the VM exits 314 if (getProcessDestroyer() != null) { 315 getProcessDestroyer().add(process); 316 } 317 // associate the watchdog with the newly created process 318 if (watchdog != null) { 319 watchdog.start(process); 320 } 321 int exitValue = Executor.INVALID_EXITVALUE; 322 try { 323 exitValue = process.waitFor(); 324 } catch (final InterruptedException e) { 325 process.destroy(); 326 } finally { 327 // see http://bugs.sun.com/view_bug.do?bug_id=6420270 328 // see https://issues.apache.org/jira/browse/EXEC-46 329 // Process.waitFor should clear interrupt status when throwing InterruptedException 330 // but we have to do that manually 331 Thread.interrupted(); 332 } 333 if (watchdog != null) { 334 watchdog.stop(); 335 } 336 try { 337 streams.stop(); 338 } catch (final IOException e) { 339 setExceptionCaught(e); 340 } 341 closeProcessStreams(process); 342 if (getExceptionCaught() != null) { 343 throw getExceptionCaught(); 344 } 345 if (watchdog != null) { 346 try { 347 watchdog.checkException(); 348 } catch (final IOException e) { 349 throw e; 350 } catch (final Exception e) { 351 throw new IOException(e); 352 } 353 } 354 if (isFailure(exitValue)) { 355 throw new ExecuteException("Process exited with an error: " + exitValue, exitValue); 356 } 357 return exitValue; 358 } finally { 359 // remove the process to the list of those to destroy if the VM exits 360 if (getProcessDestroyer() != null) { 361 getProcessDestroyer().remove(process); 362 } 363 } 364 } 365 366 /** 367 * Gets the first IOException being thrown. 368 * 369 * @return the first IOException being caught. 370 */ 371 private IOException getExceptionCaught() { 372 return exceptionCaught; 373 } 374 375 /** 376 * Gets the worker thread being used for asynchronous execution. 377 * 378 * @return the worker thread. 379 */ 380 protected Thread getExecutorThread() { 381 return executorThread; 382 } 383 384 /** 385 * @see org.apache.commons.exec.Executor#getProcessDestroyer() 386 */ 387 @Override 388 public ProcessDestroyer getProcessDestroyer() { 389 return processDestroyer; 390 } 391 392 /** 393 * @see org.apache.commons.exec.Executor#getStreamHandler() 394 */ 395 @Override 396 public ExecuteStreamHandler getStreamHandler() { 397 return executeStreamHandler; 398 } 399 400 /** 401 * Gets the thread factory. Z 402 * 403 * @return the thread factory. 404 */ 405 ThreadFactory getThreadFactory() { 406 return threadFactory; 407 } 408 409 /** 410 * @see org.apache.commons.exec.Executor#getWatchdog() 411 */ 412 @Override 413 public ExecuteWatchdog getWatchdog() { 414 return watchdog; 415 } 416 417 /** 418 * @see org.apache.commons.exec.Executor#getWorkingDirectory() 419 */ 420 @Override 421 public File getWorkingDirectory() { 422 return workingDirectory; 423 } 424 425 /** @see org.apache.commons.exec.Executor#isFailure(int) */ 426 @Override 427 public boolean isFailure(final int exitValue) { 428 if (exitValues == null) { 429 return false; 430 } 431 if (exitValues.length == 0) { 432 return launcher.isFailure(exitValue); 433 } 434 for (final int exitValue2 : exitValues) { 435 if (exitValue2 == exitValue) { 436 return false; 437 } 438 } 439 return true; 440 } 441 442 /** 443 * Creates a process that runs a command. 444 * 445 * @param command the command to run. 446 * @param env the environment for the command. 447 * @param workingDirectory the working directory for the command. 448 * @return the process started. 449 * @throws IOException forwarded from the particular launcher used. 450 */ 451 protected Process launch(final CommandLine command, final Map<String, String> env, final File workingDirectory) throws IOException { 452 if (launcher == null) { 453 throw new IllegalStateException("CommandLauncher can not be null"); 454 } 455 checkWorkingDirectory(workingDirectory); 456 return launcher.exec(command, env, workingDirectory); 457 } 458 459 /** 460 * Sets the first IOException thrown. 461 * 462 * @param e the IOException. 463 */ 464 private void setExceptionCaught(final IOException e) { 465 if (exceptionCaught == null) { 466 exceptionCaught = e; 467 } 468 } 469 470 /** @see org.apache.commons.exec.Executor#setExitValue(int) */ 471 @Override 472 public void setExitValue(final int value) { 473 setExitValues(new int[] { value }); 474 } 475 476 /** @see org.apache.commons.exec.Executor#setExitValues(int[]) */ 477 @Override 478 public void setExitValues(final int[] values) { 479 exitValues = values == null ? null : (int[]) values.clone(); 480 } 481 482 /** 483 * @see org.apache.commons.exec.Executor#setProcessDestroyer(ProcessDestroyer) 484 */ 485 @Override 486 public void setProcessDestroyer(final ProcessDestroyer processDestroyer) { 487 this.processDestroyer = processDestroyer; 488 } 489 490 /** 491 * @see org.apache.commons.exec.Executor#setStreamHandler(org.apache.commons.exec.ExecuteStreamHandler) 492 */ 493 @Override 494 public void setStreamHandler(final ExecuteStreamHandler streamHandler) { 495 this.executeStreamHandler = streamHandler; 496 } 497 498 @SuppressWarnings("resource") 499 private void setStreams(final ExecuteStreamHandler streams, final Process process) throws IOException { 500 streams.setProcessInputStream(process.getOutputStream()); 501 streams.setProcessOutputStream(process.getInputStream()); 502 streams.setProcessErrorStream(process.getErrorStream()); 503 } 504 505 /** 506 * @see org.apache.commons.exec.Executor#setWatchdog(org.apache.commons.exec.ExecuteWatchdog) 507 */ 508 @Override 509 public void setWatchdog(final ExecuteWatchdog watchdog) { 510 this.watchdog = watchdog; 511 } 512 513 /** 514 * Sets the working directory. 515 * 516 * @see org.apache.commons.exec.Executor#setWorkingDirectory(java.io.File) 517 * @deprecated Use {@link Builder#setWorkingDirectory(File)}. 518 */ 519 @Deprecated 520 @Override 521 public void setWorkingDirectory(final File workingDirectory) { 522 this.workingDirectory = workingDirectory; 523 } 524 525}