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