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}