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}