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}