    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}