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.common.utils.ExceptionUtils;
049import org.nuxeo.ecm.platform.commandline.executor.api.CmdParameters;
050import org.nuxeo.ecm.platform.commandline.executor.api.CmdParameters.ParameterValue;
051import org.nuxeo.ecm.platform.commandline.executor.api.ExecResult;
052import org.nuxeo.ecm.platform.commandline.executor.service.CommandLineDescriptor;
053import org.nuxeo.ecm.platform.commandline.executor.service.EnvironmentDescriptor;
054
055/**
056 * Default implementation of the {@link Executor} interface. Use simple shell exec.
057 */
058public class ShellExecutor implements Executor {
059
060    private static final Log log = LogFactory.getLog(ShellExecutor.class);
061
062    @Deprecated
063    @Override
064    public ExecResult exec(CommandLineDescriptor cmdDesc, CmdParameters params) {
065        return exec(cmdDesc, params, new EnvironmentDescriptor());
066    }
067
068    protected static final AtomicInteger PIPE_COUNT = new AtomicInteger();
069
070    /** Used to split the contributed command, NOT the passed parameter values. */
071    protected static final Pattern COMMAND_SPLIT = Pattern.compile("\"([^\"]*)\"|'([^']*)'|[^\\s]+");
072
073    @Override
074    public ExecResult exec(CommandLineDescriptor cmdDesc, CmdParameters params, EnvironmentDescriptor env) {
075        String commandLine = cmdDesc.getCommand() + " " + String.join(" ", cmdDesc.getParametersString());
076        try {
077            if (log.isDebugEnabled()) {
078                log.debug("Running system command: " + commandLine);
079            }
080            long t0 = System.currentTimeMillis();
081            ExecResult res = exec1(cmdDesc, params, env);
082            long t1 = System.currentTimeMillis();
083            return new ExecResult(commandLine, res.getOutput(), t1 - t0, res.getReturnCode());
084        } catch (IOException e) {
085            return new ExecResult(commandLine, e);
086        }
087    }
088
089    protected ExecResult exec1(CommandLineDescriptor cmdDesc, CmdParameters params, EnvironmentDescriptor env)
090            throws IOException {
091        // split the configured parameters while keeping quoted parts intact
092        List<String> list = new ArrayList<>();
093        list.add(cmdDesc.getCommand());
094        Matcher m = COMMAND_SPLIT.matcher(cmdDesc.getParametersString());
095        while (m.find()) {
096            String word;
097            if (m.group(1) != null) {
098                word = m.group(1); // double-quoted
099            } else if (m.group(2) != null) {
100                word = m.group(2); // single-quoted
101            } else {
102                word = m.group(); // word
103            }
104            List<String> words = replaceParams(word, params);
105            list.addAll(words);
106        }
107
108        List<Process> processes = new LinkedList<>();
109        List<Thread> pipes = new LinkedList<>();
110        List<String> command = new LinkedList<>();
111        Process process = null;
112        for (Iterator<String> it = list.iterator(); it.hasNext();) {
113            String word = it.next();
114            boolean build;
115            if (word.equals("|")) {
116                build = true;
117            } else {
118                // on Windows, look up the command in the PATH first
119                if (command.isEmpty() && SystemUtils.IS_OS_WINDOWS) {
120                    command.add(getCommandAbsolutePath(word));
121                } else {
122                    command.add(word);
123                }
124                build = !it.hasNext();
125            }
126            if (!build) {
127                continue;
128            }
129            ProcessBuilder processBuilder = new ProcessBuilder(command);
130            command = new LinkedList<>(); // reset for next loop
131            processBuilder.directory(new File(env.getWorkingDirectory()));
132            processBuilder.environment().putAll(env.getParameters());
133            processBuilder.redirectErrorStream(true);
134            Process newProcess = processBuilder.start();
135            processes.add(newProcess);
136            if (process == null) {
137                // first process, nothing to input
138                IOUtils.closeQuietly(newProcess.getOutputStream());
139            } else {
140                // pipe previous process output into new process input
141                // needs a thread doing the piping because Java has no way to connect two children processes directly
142                // except through a filesystem named pipe but that can't be created in a portable manner
143                Thread pipe = pipe(process.getInputStream(), newProcess.getOutputStream());
144                pipes.add(pipe);
145            }
146            process = newProcess;
147        }
148
149        // get result from last process
150        @SuppressWarnings("null")
151        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
152        String line;
153        List<String> output = new ArrayList<>();
154        while ((line = reader.readLine()) != null) {
155            output.add(line);
156        }
157        reader.close();
158
159        // wait for all processes, get first non-0 exit status
160        int returnCode = 0;
161        for (Process p : processes) {
162            try {
163                int exitCode = p.waitFor();
164                if (returnCode == 0) {
165                    returnCode = exitCode;
166                }
167            } catch (InterruptedException e) {
168                ExceptionUtils.checkInterrupt(e);
169            }
170        }
171
172        // wait for all pipes
173        for (Thread t : pipes) {
174            try {
175                t.join();
176            } catch (InterruptedException e) {
177                ExceptionUtils.checkInterrupt(e);
178            }
179        }
180
181        return new ExecResult(null, output, 0, returnCode);
182    }
183
184    /**
185     * Returns a started daemon thread piping bytes from the InputStream to the OutputStream.
186     * <p>
187     * The streams are both closed when the copy is finished.
188     *
189     * @since 7.10
190     */
191    public static Thread pipe(InputStream in, OutputStream out) {
192        Runnable run = new Runnable() {
193            @Override
194            public void run() {
195                try {
196                    IOUtils.copy(in, out);
197                    out.flush();
198                } catch (IOException e) {
199                    throw new RuntimeException(e);
200                } finally {
201                    IOUtils.closeQuietly(in);
202                    IOUtils.closeQuietly(out);
203                }
204            }
205        };
206        Thread thread = new Thread(run, "Nuxeo-pipe-" + PIPE_COUNT.incrementAndGet());
207        thread.setDaemon(true);
208        thread.start();
209        return thread;
210    }
211
212    /**
213     * Expands parameter strings in a parameter word.
214     * <p>
215     * This may return several words if the parameter value is marked as a list.
216     *
217     * @since 7.10
218     */
219    public static List<String> replaceParams(String word, CmdParameters params) {
220        for (Entry<String, ParameterValue> es : params.getParameters().entrySet()) {
221            String name = es.getKey();
222            ParameterValue paramVal = es.getValue();
223            String param = "#{" + name + "}";
224            if (paramVal.isMulti()) {
225                if (word.equals(param)) {
226                    return paramVal.getValues();
227                }
228            } else if (word.contains(param)) {
229                word = word.replace(param, paramVal.getValue());
230            }
231
232        }
233        return Collections.singletonList(word);
234    }
235
236    /**
237     * Returns the absolute path of a command looked up on the PATH or the initial string if not found.
238     *
239     * @since 7.10
240     */
241    public static String getCommandAbsolutePath(String command) {
242        // no lookup if the command is already an absolute path
243        if (Paths.get(command).isAbsolute()) {
244            return command;
245        }
246        List<String> extensions = Arrays.asList("", ".exe");
247        // lookup for "command" or "command.exe" in the PATH
248        String[] systemPaths = System.getenv("PATH").split(File.pathSeparator);
249        for (String ext : extensions) {
250            for (String sp : systemPaths) {
251                try {
252                    Path path = Paths.get(sp.trim());
253                    if (Files.exists(path.resolve(command + ext))) {
254                        return path.resolve(command + ext).toString();
255                    }
256                } catch (InvalidPathException e) {
257                    log.warn("PATH environment variable contains an invalid path : " + e.getMessage());
258                }
259            }
260        }
261        // not found : return the initial string
262        return command;
263    }
264
265}