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