001/*
002 * (C) Copyright 2006-2015 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Thierry Delprat
018 *     Julien Carsique
019 *     Florent Guillaume
020 */
021package org.nuxeo.ecm.platform.commandline.executor.service.executors;
022
023import java.io.BufferedReader;
024import java.io.File;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.InputStreamReader;
028import java.io.OutputStream;
029import java.nio.file.Files;
030import java.nio.file.InvalidPathException;
031import java.nio.file.Path;
032import java.nio.file.Paths;
033import java.util.ArrayList;
034import java.util.Arrays;
035import java.util.Collections;
036import java.util.Iterator;
037import java.util.LinkedList;
038import java.util.List;
039import java.util.Map.Entry;
040import java.util.concurrent.atomic.AtomicInteger;
041import java.util.regex.Matcher;
042import java.util.regex.Pattern;
043
044import org.apache.commons.io.IOUtils;
045import org.apache.commons.lang3.SystemUtils;
046import org.apache.commons.logging.Log;
047import org.apache.commons.logging.LogFactory;
048import org.nuxeo.ecm.platform.commandline.executor.api.CmdParameters;
049import org.nuxeo.ecm.platform.commandline.executor.api.CmdParameters.ParameterValue;
050import org.nuxeo.ecm.platform.commandline.executor.api.ExecResult;
051import org.nuxeo.ecm.platform.commandline.executor.service.CommandLineDescriptor;
052import org.nuxeo.ecm.platform.commandline.executor.service.EnvironmentDescriptor;
053
054/**
055 * Default implementation of the {@link Executor} interface. Use simple shell exec.
056 */
057public class ShellExecutor implements Executor {
058
059    private static final Log log = LogFactory.getLog(ShellExecutor.class);
060
061    @Deprecated
062    @Override
063    public ExecResult exec(CommandLineDescriptor cmdDesc, CmdParameters params) {
064        return exec(cmdDesc, params, new EnvironmentDescriptor());
065    }
066
067    protected static final AtomicInteger PIPE_COUNT = new AtomicInteger();
068
069    /** Used to split the contributed command, NOT the passed parameter values. */
070    protected static final Pattern COMMAND_SPLIT = Pattern.compile("\"([^\"]*)\"|'([^']*)'|[^\\s]+");
071
072    @Override
073    public ExecResult exec(CommandLineDescriptor cmdDesc, CmdParameters params, EnvironmentDescriptor env) {
074        String commandLine = cmdDesc.getCommand() + " " + String.join(" ", cmdDesc.getParametersString());
075        try {
076            if (log.isDebugEnabled()) {
077                log.debug("Running system command: " + commandLine);
078            }
079            long t0 = System.currentTimeMillis();
080            ExecResult res = exec1(cmdDesc, params, env);
081            long t1 = System.currentTimeMillis();
082            return new ExecResult(commandLine, res.getOutput(), t1 - t0, res.getReturnCode());
083        } catch (IOException e) {
084            return new ExecResult(commandLine, e);
085        }
086    }
087
088    protected ExecResult exec1(CommandLineDescriptor cmdDesc, CmdParameters params, EnvironmentDescriptor env)
089            throws IOException {
090        // split the configured parameters while keeping quoted parts intact
091        List<String> list = new ArrayList<>();
092        list.add(cmdDesc.getCommand());
093        Matcher m = COMMAND_SPLIT.matcher(cmdDesc.getParametersString());
094        while (m.find()) {
095            String word;
096            if (m.group(1) != null) {
097                word = m.group(1); // double-quoted
098            } else if (m.group(2) != null) {
099                word = m.group(2); // single-quoted
100            } else {
101                word = m.group(); // word
102            }
103            List<String> words = replaceParams(word, params);
104            list.addAll(words);
105        }
106
107        List<Process> processes = new LinkedList<>();
108        List<Thread> pipes = new LinkedList<>();
109        List<String> command = new LinkedList<>();
110        Process process = null;
111        for (Iterator<String> it = list.iterator(); it.hasNext();) {
112            String word = it.next();
113            boolean build;
114            if (word.equals("|")) {
115                build = true;
116            } else {
117                // on Windows, look up the command in the PATH first
118                if (command.isEmpty() && SystemUtils.IS_OS_WINDOWS) {
119                    command.add(getCommandAbsolutePath(word));
120                } else {
121                    command.add(word);
122                }
123                build = !it.hasNext();
124            }
125            if (!build) {
126                continue;
127            }
128            ProcessBuilder processBuilder = new ProcessBuilder(command);
129            command = new LinkedList<>(); // reset for next loop
130            processBuilder.directory(new File(env.getWorkingDirectory()));
131            processBuilder.environment().putAll(env.getParameters());
132            processBuilder.redirectErrorStream(true);
133            Process newProcess = processBuilder.start();
134            processes.add(newProcess);
135            if (process == null) {
136                // first process, nothing to input
137                IOUtils.closeQuietly(newProcess.getOutputStream());
138            } else {
139                // pipe previous process output into new process input
140                // needs a thread doing the piping because Java has no way to connect two children processes directly
141                // except through a filesystem named pipe but that can't be created in a portable manner
142                Thread pipe = pipe(process.getInputStream(), newProcess.getOutputStream());
143                pipes.add(pipe);
144            }
145            process = newProcess;
146        }
147
148        // get result from last process
149        @SuppressWarnings("null")
150        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
151        String line;
152        List<String> output = new ArrayList<>();
153        while ((line = reader.readLine()) != null) {
154            output.add(line);
155        }
156        reader.close();
157
158        // wait for all processes, get first non-0 exit status
159        int returnCode = 0;
160        for (Process p : processes) {
161            try {
162                int exitCode = p.waitFor();
163                if (returnCode == 0) {
164                    returnCode = exitCode;
165                }
166            } catch (InterruptedException e) {
167                Thread.currentThread().interrupt();
168                throw new RuntimeException(e);
169            }
170        }
171
172        // wait for all pipes
173        for (Thread t : pipes) {
174            try {
175                t.join();
176            } catch (InterruptedException e) {
177                Thread.currentThread().interrupt();
178                throw new RuntimeException(e);
179            }
180        }
181
182        return new ExecResult(null, output, 0, returnCode);
183    }
184
185    /**
186     * Returns a started daemon thread piping bytes from the InputStream to the OutputStream.
187     * <p>
188     * The streams are both closed when the copy is finished.
189     *
190     * @since 7.10
191     */
192    public static Thread pipe(InputStream in, OutputStream out) {
193        Runnable run = new Runnable() {
194            @Override
195            public void run() {
196                try {
197                    IOUtils.copy(in, out);
198                    out.flush();
199                } catch (IOException e) {
200                    throw new RuntimeException(e);
201                } finally {
202                    IOUtils.closeQuietly(in);
203                    IOUtils.closeQuietly(out);
204                }
205            }
206        };
207        Thread thread = new Thread(run, "Nuxeo-pipe-" + PIPE_COUNT.incrementAndGet());
208        thread.setDaemon(true);
209        thread.start();
210        return thread;
211    }
212
213    /**
214     * Expands parameter strings in a parameter word.
215     * <p>
216     * This may return several words if the parameter value is marked as a list.
217     *
218     * @since 7.10
219     */
220    public static List<String> replaceParams(String word, CmdParameters params) {
221        for (Entry<String, ParameterValue> es : params.getParameters().entrySet()) {
222            String name = es.getKey();
223            ParameterValue paramVal = es.getValue();
224            String param = "#{" + name + "}";
225            if (paramVal.isMulti()) {
226                if (word.equals(param)) {
227                    return paramVal.getValues();
228                }
229            } else if (word.contains(param)) {
230                word = word.replace(param, paramVal.getValue());
231            }
232
233        }
234        return Collections.singletonList(word);
235    }
236
237    /**
238     * Returns the absolute path of a command looked up on the PATH or the initial string if not found.
239     *
240     * @since 7.10
241     */
242    public static String getCommandAbsolutePath(String command) {
243        // no lookup if the command is already an absolute path
244        if (Paths.get(command).isAbsolute()) {
245            return command;
246        }
247        List<String> extensions = Arrays.asList("", ".exe");
248        // lookup for "command" or "command.exe" in the PATH
249        String[] systemPaths = System.getenv("PATH").split(File.pathSeparator);
250        for (String ext : extensions) {
251            for (String sp : systemPaths) {
252                try {
253                    Path path = Paths.get(sp.trim());
254                    if (Files.exists(path.resolve(command + ext))) {
255                        return path.resolve(command + ext).toString();
256                    }
257                } catch (InvalidPathException e) {
258                    log.warn("PATH environment variable contains an invalid path : " + e.getMessage());
259                }
260            }
261        }
262        // not found : return the initial string
263        return command;
264    }
265
266}