    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.Closeable;
    020import java.io.File;
    021import java.io.IOException;
    022import java.util.Map;
    023import java.util.concurrent.Executors;
    024import java.util.concurrent.ThreadFactory;
    025import java.util.function.Supplier;
    026
    027import org.apache.commons.exec.launcher.CommandLauncher;
    028import org.apache.commons.exec.launcher.CommandLauncherFactory;
    029
    030/**
    031 * The default class to start a subprocess. The implementation allows to
    032 * <ul>
    033 * <li>set a current working directory for the subprocess</li>
    034 * <li>provide a set of environment variables passed to the subprocess</li>
    035 * <li>capture the subprocess output of stdout and stderr using an ExecuteStreamHandler</li>
    036 * <li>kill long-running processes using an ExecuteWatchdog</li>
    037 * <li>define a set of expected exit values</li>
    038 * <li>terminate any started processes when the main process is terminating using a ProcessDestroyer</li>
    039 * </ul>
    040 *
    041 * The following example shows the basic usage:
    042 *
    043 * <pre>
    044 * Executor exec = DefaultExecutor.builder().get();
    045 * CommandLine cl = new CommandLine("ls -l");
    046 * int exitvalue = exec.execute(cl);
    047 * </pre>
    048 */
    049public class DefaultExecutor implements Executor {
    050
    051    /**
    052     * Constructs a new builder.
    053     *
    054     * @param <T> The builder type.
    055     * @since 1.4.0
    056     */
    057    public static class Builder<T extends Builder<T>> implements Supplier<DefaultExecutor> {
    058
    059        private ThreadFactory threadFactory;
    060        private ExecuteStreamHandler executeStreamHandler;
    061        private File workingDirectory;
    062
    063        @SuppressWarnings("unchecked")
    064        T asThis() {
    065            return (T) this;
    066        }
    067
    068        /**
    069         * Creates a new configured DefaultExecutor.
    070         *
    071         * @return a new configured DefaultExecutor.
    072         */
    073        @Override
    074        public DefaultExecutor get() {
    075            return new DefaultExecutor(threadFactory, executeStreamHandler, workingDirectory);
    076        }
    077
    078        ExecuteStreamHandler getExecuteStreamHandler() {
    079            return executeStreamHandler;
    080        }
    081
    082        ThreadFactory getThreadFactory() {
    083            return threadFactory;
    084        }
    085
    086        File getWorkingDirectory() {
    087            return workingDirectory;
    088        }
    089
    090        /**
    091         * Sets the PumpStreamHandler.
    092         *
    093         * @param executeStreamHandler the ExecuteStreamHandler, null resets to the default.
    094         * @return this.
    095         */
    096        public T setExecuteStreamHandler(final ExecuteStreamHandler executeStreamHandler) {
    097            this.executeStreamHandler = executeStreamHandler;
    098            return asThis();
    099        }
    100
    101        /**
    102         * Sets the ThreadFactory.
    103         *
    104         * @param threadFactory the ThreadFactory, null resets to the default.
    105         * @return this.
    106         */
    107        public T setThreadFactory(final ThreadFactory threadFactory) {
    108            this.threadFactory = threadFactory;
    109            return asThis();
    110        }
    111
    112        /**
    113         * Sets the working directory.
    114         *
    115         * @param workingDirectory the working directory., null resets to the default.
    116         * @return this.
    117         */
    118        public T setWorkingDirectory(final File workingDirectory) {
    119            this.workingDirectory = workingDirectory;
    120            return asThis();
    121        }
    122
    123    }
    124
    125    /**
    126     * Creates a new builder.
    127     *
    128     * @return a new builder.
    129     * @since 1.4.0
    130     */
    131    public static Builder<?> builder() {
    132        return new Builder<>();
    133    }
    134
    135    /** Taking care of output and error stream. */
    136    private ExecuteStreamHandler executeStreamHandler;
    137
    138    /** The working directory of the process. */
    139    private File workingDirectory;
    140
    141    /** Monitoring of long running processes. */
    142    private ExecuteWatchdog watchdog;
    143
    144    /** The exit values considered to be successful. */
    145    private int[] exitValues;
    146
    147    /** Launches the command in a new process. */
    148    private final CommandLauncher launcher;
    149
    150    /** Optional cleanup of started processes. */
    151    private ProcessDestroyer processDestroyer;
    152
    153    /** Worker thread for asynchronous execution. */
    154    private Thread executorThread;
    155
    156    /** The first exception being caught to be thrown to the caller. */
    157    private IOException exceptionCaught;
    158
    159    /**
    160     * The thread factory.
    161     */
    162    private final ThreadFactory threadFactory;
    163
    164    /**
    165     * Constructs a default {@code PumpStreamHandler} and sets the working directory of the subprocess to the current working directory.
    166     *
    167     * 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}
    168     * and {@code System.err} to avoid a blocked or deadlocked subprocess (see {@link Process Process}).
    169     *
    170     * @deprecated Use {@link Builder#get()}.
    171     */
    172    @Deprecated
    173    public DefaultExecutor() {
    174        this(Executors.defaultThreadFactory(), new PumpStreamHandler(), new File("."));
    175    }
    176
    177    DefaultExecutor(final ThreadFactory threadFactory, final ExecuteStreamHandler executeStreamHandler, final File workingDirectory) {
    178        this.threadFactory = threadFactory != null ? threadFactory : Executors.defaultThreadFactory();
    179        this.executeStreamHandler = executeStreamHandler != null ? executeStreamHandler : new PumpStreamHandler();
    180        this.workingDirectory = workingDirectory != null ? workingDirectory : new File(".");
    181        this.launcher = CommandLauncherFactory.createVMLauncher();
    182        this.exitValues = new int[0];
    183    }
    184
    185    private void checkWorkingDirectory() throws IOException {
    186        checkWorkingDirectory(workingDirectory);
    187    }
    188
    189    private void checkWorkingDirectory(final File directory) throws IOException {
    190        if (directory != null && !directory.exists()) {
    191            throw new IOException(directory + " doesn't exist.");
    192        }
    193    }
    194
    195    /**
    196     * Closes the Closeable, remembering any exception.
    197     *
    198     * @param closeable the {@link Closeable} to close.
    199     */
    200    private void closeCatch(final Closeable closeable) {
    201        try {
    202            closeable.close();
    203        } catch (final IOException e) {
    204            setExceptionCaught(e);
    205        }
    206    }
    207
    208    /**
    209     * Closes the streams belonging to the given Process.
    210     *
    211     * @param process the {@link Process}.
    212     */
    213    @SuppressWarnings("resource")
    214    private void closeProcessStreams(final Process process) {
    215        closeCatch(process.getInputStream());
    216        closeCatch(process.getOutputStream());
    217        closeCatch(process.getErrorStream());
    218    }
    219
    220    /**
    221     * Creates a thread waiting for the result of an asynchronous execution.
    222     *
    223     * @param runnable the runnable passed to the thread.
    224     * @param name     the name of the thread.
    225     * @return the thread
    226     */
    227    protected Thread createThread(final Runnable runnable, final String name) {
    228        return ThreadUtil.newThread(threadFactory, runnable, name, false);
    229    }
    230
    231    /**
    232     * @see org.apache.commons.exec.Executor#execute(CommandLine)
    233     */
    234    @Override
    235    public int execute(final CommandLine command) throws ExecuteException, IOException {
    236        return execute(command, (Map<String, String>) null);
    237    }
    238
    239    /**
    240     * @see org.apache.commons.exec.Executor#execute(CommandLine, org.apache.commons.exec.ExecuteResultHandler)
    241     */
    242    @Override
    243    public void execute(final CommandLine command, final ExecuteResultHandler handler) throws ExecuteException, IOException {
    244        execute(command, null, handler);
    245    }
    246
    247    /**
    248     * @see org.apache.commons.exec.Executor#execute(CommandLine, java.util.Map)
    249     */
    250    @Override
    251    public int execute(final CommandLine command, final Map<String, String> environment) throws ExecuteException, IOException {
    252        checkWorkingDirectory();
    253        return executeInternal(command, environment, workingDirectory, executeStreamHandler);
    254    }
    255
    256    /**
    257     * @see org.apache.commons.exec.Executor#execute(CommandLine, java.util.Map, org.apache.commons.exec.ExecuteResultHandler)
    258     */
    259    @Override
    260    public void execute(final CommandLine command, final Map<String, String> environment, final ExecuteResultHandler handler)
    261            throws ExecuteException, IOException {
    262        checkWorkingDirectory();
    263        if (watchdog != null) {
    264            watchdog.setProcessNotStarted();
    265        }
    266        executorThread = createThread(() -> {
    267            int exitValue = Executor.INVALID_EXITVALUE;
    268            try {
    269                exitValue = executeInternal(command, environment, workingDirectory, executeStreamHandler);
    270                handler.onProcessComplete(exitValue);
    271            } catch (final ExecuteException e) {
    272                handler.onProcessFailed(e);
    273            } catch (final Exception e) {
    274                handler.onProcessFailed(new ExecuteException("Execution failed", exitValue, e));
    275            }
    276        }, "CommonsExecDefaultExecutor");
    277        getExecutorThread().start();
    278    }
    279
    280    /**
    281     * Execute an internal process. If the executing thread is interrupted while waiting for the child process to return the child process will be killed.
    282     *
    283     * @param command          the command to execute.
    284     * @param environment      the execution environment.
    285     * @param workingDirectory the working directory.
    286     * @param streams          process the streams (in, out, err) of the process.
    287     * @return the exit code of the process.
    288     * @throws IOException executing the process failed.
    289     */
    290    private int executeInternal(final CommandLine command, final Map<String, String> environment, final File workingDirectory,
    291            final ExecuteStreamHandler streams) throws IOException {
    292        final Process process;
    293        exceptionCaught = null;
    294        try {
    295            process = launch(command, environment, workingDirectory);
    296        } catch (final IOException e) {
    297            if (watchdog != null) {
    298                watchdog.failedToStart(e);
    299            }
    300            throw e;
    301        }
    302        try {
    303            setStreams(streams, process);
    304        } catch (final IOException e) {
    305            process.destroy();
    306            if (watchdog != null) {
    307                watchdog.failedToStart(e);
    308            }
    309            throw e;
    310        }
    311        streams.start();
    312        try {
    313            // add the process to the list of those to destroy if the VM exits
    314            if (getProcessDestroyer() != null) {
    315                getProcessDestroyer().add(process);
    316            }
    317            // associate the watchdog with the newly created process
    318            if (watchdog != null) {
    319                watchdog.start(process);
    320            }
    321            int exitValue = Executor.INVALID_EXITVALUE;
    322            try {
    323                exitValue = process.waitFor();
    324            } catch (final InterruptedException e) {
    325                process.destroy();
    326            } finally {
    327                // see http://bugs.sun.com/view_bug.do?bug_id=6420270
    328                // see https://issues.apache.org/jira/browse/EXEC-46
    329                // Process.waitFor should clear interrupt status when throwing InterruptedException
    330                // but we have to do that manually
    331                Thread.interrupted();
    332            }
    333            if (watchdog != null) {
    334                watchdog.stop();
    335            }
    336            try {
    337                streams.stop();
    338            } catch (final IOException e) {
    339                setExceptionCaught(e);
    340            }
    341            closeProcessStreams(process);
    342            if (getExceptionCaught() != null) {
    343                throw getExceptionCaught();
    344            }
    345            if (watchdog != null) {
    346                try {
    347                    watchdog.checkException();
    348                } catch (final IOException e) {
    349                    throw e;
    350                } catch (final Exception e) {
    351                    throw new IOException(e);
    352                }
    353            }
    354            if (isFailure(exitValue)) {
    355                throw new ExecuteException("Process exited with an error: " + exitValue, exitValue);
    356            }
    357            return exitValue;
    358        } finally {
    359            // remove the process to the list of those to destroy if the VM exits
    360            if (getProcessDestroyer() != null) {
    361                getProcessDestroyer().remove(process);
    362            }
    363        }
    364    }
    365
    366    /**
    367     * Gets the first IOException being thrown.
    368     *
    369     * @return the first IOException being caught.
    370     */
    371    private IOException getExceptionCaught() {
    372        return exceptionCaught;
    373    }
    374
    375    /**
    376     * Gets the worker thread being used for asynchronous execution.
    377     *
    378     * @return the worker thread.
    379     */
    380    protected Thread getExecutorThread() {
    381        return executorThread;
    382    }
    383
    384    /**
    385     * @see org.apache.commons.exec.Executor#getProcessDestroyer()
    386     */
    387    @Override
    388    public ProcessDestroyer getProcessDestroyer() {
    389        return processDestroyer;
    390    }
    391
    392    /**
    393     * @see org.apache.commons.exec.Executor#getStreamHandler()
    394     */
    395    @Override
    396    public ExecuteStreamHandler getStreamHandler() {
    397        return executeStreamHandler;
    398    }
    399
    400    /**
    401     * Gets the thread factory. Z
    402     *
    403     * @return the thread factory.
    404     */
    405    ThreadFactory getThreadFactory() {
    406        return threadFactory;
    407    }
    408
    409    /**
    410     * @see org.apache.commons.exec.Executor#getWatchdog()
    411     */
    412    @Override
    413    public ExecuteWatchdog getWatchdog() {
    414        return watchdog;
    415    }
    416
    417    /**
    418     * @see org.apache.commons.exec.Executor#getWorkingDirectory()
    419     */
    420    @Override
    421    public File getWorkingDirectory() {
    422        return workingDirectory;
    423    }
    424
    425    /** @see org.apache.commons.exec.Executor#isFailure(int) */
    426    @Override
    427    public boolean isFailure(final int exitValue) {
    428        if (exitValues == null) {
    429            return false;
    430        }
    431        if (exitValues.length == 0) {
    432            return launcher.isFailure(exitValue);
    433        }
    434        for (final int exitValue2 : exitValues) {
    435            if (exitValue2 == exitValue) {
    436                return false;
    437            }
    438        }
    439        return true;
    440    }
    441
    442    /**
    443     * Creates a process that runs a command.
    444     *
    445     * @param command          the command to run.
    446     * @param env              the environment for the command.
    447     * @param workingDirectory the working directory for the command.
    448     * @return the process started.
    449     * @throws IOException forwarded from the particular launcher used.
    450     */
    451    protected Process launch(final CommandLine command, final Map<String, String> env, final File workingDirectory) throws IOException {
    452        if (launcher == null) {
    453            throw new IllegalStateException("CommandLauncher can not be null");
    454        }
    455        checkWorkingDirectory(workingDirectory);
    456        return launcher.exec(command, env, workingDirectory);
    457    }
    458
    459    /**
    460     * Sets the first IOException thrown.
    461     *
    462     * @param e the IOException.
    463     */
    464    private void setExceptionCaught(final IOException e) {
    465        if (exceptionCaught == null) {
    466            exceptionCaught = e;
    467        }
    468    }
    469
    470    /** @see org.apache.commons.exec.Executor#setExitValue(int) */
    471    @Override
    472    public void setExitValue(final int value) {
    473        setExitValues(new int[] { value });
    474    }
    475
    476    /** @see org.apache.commons.exec.Executor#setExitValues(int[]) */
    477    @Override
    478    public void setExitValues(final int[] values) {
    479        exitValues = values == null ? null : (int[]) values.clone();
    480    }
    481
    482    /**
    483     * @see org.apache.commons.exec.Executor#setProcessDestroyer(ProcessDestroyer)
    484     */
    485    @Override
    486    public void setProcessDestroyer(final ProcessDestroyer processDestroyer) {
    487        this.processDestroyer = processDestroyer;
    488    }
    489
    490    /**
    491     * @see org.apache.commons.exec.Executor#setStreamHandler(org.apache.commons.exec.ExecuteStreamHandler)
    492     */
    493    @Override
    494    public void setStreamHandler(final ExecuteStreamHandler streamHandler) {
    495        this.executeStreamHandler = streamHandler;
    496    }
    497
    498    @SuppressWarnings("resource")
    499    private void setStreams(final ExecuteStreamHandler streams, final Process process) throws IOException {
    500        streams.setProcessInputStream(process.getOutputStream());
    501        streams.setProcessOutputStream(process.getInputStream());
    502        streams.setProcessErrorStream(process.getErrorStream());
    503    }
    504
    505    /**
    506     * @see org.apache.commons.exec.Executor#setWatchdog(org.apache.commons.exec.ExecuteWatchdog)
    507     */
    508    @Override
    509    public void setWatchdog(final ExecuteWatchdog watchdog) {
    510        this.watchdog = watchdog;
    511    }
    512
    513    /**
    514     * Sets the working directory.
    515     *
    516     * @see org.apache.commons.exec.Executor#setWorkingDirectory(java.io.File)
    517     * @deprecated Use {@link Builder#setWorkingDirectory(File)}.
    518     */
    519    @Deprecated
    520    @Override
    521    public void setWorkingDirectory(final File workingDirectory) {
    522        this.workingDirectory = workingDirectory;
    523    }
    524
    525}