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.File; 020import java.io.IOException; 021import java.util.Map; 022 023import org.apache.commons.exec.launcher.CommandLauncher; 024import org.apache.commons.exec.launcher.CommandLauncherFactory; 025 026/** 027 * The default class to start a subprocess. The implementation 028 * allows to 029 * <ul> 030 * <li>set a current working directory for the subprocess</li> 031 * <li>provide a set of environment variables passed to the subprocess</li> 032 * <li>capture the subprocess output of stdout and stderr using an ExecuteStreamHandler</li> 033 * <li>kill long-running processes using an ExecuteWatchdog</li> 034 * <li>define a set of expected exit values</li> 035 * <li>terminate any started processes when the main process is terminating using a ProcessDestroyer</li> 036 * </ul> 037 * 038 * The following example shows the basic usage: 039 * 040 * <pre> 041 * Executor exec = new DefaultExecutor(); 042 * CommandLine cl = new CommandLine("ls -l"); 043 * int exitvalue = exec.execute(cl); 044 * </pre> 045 * 046 * @version $Id: DefaultExecutor.java 1636056 2014-11-01 21:12:52Z ggregory $ 047 */ 048public class DefaultExecutor implements Executor { 049 050 /** taking care of output and error stream */ 051 private ExecuteStreamHandler streamHandler; 052 053 /** the working directory of the process */ 054 private File workingDirectory; 055 056 /** monitoring of long running processes */ 057 private ExecuteWatchdog watchdog; 058 059 /** the exit values considered to be successful */ 060 private int[] exitValues; 061 062 /** launches the command in a new process */ 063 private final CommandLauncher launcher; 064 065 /** optional cleanup of started processes */ 066 private ProcessDestroyer processDestroyer; 067 068 /** worker thread for asynchronous execution */ 069 private Thread executorThread; 070 071 /** the first exception being caught to be thrown to the caller */ 072 private IOException exceptionCaught; 073 074 /** 075 * Default constructor creating a default {@code PumpStreamHandler} 076 * and sets the working directory of the subprocess to the current 077 * working directory. 078 * 079 * The {@code PumpStreamHandler} pumps the output of the subprocess 080 * into our {@code System.out} and {@code System.err} to avoid 081 * into our {@code System.out} and {@code System.err} to avoid 082 * a blocked or deadlocked subprocess (see{@link java.lang.Process Process}). 083 */ 084 public DefaultExecutor() { 085 this.streamHandler = new PumpStreamHandler(); 086 this.launcher = CommandLauncherFactory.createVMLauncher(); 087 this.exitValues = new int[0]; 088 this.workingDirectory = new File("."); 089 this.exceptionCaught = null; 090 } 091 092 /** 093 * @see org.apache.commons.exec.Executor#getStreamHandler() 094 */ 095 public ExecuteStreamHandler getStreamHandler() { 096 return streamHandler; 097 } 098 099 /** 100 * @see org.apache.commons.exec.Executor#setStreamHandler(org.apache.commons.exec.ExecuteStreamHandler) 101 */ 102 public void setStreamHandler(final ExecuteStreamHandler streamHandler) { 103 this.streamHandler = streamHandler; 104 } 105 106 /** 107 * @see org.apache.commons.exec.Executor#getWatchdog() 108 */ 109 public ExecuteWatchdog getWatchdog() { 110 return watchdog; 111 } 112 113 /** 114 * @see org.apache.commons.exec.Executor#setWatchdog(org.apache.commons.exec.ExecuteWatchdog) 115 */ 116 public void setWatchdog(final ExecuteWatchdog watchDog) { 117 this.watchdog = watchDog; 118 } 119 120 /** 121 * @see org.apache.commons.exec.Executor#getProcessDestroyer() 122 */ 123 public ProcessDestroyer getProcessDestroyer() { 124 return this.processDestroyer; 125 } 126 127 /** 128 * @see org.apache.commons.exec.Executor#setProcessDestroyer(ProcessDestroyer) 129 */ 130 public void setProcessDestroyer(final ProcessDestroyer processDestroyer) { 131 this.processDestroyer = processDestroyer; 132 } 133 134 /** 135 * @see org.apache.commons.exec.Executor#getWorkingDirectory() 136 */ 137 public File getWorkingDirectory() { 138 return workingDirectory; 139 } 140 141 /** 142 * @see org.apache.commons.exec.Executor#setWorkingDirectory(java.io.File) 143 */ 144 public void setWorkingDirectory(final File dir) { 145 this.workingDirectory = dir; 146 } 147 148 /** 149 * @see org.apache.commons.exec.Executor#execute(CommandLine) 150 */ 151 public int execute(final CommandLine command) throws ExecuteException, 152 IOException { 153 return execute(command, (Map<String, String>) null); 154 } 155 156 /** 157 * @see org.apache.commons.exec.Executor#execute(CommandLine, java.util.Map) 158 */ 159 public int execute(final CommandLine command, final Map<String, String> environment) 160 throws ExecuteException, IOException { 161 162 if (workingDirectory != null && !workingDirectory.exists()) { 163 throw new IOException(workingDirectory + " doesn't exist."); 164 } 165 166 return executeInternal(command, environment, workingDirectory, streamHandler); 167 168 } 169 170 /** 171 * @see org.apache.commons.exec.Executor#execute(CommandLine, 172 * org.apache.commons.exec.ExecuteResultHandler) 173 */ 174 public void execute(final CommandLine command, final ExecuteResultHandler handler) 175 throws ExecuteException, IOException { 176 execute(command, null, handler); 177 } 178 179 /** 180 * @see org.apache.commons.exec.Executor#execute(CommandLine, 181 * java.util.Map, org.apache.commons.exec.ExecuteResultHandler) 182 */ 183 public void execute(final CommandLine command, final Map<String, String> environment, 184 final ExecuteResultHandler handler) throws ExecuteException, IOException { 185 186 if (workingDirectory != null && !workingDirectory.exists()) { 187 throw new IOException(workingDirectory + " doesn't exist."); 188 } 189 190 if (watchdog != null) { 191 watchdog.setProcessNotStarted(); 192 } 193 194 final Runnable runnable = new Runnable() 195 { 196 public void run() 197 { 198 int exitValue = Executor.INVALID_EXITVALUE; 199 try { 200 exitValue = executeInternal(command, environment, workingDirectory, streamHandler); 201 handler.onProcessComplete(exitValue); 202 } catch (final ExecuteException e) { 203 handler.onProcessFailed(e); 204 } catch (final Exception e) { 205 handler.onProcessFailed(new ExecuteException("Execution failed", exitValue, e)); 206 } 207 } 208 }; 209 210 this.executorThread = createThread(runnable, "Exec Default Executor"); 211 getExecutorThread().start(); 212 } 213 214 /** @see org.apache.commons.exec.Executor#setExitValue(int) */ 215 public void setExitValue(final int value) { 216 this.setExitValues(new int[] {value}); 217 } 218 219 220 /** @see org.apache.commons.exec.Executor#setExitValues(int[]) */ 221 public void setExitValues(final int[] values) { 222 this.exitValues = values == null ? null : (int[]) values.clone(); 223 } 224 225 /** @see org.apache.commons.exec.Executor#isFailure(int) */ 226 public boolean isFailure(final int exitValue) { 227 228 if (this.exitValues == null) { 229 return false; 230 } 231 else if (this.exitValues.length == 0) { 232 return this.launcher.isFailure(exitValue); 233 } 234 else { 235 for (final int exitValue2 : this.exitValues) { 236 if (exitValue2 == exitValue) { 237 return false; 238 } 239 } 240 } 241 return true; 242 } 243 244 /** 245 * Factory method to create a thread waiting for the result of an 246 * asynchronous execution. 247 * 248 * @param runnable the runnable passed to the thread 249 * @param name the name of the thread 250 * @return the thread 251 */ 252 protected Thread createThread(final Runnable runnable, final String name) { 253 return new Thread(runnable, name); 254 } 255 256 /** 257 * Creates a process that runs a command. 258 * 259 * @param command 260 * the command to run 261 * @param env 262 * the environment for the command 263 * @param dir 264 * the working directory for the command 265 * @return the process started 266 * @throws IOException 267 * forwarded from the particular launcher used 268 */ 269 protected Process launch(final CommandLine command, final Map<String, String> env, 270 final File dir) throws IOException { 271 272 if (this.launcher == null) { 273 throw new IllegalStateException("CommandLauncher can not be null"); 274 } 275 276 if (dir != null && !dir.exists()) { 277 throw new IOException(dir + " doesn't exist."); 278 } 279 return this.launcher.exec(command, env, dir); 280 } 281 282 /** 283 * Get the worker thread being used for asynchronous execution. 284 * 285 * @return the worker thread 286 */ 287 protected Thread getExecutorThread() { 288 return executorThread; 289 } 290 291 /** 292 * Close the streams belonging to the given Process. 293 * 294 * @param process the <CODE>Process</CODE>. 295 */ 296 private void closeProcessStreams(final Process process) { 297 298 try { 299 process.getInputStream().close(); 300 } 301 catch (final IOException e) { 302 setExceptionCaught(e); 303 } 304 305 try { 306 process.getOutputStream().close(); 307 } 308 catch (final IOException e) { 309 setExceptionCaught(e); 310 } 311 312 try { 313 process.getErrorStream().close(); 314 } 315 catch (final IOException e) { 316 setExceptionCaught(e); 317 } 318 } 319 320 /** 321 * Execute an internal process. If the executing thread is interrupted while waiting for the 322 * child process to return the child process will be killed. 323 * 324 * @param command the command to execute 325 * @param environment the execution environment 326 * @param dir the working directory 327 * @param streams process the streams (in, out, err) of the process 328 * @return the exit code of the process 329 * @throws IOException executing the process failed 330 */ 331 private int executeInternal(final CommandLine command, final Map<String, String> environment, 332 final File dir, final ExecuteStreamHandler streams) throws IOException { 333 334 setExceptionCaught(null); 335 336 final Process process = this.launch(command, environment, dir); 337 338 try { 339 streams.setProcessInputStream(process.getOutputStream()); 340 streams.setProcessOutputStream(process.getInputStream()); 341 streams.setProcessErrorStream(process.getErrorStream()); 342 } catch (final IOException e) { 343 process.destroy(); 344 throw e; 345 } 346 347 streams.start(); 348 349 try { 350 351 // add the process to the list of those to destroy if the VM exits 352 if (this.getProcessDestroyer() != null) { 353 this.getProcessDestroyer().add(process); 354 } 355 356 // associate the watchdog with the newly created process 357 if (watchdog != null) { 358 watchdog.start(process); 359 } 360 361 int exitValue = Executor.INVALID_EXITVALUE; 362 363 try { 364 exitValue = process.waitFor(); 365 } catch (final InterruptedException e) { 366 process.destroy(); 367 } 368 finally { 369 // see http://bugs.sun.com/view_bug.do?bug_id=6420270 370 // see https://issues.apache.org/jira/browse/EXEC-46 371 // Process.waitFor should clear interrupt status when throwing InterruptedException 372 // but we have to do that manually 373 Thread.interrupted(); 374 } 375 376 if (watchdog != null) { 377 watchdog.stop(); 378 } 379 380 try { 381 streams.stop(); 382 } 383 catch (final IOException e) { 384 setExceptionCaught(e); 385 } 386 387 closeProcessStreams(process); 388 389 if (getExceptionCaught() != null) { 390 throw getExceptionCaught(); 391 } 392 393 if (watchdog != null) { 394 try { 395 watchdog.checkException(); 396 } catch (final IOException e) { 397 throw e; 398 } catch (final Exception e) { 399 throw new IOException(e.getMessage()); 400 } 401 } 402 403 if (this.isFailure(exitValue)) { 404 throw new ExecuteException("Process exited with an error: " + exitValue, exitValue); 405 } 406 407 return exitValue; 408 } finally { 409 // remove the process to the list of those to destroy if the VM exits 410 if (this.getProcessDestroyer() != null) { 411 this.getProcessDestroyer().remove(process); 412 } 413 } 414 } 415 416 /** 417 * Keep track of the first IOException being thrown. 418 * 419 * @param e the IOException 420 */ 421 private void setExceptionCaught(final IOException e) { 422 if (this.exceptionCaught == null) { 423 this.exceptionCaught = e; 424 } 425 } 426 427 /** 428 * Get the first IOException being thrown. 429 * 430 * @return the first IOException being caught 431 */ 432 private IOException getExceptionCaught() { 433 return this.exceptionCaught; 434 } 435 436}