All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.modules.extexecution.base.ExternalProcessBuilder Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.modules.extexecution.base;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import java.util.regex.Pattern;
import org.netbeans.api.annotations.common.CheckReturnValue;
import org.netbeans.api.annotations.common.NonNull;
import org.openide.util.NbPreferences;
import org.openide.util.Parameters;
import org.openide.util.BaseUtilities;

/**
 * Utility class to make the local external process creation easier.
 * 

* Builder handle command, working directory, PATH variable and HTTP proxy. *

* This class is immutable. *

* Also see {@link ProcessBuilder#getLocal()}. * * @author Petr Hejl * @see #call() */ public final class ExternalProcessBuilder implements Callable { private static final Logger LOGGER = Logger.getLogger(ExternalProcessBuilder.class.getName()); private static final Pattern ESCAPED_PATTERN = Pattern.compile("\".*\""); // NOI18N // FIXME: get rid of those proxy constants as soon as some NB Proxy API is available private static final String USE_PROXY_AUTHENTICATION = "useProxyAuthentication"; // NOI18N private static final String PROXY_AUTHENTICATION_USERNAME = "proxyAuthenticationUsername"; // NOI18N private static final String PROXY_AUTHENTICATION_PASSWORD = "proxyAuthenticationPassword"; // NOI18N private final String executable; private final File workingDirectory; private final boolean redirectErrorStream; private final List arguments = new ArrayList(); private final List paths = new ArrayList(); private final Map envVariables = new HashMap(); /** * Creates the new builder that will create the process by running * given executable. Arguments must not be part of the string. * * @param executable executable to run */ public ExternalProcessBuilder(@NonNull String executable) { this(new BuilderData(executable)); } private ExternalProcessBuilder(BuilderData builder) { this.executable = builder.executable; this.workingDirectory = builder.workingDirectory; this.redirectErrorStream = builder.redirectErrorStream; this.arguments.addAll(builder.arguments); this.paths.addAll(builder.paths); this.envVariables.putAll(builder.envVariables); } /** * Returns a builder with configured working directory. Process * subsequently created by the {@link #call()} method on returned builder * will be executed with this directory as current working dir. *

* The default value is undefined. Note that in such case each process has * working directory corresponding to the value of user.dir * system property. *

* All other properties of the returned builder are inherited from * this. * * @param workingDirectory working directory * @return new builder with configured working directory */ @NonNull @CheckReturnValue public ExternalProcessBuilder workingDirectory(@NonNull File workingDirectory) { Parameters.notNull("workingDirectory", workingDirectory); BuilderData builder = new BuilderData(this); return new ExternalProcessBuilder(builder.workingDirectory(workingDirectory)); } /** * Returns a builder with configured error stream redirection. If configured * value is true process subsequently created by * the {@link #call()} method on returned builder will redirect the error * stream to the standard output stream. *

* The default value is false. *

* All other properties of the returned builder are inherited from * this. * * @param redirectErrorStream if true error stream will be * redirected to standard output * @return new builder with configured error stream redirection */ @NonNull @CheckReturnValue public ExternalProcessBuilder redirectErrorStream(boolean redirectErrorStream) { BuilderData builder = new BuilderData(this); return new ExternalProcessBuilder(builder.redirectErrorStream(redirectErrorStream)); } /** * Returns a builder with additional path in PATH variable. *

* In the group of paths added by this call the last added path will * be the first one in the PATH variable. *

* By default no additional paths are added to PATH variable. *

* All other properties of the returned builder are inherited from * this. * * @param path path to add to PATH variable * @return new builder with additional path in PATH variable */ @NonNull @CheckReturnValue public ExternalProcessBuilder prependPath(@NonNull File path) { Parameters.notNull("path", path); BuilderData builder = new BuilderData(this); return new ExternalProcessBuilder(builder.prependPath(path)); } /** * Returns a builder with additional argument for the command. Arguments * are passed to executable in the same order in which they are added. *

* By default no additional arguments are passed to executable. *

* All other properties of the returned builder are inherited from * this. *

* If there is a need to parse arguments already provided as one big * string the method that can help is * {@link Utilities#parseParameters(java.lang.String)}. * * * @param argument command argument to add * @return new builder with additional argument for the command */ @NonNull @CheckReturnValue public ExternalProcessBuilder addArgument(@NonNull String argument) { Parameters.notNull("argument", argument); BuilderData builder = new BuilderData(this); return new ExternalProcessBuilder(builder.addArgument(argument)); } /** * Returns a builder with additional environment variable for the command. *

* By default no additional environment variables are configured. *

* All other properties of the returned builder are inherited from * this. * * @param name name of the variable * @param value value of the variable * @return new builder with additional environment variable for the command * @see #call() */ @NonNull @CheckReturnValue public ExternalProcessBuilder addEnvironmentVariable(@NonNull String name, @NonNull String value) { Parameters.notNull("name", name); Parameters.notNull("value", value); BuilderData builder = new BuilderData(this); return new ExternalProcessBuilder(builder.addEnvironmentVariable(name, value)); } /** * Creates the new {@link Process} based on the properties configured * in this builder. Created process will try to kill all its children on * call to {@link Process#destroy()}. *

* Process is created by executing the executable with configured arguments. * If custom working directory is specified it is used otherwise value * of system property user.dir is used as working dir. *

* Environment variables are prepared in following way: *

    *
  1. Get table of system environment variables. *
  2. Put all environment variables configured by * {@link #addEnvironmentVariable(java.lang.String, java.lang.String)}. * This rewrites system variables if conflict occurs. *
  3. Get PATH variable and append all paths added * by {@link #prependPath(java.io.File)}. The order of paths in PATH * variable is reversed to order of addition (the last added is the first * one in PATH). Original content of PATH follows * the added content. *
  4. If neither http_proxy nor HTTP_PROXY * environment variable is set then HTTP proxy settings configured in the * IDE are stored as http_proxy environment variable * (the format of the value is http://username:password@host:port). *
* @return the new {@link Process} based on the properties configured * in this builder * @throws IOException if the process could not be created */ @NonNull @Override public Process call() throws IOException { List commandList = new ArrayList(); if (BaseUtilities.isWindows() && !ESCAPED_PATTERN.matcher(executable).matches()) { commandList.add(escapeString(executable)); } else { commandList.add(executable); } List args = buildArguments(); commandList.addAll(args); java.lang.ProcessBuilder pb = new java.lang.ProcessBuilder(commandList.toArray(new String[0])); if (workingDirectory != null) { pb.directory(workingDirectory); } Map pbEnv = pb.environment(); Map env = buildEnvironment(pbEnv); pbEnv.putAll(env); String uuid = UUID.randomUUID().toString(); pbEnv.put(WrapperProcess.KEY_UUID, uuid); adjustProxy(pb); pb.redirectErrorStream(redirectErrorStream); logProcess(Level.FINE, pb); WrapperProcess wp = new WrapperProcess(pb.start(), uuid); return wp; } /** * Logs the given pb using the given level. * * @param pb the ProcessBuilder to log. * @param level the level for logging. */ private void logProcess(final Level level, final java.lang.ProcessBuilder pb) { if (!LOGGER.isLoggable(level)) { return; } File dir = pb.directory(); String basedir = dir == null ? "" : "(basedir: " + dir.getAbsolutePath() + ") "; //NOI18N StringBuilder command = new StringBuilder(); for (Iterator it = pb.command().iterator(); it.hasNext();) { command.append(it.next()); if (it.hasNext()) { command.append(' '); //NOI18N } } LOGGER.log(level, "Running: " + basedir + '"' + command.toString() + '"'); //NOI18N LOGGER.log(level, "Environment: " + pb.environment()); //NOI18N } // package level for unit testing Map buildEnvironment(Map original) { Map ret = new HashMap(original); ret.putAll(envVariables); // Find PATH environment variable - on Windows it can be some other // case and we should use whatever it has. String pathName = getPathName(original); // TODO use StringBuilder String currentPath = ret.get(pathName); if (currentPath == null) { currentPath = ""; } for (File path : paths) { currentPath = path.getAbsolutePath().replace(" ", "\\ ") //NOI18N + File.pathSeparator + currentPath; } if (!"".equals(currentPath.trim())) { ret.put(pathName, currentPath); } return ret; } // package level for unit testing List buildArguments() { if (!BaseUtilities.isWindows()) { return new ArrayList(arguments); } List result = new ArrayList(arguments.size()); for (String arg : arguments) { if (arg != null && !ESCAPED_PATTERN.matcher(arg).matches()) { result.add(escapeString(arg)); } else { result.add(arg); } } return result; } public static void putPath(File path, String pathName, boolean prepend, Map current) { String currentPath = current.get(pathName); if (currentPath == null) { currentPath = ""; } if (prepend) { currentPath = path.getAbsolutePath().replace(" ", "\\ ") //NOI18N + File.pathSeparator + currentPath; } else { currentPath = currentPath + File.pathSeparator + path.getAbsolutePath().replace(" ", "\\ "); //NOI18N } if (!"".equals(currentPath.trim())) { current.put(pathName, currentPath); } } public static String getPathName(Map systemEnv) { // Find PATH environment variable - on Windows it can be some other // case and we should use whatever it has. String pathName = "PATH"; // NOI18N if (BaseUtilities.isWindows()) { pathName = "Path"; // NOI18N for (String keySystem : systemEnv.keySet()) { if ("PATH".equals(keySystem.toUpperCase(Locale.ENGLISH))) { // NOI18N pathName = keySystem; break; } } } return pathName; } private static String escapeString(String s) { if (s.length() == 0) { return "\"\""; // NOI18N } StringBuilder sb = new StringBuilder(); boolean hasSpace = false; final int slen = s.length(); char c; for (int i = 0; i < slen; i++) { c = s.charAt(i); if (Character.isWhitespace(c)) { hasSpace = true; sb.append(c); continue; } sb.append(c); } if (hasSpace) { sb.insert(0, '"'); // NOI18N sb.append('"'); // NOI18N } return sb.toString(); } private void adjustProxy(java.lang.ProcessBuilder pb) { String proxy = getNetBeansHttpProxy(); if (proxy != null) { Map env = pb.environment(); if ((env.get("HTTP_PROXY") == null) && (env.get("http_proxy") == null)) { // NOI18N env.put("HTTP_PROXY", proxy); // NOI18N env.put("http_proxy", proxy); // NOI18N } // PENDING - what if proxy was null so the user has TURNED off // proxies while there is still an environment variable set - should // we honor their environment, or honor their NetBeans proxy // settings (e.g. unset HTTP_PROXY in the environment before // launching plugin? } } /** * FIXME: get rid of the whole method as soon as some NB Proxy API is * available. */ private static String getNetBeansHttpProxy() { // FIXME use ProxySelector String host = System.getProperty("http.proxyHost"); // NOI18N if (host == null) { return null; } String portHttp = System.getProperty("http.proxyPort"); // NOI18N int port; try { port = Integer.parseInt(portHttp); } catch (NumberFormatException e) { port = 8080; } Preferences prefs = NbPreferences.root().node("org/netbeans/core"); // NOI18N boolean useAuth = prefs.getBoolean(USE_PROXY_AUTHENTICATION, false); String auth = ""; if (useAuth) { auth = prefs.get(PROXY_AUTHENTICATION_USERNAME, "") + ":" + prefs.get(PROXY_AUTHENTICATION_PASSWORD, "") + '@'; // NOI18N } // Gem requires "http://" in front of the port name if it's not already there if (host.indexOf(':') == -1) { host = "http://" + auth + host; // NOI18N } return host + ":" + port; // NOI18N } private static class BuilderData { private final String executable; private File workingDirectory; private boolean redirectErrorStream; private List arguments = new ArrayList(); private List paths = new ArrayList(); private Map envVariables = new HashMap(); public BuilderData(String executable) { this.executable = executable; } public BuilderData(ExternalProcessBuilder builder) { this.executable = builder.executable; this.workingDirectory = builder.workingDirectory; this.redirectErrorStream = builder.redirectErrorStream; this.arguments.addAll(builder.arguments); this.paths.addAll(builder.paths); this.envVariables.putAll(builder.envVariables); } public BuilderData workingDirectory(File workingDirectory) { assert workingDirectory != null; this.workingDirectory = workingDirectory; return this; } public BuilderData redirectErrorStream(boolean redirectErrorStream) { this.redirectErrorStream = redirectErrorStream; return this; } public BuilderData prependPath(File path) { assert path != null; paths.add(path); return this; } public BuilderData addArgument(String argument) { assert argument != null; arguments.add(argument); return this; } public BuilderData addEnvironmentVariable(String name, String value) { assert name != null; assert value != null; envVariables.put(name, value); return this; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy