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