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}