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}