001/* 002 * (C) Copyright 2006-2019 Nuxeo (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 static org.apache.commons.io.IOUtils.buffer; 024 025import java.io.File; 026import java.io.IOException; 027import java.io.InputStream; 028import java.io.OutputStream; 029import java.nio.charset.Charset; 030import java.nio.file.Files; 031import java.nio.file.InvalidPathException; 032import java.nio.file.Path; 033import java.nio.file.Paths; 034import java.util.ArrayList; 035import java.util.Arrays; 036import java.util.Collections; 037import java.util.Iterator; 038import java.util.LinkedList; 039import java.util.List; 040import java.util.Map.Entry; 041import java.util.concurrent.atomic.AtomicInteger; 042import java.util.regex.Matcher; 043import java.util.regex.Pattern; 044import java.util.stream.Collectors; 045 046import org.apache.commons.io.IOUtils; 047import org.apache.commons.lang3.SystemUtils; 048import org.apache.logging.log4j.LogManager; 049import org.apache.logging.log4j.Logger; 050import org.nuxeo.ecm.platform.commandline.executor.api.CmdParameters; 051import org.nuxeo.ecm.platform.commandline.executor.api.CmdParameters.ParameterValue; 052import org.nuxeo.ecm.platform.commandline.executor.api.ExecResult; 053import org.nuxeo.ecm.platform.commandline.executor.service.CommandLineDescriptor; 054import org.nuxeo.ecm.platform.commandline.executor.service.EnvironmentDescriptor; 055import org.nuxeo.runtime.RuntimeServiceException; 056 057/** 058 * Default implementation of the {@link Executor} interface. Use simple shell exec. 059 */ 060public class ShellExecutor implements Executor { 061 062 private static final Logger log = LogManager.getLogger(ShellExecutor.class); 063 064 protected static final AtomicInteger PIPE_COUNT = new AtomicInteger(); 065 066 /** Used to split the contributed command, NOT the passed parameter values. */ 067 protected static final Pattern COMMAND_SPLIT = Pattern.compile("\"([^\"]*)\"|'([^']*)'|[^\\s]+"); 068 069 @Override 070 public ExecResult exec(CommandLineDescriptor cmdDesc, CmdParameters params, EnvironmentDescriptor env) { 071 String commandLine = cmdDesc.getCommand() + " " + String.join(" ", cmdDesc.getParametersString()); 072 try { 073 log.debug("Running system command: {} with parameters: {}", () -> commandLine, 074 () -> params.getParameters() 075 .entrySet() 076 .stream() 077 .map(e -> String.format("%s=%s", e.getKey(), e.getValue().getValue())) 078 .collect(Collectors.joining(", "))); 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<ProcessBuilder> builders = new LinkedList<>(); 108 List<String> command = new LinkedList<>(); 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 var processBuilder = createProcessBuilder(command, env); 127 builders.add(processBuilder); 128 129 command = new LinkedList<>(); // reset for next loop 130 } 131 // now start all process 132 List<Process> processes = ProcessBuilder.startPipeline(builders); 133 134 // get result from last process 135 List<String> output; 136 Process last = processes.get(processes.size() - 1); 137 try (var stream = buffer(last.getInputStream())) { 138 output = IOUtils.readLines(stream, Charset.defaultCharset()); // use the host charset 139 } 140 141 // get return code from processes 142 int returnCode = getReturnCode(processes); 143 144 return new ExecResult(null, output, 0, returnCode); 145 } 146 147 protected ProcessBuilder createProcessBuilder(List<String> command, EnvironmentDescriptor env) { 148 ProcessBuilder processBuilder = new ProcessBuilder(command); 149 log.debug("Building Process for command: {}", () -> String.join(" ", processBuilder.command())); 150 processBuilder.directory(new File(env.getWorkingDirectory())); 151 processBuilder.environment().putAll(env.getParameters()); 152 processBuilder.redirectErrorStream(true); 153 return processBuilder; 154 } 155 156 protected int getReturnCode(List<Process> processes) { 157 // wait for all processes, get first non-0 exit status 158 int returnCode = 0; 159 for (Process p : processes) { 160 try { 161 int exitCode = p.waitFor(); 162 if (returnCode == 0) { 163 returnCode = exitCode; 164 } 165 } catch (InterruptedException e) { 166 Thread.currentThread().interrupt(); 167 throw new RuntimeServiceException(e); 168 } 169 } 170 return returnCode; 171 } 172 173 /** 174 * Returns a started daemon thread piping bytes from the InputStream to the OutputStream. 175 * <p> 176 * The streams are both closed when the copy is finished. 177 * 178 * @since 7.10 179 * @deprecated since 11.1, seems unused 180 */ 181 @Deprecated(since = "11.1") 182 public static Thread pipe(InputStream in, OutputStream out) { 183 Runnable run = () -> { 184 try (in; out) { 185 IOUtils.copy(in, out); 186 out.flush(); 187 } catch (IOException e) { 188 throw new RuntimeServiceException(e); 189 } 190 }; 191 Thread thread = new Thread(run, "Nuxeo-pipe-" + PIPE_COUNT.incrementAndGet()); 192 thread.setDaemon(true); 193 thread.start(); 194 return thread; 195 } 196 197 /** 198 * Expands parameter strings in a parameter word. 199 * <p> 200 * This may return several words if the parameter value is marked as a list. 201 * 202 * @since 7.10 203 */ 204 public static List<String> replaceParams(String word, CmdParameters params) { 205 for (Entry<String, ParameterValue> es : params.getParameters().entrySet()) { 206 String name = es.getKey(); 207 ParameterValue paramVal = es.getValue(); 208 String param = "#{" + name + "}"; 209 if (paramVal.isMulti()) { 210 if (word.equals(param)) { 211 return paramVal.getValues(); 212 } 213 } else if (word.contains(param)) { 214 word = word.replace(param, paramVal.getValue()); 215 } 216 217 } 218 return Collections.singletonList(word); 219 } 220 221 /** 222 * Returns the absolute path of a command looked up on the PATH or the initial string if not found. 223 * 224 * @since 7.10 225 */ 226 public static String getCommandAbsolutePath(String command) { 227 // no lookup if the command is already an absolute path 228 if (Paths.get(command).isAbsolute()) { 229 return command; 230 } 231 List<String> extensions = Arrays.asList("", ".exe"); 232 // lookup for "command" or "command.exe" in the PATH 233 String[] systemPaths = System.getenv("PATH").split(File.pathSeparator); 234 for (String ext : extensions) { 235 for (String sp : systemPaths) { 236 String fullCommand = command + ext; 237 try { 238 Path path = Paths.get(sp.trim()); 239 if (Files.exists(path.resolve(fullCommand))) { 240 return path.resolve(fullCommand).toString(); 241 } 242 } catch (InvalidPathException e) { 243 log.warn("PATH environment variable contains an invalid path: {}", fullCommand, e); 244 } 245 } 246 } 247 // not found : return the initial string 248 return command; 249 } 250 251}