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 */
017 package org.apache.commons.exec;
018
019 import java.io.File;
020 import java.io.IOException;
021 import java.util.Map;
022
023 import org.apache.commons.exec.launcher.CommandLauncher;
024 import 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 public 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 /**
070 * Default constructor creating a default <code>PumpStreamHandler</code>
071 * and sets the working directory of the subprocess to the current
072 * working directory.
073 *
074 * The <code>PumpStreamHandler</code> pumps the output of the subprocess
075 * into our <code>System.out</code> and <code>System.err</code> to avoid
076 * into our <code>System.out</code> and <code>System.err</code> to avoid
077 * a blocked or deadlocked subprocess (see{@link java.lang.Process Process}).
078 */
079 public DefaultExecutor() {
080 this.streamHandler = new PumpStreamHandler();
081 this.launcher = CommandLauncherFactory.createVMLauncher();
082 this.exitValues = new int[0];
083 this.workingDirectory = new File(".");
084 }
085
086 /**
087 * @see org.apache.commons.exec.Executor#getStreamHandler()
088 */
089 public ExecuteStreamHandler getStreamHandler() {
090 return streamHandler;
091 }
092
093 /**
094 * @see org.apache.commons.exec.Executor#setStreamHandler(org.apache.commons.exec.ExecuteStreamHandler)
095 */
096 public void setStreamHandler(ExecuteStreamHandler streamHandler) {
097 this.streamHandler = streamHandler;
098 }
099
100 /**
101 * @see org.apache.commons.exec.Executor#getWatchdog()
102 */
103 public ExecuteWatchdog getWatchdog() {
104 return watchdog;
105 }
106
107 /**
108 * @see org.apache.commons.exec.Executor#setWatchdog(org.apache.commons.exec.ExecuteWatchdog)
109 */
110 public void setWatchdog(ExecuteWatchdog watchDog) {
111 this.watchdog = watchDog;
112 }
113
114 /**
115 * @see org.apache.commons.exec.Executor#getProcessDestroyer()
116 */
117 public ProcessDestroyer getProcessDestroyer() {
118 return this.processDestroyer;
119 }
120
121 /**
122 * @see org.apache.commons.exec.Executor#setProcessDestroyer(ProcessDestroyer)
123 */
124 public void setProcessDestroyer(ProcessDestroyer processDestroyer) {
125 this.processDestroyer = processDestroyer;
126 }
127
128 /**
129 * @see org.apache.commons.exec.Executor#getWorkingDirectory()
130 */
131 public File getWorkingDirectory() {
132 return workingDirectory;
133 }
134
135 /**
136 * @see org.apache.commons.exec.Executor#setWorkingDirectory(java.io.File)
137 */
138 public void setWorkingDirectory(File dir) {
139 this.workingDirectory = dir;
140 }
141
142 /**
143 * @see org.apache.commons.exec.Executor#execute(CommandLine)
144 */
145 public int execute(final CommandLine command) throws ExecuteException,
146 IOException {
147 return execute(command, (Map) null);
148 }
149
150 /**
151 * @see org.apache.commons.exec.Executor#execute(CommandLine, java.util.Map)
152 */
153 public int execute(final CommandLine command, Map environment)
154 throws ExecuteException, IOException {
155
156 if (workingDirectory != null && !workingDirectory.exists()) {
157 throw new IOException(workingDirectory + " doesn't exist.");
158 }
159
160 return executeInternal(command, environment, workingDirectory, streamHandler);
161
162 }
163
164 /**
165 * @see org.apache.commons.exec.Executor#execute(CommandLine,
166 * org.apache.commons.exec.ExecuteResultHandler)
167 */
168 public void execute(final CommandLine command, ExecuteResultHandler handler)
169 throws ExecuteException, IOException {
170 execute(command, null, handler);
171 }
172
173 /**
174 * @see org.apache.commons.exec.Executor#execute(CommandLine,
175 * java.util.Map, org.apache.commons.exec.ExecuteResultHandler)
176 */
177 public void execute(final CommandLine command, final Map environment,
178 final ExecuteResultHandler handler) throws ExecuteException, IOException {
179
180 if (workingDirectory != null && !workingDirectory.exists()) {
181 throw new IOException(workingDirectory + " doesn't exist.");
182 }
183
184 executorThread = new Thread() {
185 public void run() {
186 int exitValue = Executor.INVALID_EXITVALUE;
187 try {
188 exitValue = executeInternal(command, environment, workingDirectory, streamHandler);
189 handler.onProcessComplete(exitValue);
190 } catch (ExecuteException e) {
191 handler.onProcessFailed(e);
192 } catch(Exception e) {
193 handler.onProcessFailed(new ExecuteException("Execution failed", exitValue, e));
194 }
195 }
196 };
197
198 getExecutorThread().start();
199 }
200
201 /** @see org.apache.commons.exec.Executor#setExitValue(int) */
202 public void setExitValue(final int value) {
203 this.setExitValues(new int[] {value});
204 }
205
206
207 /** @see org.apache.commons.exec.Executor#setExitValues(int[]) */
208 public void setExitValues(final int[] values) {
209 this.exitValues = (values == null ? null : (int[]) values.clone());
210 }
211
212 /** @see org.apache.commons.exec.Executor#isFailure(int) */
213 public boolean isFailure(final int exitValue) {
214
215 if(this.exitValues == null) {
216 return false;
217 }
218 else if(this.exitValues.length == 0) {
219 return this.launcher.isFailure(exitValue);
220 }
221 else {
222 for(int i=0; i<this.exitValues.length; i++) {
223 if(this.exitValues[i] == exitValue) {
224 return false;
225 }
226 }
227 }
228 return true;
229 }
230
231 /**
232 * Creates a process that runs a command.
233 *
234 * @param command
235 * the command to run
236 * @param env
237 * the environment for the command
238 * @param dir
239 * the working directory for the command
240 * @return the process started
241 * @throws IOException
242 * forwarded from the particular launcher used
243 */
244 protected Process launch(final CommandLine command, final Map env,
245 final File dir) throws IOException {
246
247 if (this.launcher == null) {
248 throw new IllegalStateException("CommandLauncher can not be null");
249 }
250
251 if (dir != null && !dir.exists()) {
252 throw new IOException(dir + " doesn't exist.");
253 }
254 return this.launcher.exec(command, env, dir);
255 }
256
257 /**
258 * Get the worker thread being used for asynchronous execution.
259 *
260 * @return the worker thread
261 */
262 protected Thread getExecutorThread() {
263 return executorThread;
264 }
265
266 /**
267 * Close the streams belonging to the given Process. In the
268 * original implementation all exceptions were dropped which
269 * is probably not a good thing. On the other hand the signature
270 * allows throwing an IOException so the current implementation
271 * might be quite okay.
272 *
273 * @param process the <CODE>Process</CODE>.
274 * @throws IOException closing one of the three streams failed
275 */
276 private void closeStreams(final Process process) throws IOException {
277
278 IOException caught = null;
279
280 try {
281 process.getInputStream().close();
282 }
283 catch(IOException e) {
284 caught = e;
285 }
286
287 try {
288 process.getOutputStream().close();
289 }
290 catch(IOException e) {
291 caught = e;
292 }
293
294 try {
295 process.getErrorStream().close();
296 }
297 catch(IOException e) {
298 caught = e;
299 }
300
301 if(caught != null) {
302 throw caught;
303 }
304 }
305
306 /**
307 * Execute an internal process.
308 *
309 * @param command the command to execute
310 * @param environment the execution enviroment
311 * @param dir the working directory
312 * @param streams process the streams (in, out, err) of the process
313 * @return the exit code of the process
314 * @throws IOException executing the process failed
315 */
316 private int executeInternal(final CommandLine command, final Map environment,
317 final File dir, final ExecuteStreamHandler streams) throws IOException {
318
319 final Process process = this.launch(command, environment, dir);
320
321 try {
322 streams.setProcessInputStream(process.getOutputStream());
323 streams.setProcessOutputStream(process.getInputStream());
324 streams.setProcessErrorStream(process.getErrorStream());
325 } catch (IOException e) {
326 process.destroy();
327 throw e;
328 }
329
330 streams.start();
331
332 try {
333
334 // add the process to the list of those to destroy if the VM exits
335 if(this.getProcessDestroyer() != null) {
336 this.getProcessDestroyer().add(process);
337 }
338
339 // associate the watchdog with the newly created process
340 if (watchdog != null) {
341 watchdog.start(process);
342 }
343
344 int exitValue = Executor.INVALID_EXITVALUE;
345
346 try {
347 exitValue = process.waitFor();
348 } catch (InterruptedException e) {
349 process.destroy();
350 }
351 finally {
352 // see http://bugs.sun.com/view_bug.do?bug_id=6420270
353 // see https://issues.apache.org/jira/browse/EXEC-46
354 // Process.waitFor should clear interrupt status when throwing InterruptedException
355 // but we have to do that manually
356 Thread.interrupted();
357 }
358
359 if (watchdog != null) {
360 watchdog.stop();
361 }
362
363 streams.stop();
364 closeStreams(process);
365
366 if (watchdog != null) {
367 try {
368 watchdog.checkException();
369 } catch (IOException e) {
370 throw e;
371 } catch (Exception e) {
372 throw new IOException(e.getMessage());
373 }
374 }
375
376 if(this.isFailure(exitValue)) {
377 throw new ExecuteException("Process exited with an error: " + exitValue, exitValue);
378 }
379
380 return exitValue;
381 } finally {
382 // remove the process to the list of those to destroy if the VM exits
383 if(this.getProcessDestroyer() != null) {
384 this.getProcessDestroyer().remove(process);
385 }
386 }
387 }
388 }