/****************************************************************************** * Copyright (C) 2013 Fabio Zadrozny * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Fabio Zadrozny <fabiofz@gmail.com> - initial API and implementation ******************************************************************************/ package org.python.pydev.shared_core.process; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Status; import org.python.pydev.shared_core.SharedCorePlugin; import org.python.pydev.shared_core.io.ThreadStreamReader; import org.python.pydev.shared_core.log.Log; import org.python.pydev.shared_core.string.FastStringBuffer; import org.python.pydev.shared_core.string.StringUtils; import org.python.pydev.shared_core.structure.Tuple; import org.python.pydev.shared_core.utils.PlatformUtils; public class ProcessUtils { /** * @param process process from where the output should be gotten * @param executionString string to execute (only for errors) * @param monitor monitor for giving progress * @return a tuple with the output of stdout and stderr */ public static Tuple<String, String> getProcessOutput(Process process, String executionString, IProgressMonitor monitor, String encoding) { if (monitor == null) { monitor = new NullProgressMonitor(); } if (process != null) { try { process.getOutputStream().close(); //we won't write to it... } catch (IOException e2) { } monitor.setTaskName("Reading output..."); monitor.worked(5); //No need to synchronize as we'll waitFor() the process before getting the contents. ThreadStreamReader std = new ThreadStreamReader(process.getInputStream(), false, encoding); ThreadStreamReader err = new ThreadStreamReader(process.getErrorStream(), false, encoding); std.start(); err.start(); boolean interrupted = true; while (interrupted) { interrupted = false; try { monitor.setTaskName("Waiting for process to finish."); monitor.worked(5); process.waitFor(); //wait until the process completion. } catch (InterruptedException e1) { interrupted = true; } } try { //just to see if we get something after the process finishes (and let the other threads run). Object sync = new Object(); synchronized (sync) { sync.wait(50); } } catch (Exception e) { //ignore } return new Tuple<String, String>(std.getContents(), err.getContents()); } else { try { throw new CoreException(new Status(IStatus.ERROR, SharedCorePlugin.PLUGIN_ID, "Error creating process - got null process(" + executionString + ")", new Exception( "Error creating process - got null process."))); } catch (CoreException e) { Log.log(IStatus.ERROR, e.getMessage(), e); } } return new Tuple<String, String>("", "Error creating process - got null process(" + executionString + ")"); //no output } /** * Passes the commands directly to Runtime.exec (with the passed envp) */ public static Process createProcess(String[] cmdarray, String[] envp, File workingDir) throws IOException { return Runtime.getRuntime().exec(getWithoutEmptyParams(cmdarray), getWithoutEmptyParams(envp), workingDir); } /** * @return a new array without any null/empty elements originally contained in the array. */ private static String[] getWithoutEmptyParams(String[] cmdarray) { if (cmdarray == null) { return null; } ArrayList<String> list = new ArrayList<String>(); for (String string : cmdarray) { if (string != null && string.length() > 0) { list.add(string); } } return list.toArray(new String[list.size()]); } /** * @return a tuple with the process created and a string representation of the cmdarray. */ public static Tuple<Process, String> run(String[] cmdarray, String[] envp, File workingDir, IProgressMonitor monitor) { if (monitor == null) { monitor = new NullProgressMonitor(); } String executionString = getArgumentsAsStr(cmdarray); monitor.setTaskName("Executing: " + executionString); monitor.worked(5); Process process = null; try { monitor.setTaskName("Making pythonpath environment..." + executionString); //Otherwise, use default (used when configuring the interpreter for instance). monitor.setTaskName("Making exec..." + executionString); if (workingDir != null) { if (!workingDir.isDirectory()) { throw new RuntimeException(StringUtils.format( "Working dir must be an existing directory (received: %s)", workingDir)); } } process = createProcess(cmdarray, envp, workingDir); } catch (Exception e) { throw new RuntimeException(e); } return new Tuple<Process, String>(process, executionString); } /** * Runs the given command line and returns a tuple with the output (stdout and stderr) of executing it. * * @param cmdarray array with the commands to be passed to Runtime.exec * @param workingDir the working dir (may be null) * @param project the project (used to get the pythonpath and put it into the environment) -- if null, no environment is passed. * @param monitor the progress monitor to be used -- may be null * * @return a tuple with stdout and stderr */ public static Tuple<String, String> runAndGetOutput(String[] cmdarray, String[] envp, File workingDir, IProgressMonitor monitor, String encoding) { Tuple<Process, String> r = run(cmdarray, envp, workingDir, monitor); return getProcessOutput(r.o1, r.o2, monitor, encoding); } /** * copied from org.eclipse.jdt.internal.launching.StandardVMRunner * @param args - other arguments to be added to the command line (may be null) * @return */ public static String getArgumentsAsStr(String[] commandLine, String... args) { if (args != null && args.length > 0) { String[] newCommandLine = new String[commandLine.length + args.length]; System.arraycopy(commandLine, 0, newCommandLine, 0, commandLine.length); System.arraycopy(args, 0, newCommandLine, commandLine.length, args.length); commandLine = newCommandLine; } if (commandLine.length < 1) { return ""; //$NON-NLS-1$ } FastStringBuffer buf = new FastStringBuffer(); FastStringBuffer command = new FastStringBuffer(); for (int i = 0; i < commandLine.length; i++) { if (commandLine[i] == null) { continue; //ignore nulls (changed from original code) } buf.append(' '); char[] characters = commandLine[i].toCharArray(); command.clear(); boolean containsSpace = false; for (int j = 0; j < characters.length; j++) { char character = characters[j]; if (character == '\"') { command.append('\\'); } else if (character == ' ') { containsSpace = true; } command.append(character); } if (containsSpace) { buf.append('\"'); buf.append(command.toString()); buf.append('\"'); } else { buf.append(command.toString()); } } return buf.toString(); } public static String getEnvironmentAsStr(String[] envp) { return StringUtils.join("\n", envp); } /** * @param env a map that will have its values formatted to xx=yy, so that it can be passed in an exec * @return an array with the formatted map */ public static String[] getMapEnvAsArray(Map<String, String> env) { List<String> strings = new ArrayList<String>(env.size()); FastStringBuffer buffer = new FastStringBuffer(); for (Iterator<Map.Entry<String, String>> iter = env.entrySet().iterator(); iter.hasNext();) { Map.Entry<String, String> entry = iter.next(); buffer.clear().append(entry.getKey()); buffer.append('=').append(entry.getValue()); strings.add(buffer.toString()); } return strings.toArray(new String[strings.size()]); } /** * Parses the given command line into separate arguments that can be passed to * <code>DebugPlugin.exec(String[], File)</code>. Embedded quotes and slashes * are escaped. * * @param args command line arguments as a single string * @return individual arguments * @since 3.1 * * Gotten from org.eclipse.debug.core.DebugPlugin */ public static String[] parseArguments(String args) { if (args == null || args.length() == 0) { return new String[0]; } if (PlatformUtils.isWindowsPlatform()) { return parseArgumentsWindows(args); } return parseArgumentsImpl(args); } /** * Gotten from org.eclipse.debug.core.DebugPlugin */ @SuppressWarnings({ "rawtypes", "unchecked" }) private static String[] parseArgumentsImpl(String args) { // man sh, see topic QUOTING List result = new ArrayList(); final int DEFAULT = 0; final int ARG = 1; final int IN_DOUBLE_QUOTE = 2; final int IN_SINGLE_QUOTE = 3; int state = DEFAULT; StringBuffer buf = new StringBuffer(); int len = args.length(); for (int i = 0; i < len; i++) { char ch = args.charAt(i); if (Character.isWhitespace(ch)) { if (state == DEFAULT) { // skip continue; } else if (state == ARG) { state = DEFAULT; result.add(buf.toString()); buf.setLength(0); continue; } } switch (state) { case DEFAULT: case ARG: if (ch == '"') { state = IN_DOUBLE_QUOTE; } else if (ch == '\'') { state = IN_SINGLE_QUOTE; } else if (ch == '\\' && i + 1 < len) { state = ARG; ch = args.charAt(++i); buf.append(ch); } else { state = ARG; buf.append(ch); } break; case IN_DOUBLE_QUOTE: if (ch == '"') { state = ARG; } else if (ch == '\\' && i + 1 < len && (args.charAt(i + 1) == '\\' || args.charAt(i + 1) == '"')) { ch = args.charAt(++i); buf.append(ch); } else { buf.append(ch); } break; case IN_SINGLE_QUOTE: if (ch == '\'') { state = ARG; } else { buf.append(ch); } break; default: throw new IllegalStateException(); } } if (buf.length() > 0 || state != DEFAULT) { result.add(buf.toString()); } return (String[]) result.toArray(new String[result.size()]); } /** * Gotten from org.eclipse.debug.core.DebugPlugin */ @SuppressWarnings({ "rawtypes", "unchecked" }) private static String[] parseArgumentsWindows(String args) { // see http://msdn.microsoft.com/en-us/library/a1y7w461.aspx List result = new ArrayList(); final int DEFAULT = 0; final int ARG = 1; final int IN_DOUBLE_QUOTE = 2; int state = DEFAULT; int backslashes = 0; StringBuffer buf = new StringBuffer(); int len = args.length(); for (int i = 0; i < len; i++) { char ch = args.charAt(i); if (ch == '\\') { backslashes++; continue; } else if (backslashes != 0) { if (ch == '"') { for (; backslashes >= 2; backslashes -= 2) { buf.append('\\'); } if (backslashes == 1) { if (state == DEFAULT) { state = ARG; } buf.append('"'); backslashes = 0; continue; } // else fall through to switch } else { // false alarm, treat passed backslashes literally... if (state == DEFAULT) { state = ARG; } for (; backslashes > 0; backslashes--) { buf.append('\\'); } // fall through to switch } } if (Character.isWhitespace(ch)) { if (state == DEFAULT) { // skip continue; } else if (state == ARG) { state = DEFAULT; result.add(buf.toString()); buf.setLength(0); continue; } } switch (state) { case DEFAULT: case ARG: if (ch == '"') { state = IN_DOUBLE_QUOTE; } else { state = ARG; buf.append(ch); } break; case IN_DOUBLE_QUOTE: if (ch == '"') { if (i + 1 < len && args.charAt(i + 1) == '"') { /* Undocumented feature in Windows: * Two consecutive double quotes inside a double-quoted argument are interpreted as * a single double quote. */ buf.append('"'); i++; } else if (buf.length() == 0) { // empty string on Windows platform. Account for bug in constructor of JDK's java.lang.ProcessImpl. result.add("\"\""); //$NON-NLS-1$ state = DEFAULT; } else { state = ARG; } } else { buf.append(ch); } break; default: throw new IllegalStateException(); } } if (buf.length() > 0 || state != DEFAULT) { result.add(buf.toString()); } return (String[]) result.toArray(new String[result.size()]); } public static Map<String, String> getArrayAsMapEnv(String[] mapEnvAsArray) { TreeMap<String, String> map = new TreeMap<>(); int length = mapEnvAsArray.length; for (int i = 0; i < length; i++) { String s = mapEnvAsArray[i]; int iEq = s.indexOf('='); if (iEq != -1) { map.put(s.substring(0, iEq), s.substring(iEq + 1)); } } return map; } public static String[] addOrReplaceEnvVar(String[] mapEnvAsArray, String nameToReplace, String newVal) { int len = mapEnvAsArray.length; nameToReplace += "="; for (int i = 0; i < len; i++) { String string = mapEnvAsArray[i]; if (string.startsWith(nameToReplace)) { mapEnvAsArray[i] = nameToReplace + newVal; return mapEnvAsArray; } } return StringUtils.addString(mapEnvAsArray, nameToReplace + newVal); } }