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