    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}