blob: a740045912cb3dcd6b1aab5b8d774c55cb20dd67 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2018, 2019 Cedric Chabanois and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Cedric Chabanois (cchabanois@gmail.com) - Launching command line exceeds the process creation command limit on *nix - https://bugs.eclipse.org/bugs/show_bug.cgi?id=385738
* IBM Corporation - Launching command line exceeds the process creation command limit on Windows - https://bugs.eclipse.org/bugs/show_bug.cgi?id=327193
*******************************************************************************/
package org.eclipse.jdt.internal.launching;
import static org.eclipse.jdt.internal.launching.LaunchingPlugin.LAUNCH_TEMP_FILE_PREFIX;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.URIUtil;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.IStatusHandler;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.jdt.launching.IVMInstall;
import org.eclipse.jdt.launching.IVMInstall2;
/**
* Shorten the classpath/modulepath if necessary.
*
* Depending on the java version, os and launch configuration, the classpath argument will be replaced by an argument file, a classpath-only jar or
* env variable. The modulepath is replaced by an argument file if necessary.
*
*/
public class ClasspathShortener {
private static final String CLASSPATH_ENV_VAR_PREFIX = "CLASSPATH="; //$NON-NLS-1$
public static final int ARG_MAX_LINUX = 2097152;
public static final int ARG_MAX_WINDOWS = 32767;
public static final int ARG_MAX_MACOS = 262144;
public static final int MAX_ARG_STRLEN_LINUX = 131072;
private final String os;
private final String javaVersion;
private final ILaunch launch;
private final List<String> cmdLine;
private int lastJavaArgumentIndex;
private String[] envp;
private File processTempFilesDir;
private final List<File> processTempFiles = new ArrayList<>();
/**
*
* @param vmInstall
* the vm installation
* @param launch
* the launch
* @param cmdLine
* the command line (java executable + VM arguments + program arguments)
* @param lastJavaArgumentIndex
* the index of the last java argument in cmdLine (next arguments if any are program arguments)
* @param workingDir
* the working dir to use for the launched VM or null
* @param envp
* array of strings, each element of which has environment variable settings in the format name=value, or null if the subprocess should
* inherit the environment of the current process.
*/
public ClasspathShortener(IVMInstall vmInstall, ILaunch launch, String[] cmdLine, int lastJavaArgumentIndex, File workingDir, String[] envp) {
this(Platform.getOS(), getJavaVersion(vmInstall), launch, cmdLine, lastJavaArgumentIndex, workingDir, envp);
}
protected ClasspathShortener(String os, String javaVersion, ILaunch launch, String[] cmdLine, int lastJavaArgumentIndex, File workingDir, String[] envp) {
Assert.isNotNull(os);
Assert.isNotNull(javaVersion);
Assert.isNotNull(launch);
Assert.isNotNull(cmdLine);
this.os = os;
this.javaVersion = javaVersion;
this.launch = launch;
this.cmdLine = new ArrayList<>(Arrays.asList(cmdLine));
this.lastJavaArgumentIndex = lastJavaArgumentIndex;
this.envp = envp == null ? null : Arrays.copyOf(envp, envp.length);
this.processTempFilesDir = workingDir != null ? workingDir : Paths.get(".").toAbsolutePath().normalize().toFile(); //$NON-NLS-1$
}
/**
* The directory to use to create temp files needed when shortening the classpath. By default, the working directory is used
*
* The java.io.tmpdir should not be used on MacOs (does not work for classpath-only jars)
*
* @param processTempFilesDir
*/
public void setProcessTempFilesDir(File processTempFilesDir) {
this.processTempFilesDir = processTempFilesDir;
}
public File getProcessTempFilesDir() {
return processTempFilesDir;
}
/**
* Get the new envp. May have been modified to shorten the classpath
*
* @return environment variables in the format name=value or null
*/
public String[] getEnvp() {
return envp;
}
/**
* Get the new command line. Modified if command line or classpath argument were too long
*
* @return the command line (java executable + VM arguments + program arguments)
*/
public String[] getCmdLine() {
return cmdLine.toArray(new String[cmdLine.size()]);
}
/**
* The files that were created while shortening the path. They can be deleted once the process is terminated
*
* @return created files
*/
public List<File> getProcessTempFiles() {
return new ArrayList<>(processTempFiles);
}
/**
* Shorten the command line if necessary. Each OS has different limits for command line length or command line argument length. And depending on
* the OS, JVM version and launch configuration, we shorten the classpath using an argument file, a classpath-only jar or env variable.
*
* If we need to use a classpath-only jar to shorten the classpath, we ask confirmation from the user because it can have side effects
* (System.getProperty("java.class.path") will return a classpath with only one jar). If
* {@link IJavaLaunchConfigurationConstants#ATTR_USE_CLASSPATH_ONLY_JAR} is set, a classpath-only jar is used (without asking confirmation).
*
* @return true if command line has been shortened or false if it was not necessary or not possible. Use {@link #getCmdLine()} and
* {@link #getEnvp()} to get the new command line/environment.
*/
public boolean shortenCommandLineIfNecessary() {
// '|' used on purpose (not short-circuiting)
return shortenClasspathIfNecessary() | shortenModulePathIfNecessary();
}
private int getClasspathArgumentIndex() {
for (int i = 0; i <= lastJavaArgumentIndex; i++) {
String element = cmdLine.get(i);
if ("-cp".equals(element) || "-classpath".equals(element) || "--class-path".equals(element)) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
return i + 1;
}
}
return -1;
}
private int getModulepathArgumentIndex() {
for (int i = 0; i <= lastJavaArgumentIndex; i++) {
String element = cmdLine.get(i);
if ("-p".equals(element) || "--module-path".equals(element)) { //$NON-NLS-1$ //$NON-NLS-2$
return i + 1;
}
}
return -1;
}
private boolean shortenModulePathIfNecessary() {
int modulePathArgumentIndex = getModulepathArgumentIndex();
if (modulePathArgumentIndex == -1) {
return false;
}
try {
String modulePath = cmdLine.get(modulePathArgumentIndex);
if (getCommandLineLength() <= getMaxCommandLineLength() && modulePath.length() <= getMaxArgLength()) {
return false;
}
if (isArgumentFileSupported()) {
shortenModulePathUsingModulePathArgumentFile(modulePathArgumentIndex);
return true;
}
} catch (CoreException e) {
LaunchingPlugin.log(e.getStatus());
}
return false;
}
private boolean shortenClasspathIfNecessary() {
int classpathArgumentIndex = getClasspathArgumentIndex();
if (classpathArgumentIndex == -1) {
return false;
}
try {
boolean forceUseClasspathOnlyJar = getLaunchConfigurationUseClasspathOnlyJarAttribute();
if (forceUseClasspathOnlyJar) {
shortenClasspathUsingClasspathOnlyJar(classpathArgumentIndex);
return true;
}
String classpath = cmdLine.get(classpathArgumentIndex);
if (getCommandLineLength() <= getMaxCommandLineLength() && classpath.length() <= getMaxArgLength()) {
return false;
}
if (isArgumentFileSupported()) {
shortenClasspathUsingClasspathArgumentFile(classpathArgumentIndex);
return true;
}
if (os.equals(Platform.OS_WIN32)) {
shortenClasspathUsingClasspathEnvVariable(classpathArgumentIndex);
return true;
} else if (handleClasspathTooLongStatus()) {
shortenClasspathUsingClasspathOnlyJar(classpathArgumentIndex);
return true;
}
} catch (CoreException e) {
LaunchingPlugin.log(e.getStatus());
}
return false;
}
protected boolean getLaunchConfigurationUseClasspathOnlyJarAttribute() throws CoreException {
ILaunchConfiguration launchConfiguration = launch.getLaunchConfiguration();
if (launchConfiguration == null) {
return false;
}
return launchConfiguration.getAttribute(IJavaLaunchConfigurationConstants.ATTR_USE_CLASSPATH_ONLY_JAR, false);
}
public static String getJavaVersion(IVMInstall vmInstall) {
if (vmInstall instanceof IVMInstall2) {
IVMInstall2 install = (IVMInstall2) vmInstall;
return install.getJavaVersion();
}
return null;
}
private boolean isArgumentFileSupported() {
return JavaCore.compareJavaVersions(javaVersion, JavaCore.VERSION_9) >= 0;
}
private int getCommandLineLength() {
return cmdLine.stream().map(argument -> argument.length() + 1).reduce((a, b) -> a + b).get();
}
private int getEnvironmentLength() {
if (envp == null) {
return 0;
}
return Arrays.stream(envp).map(element -> element.length() + 1).reduce((a, b) -> a + b).orElse(0);
}
protected int getMaxCommandLineLength() {
// for Posix systems, ARG_MAX is the maximum length of argument to the exec functions including environment data.
// POSIX suggests to subtract 2048 additionally so that the process may safely modify its environment.
// see https://www.in-ulm.de/~mascheck/various/argmax/
switch (os) {
case Platform.OS_LINUX:
// ARG_MAX will be 1/4 of the stack size. 2097152 by default
return ARG_MAX_LINUX - getEnvironmentLength() - 2048;
case Platform.OS_MACOSX:
// on MacOs, ARG_MAX is 262144
return ARG_MAX_MACOS - getEnvironmentLength() - 2048;
case Platform.OS_WIN32:
// On Windows, the maximum length of the command line is 32,768 characters, including the Unicode terminating null character.
// see http://msdn.microsoft.com/en-us/library/windows/desktop/ms682425(v=vs.85).aspx
return ARG_MAX_WINDOWS - 2048;
default:
return Integer.MAX_VALUE;
}
}
protected int getMaxArgLength() {
if (os.equals(Platform.OS_LINUX)) {
// On Linux, MAX_ARG_STRLEN (kernel >= 2.6.23) is the maximum length of a command line argument (or environment variable). Its value
// cannot be changed without recompiling the kernel.
return MAX_ARG_STRLEN_LINUX - 2048;
}
return Integer.MAX_VALUE;
}
private void shortenClasspathUsingClasspathArgumentFile(int classpathArgumentIndex) throws CoreException {
String classpath = cmdLine.get(classpathArgumentIndex);
File file = createClassPathArgumentFile(classpath);
removeCmdLineArgs(classpathArgumentIndex - 1, 2);
addCmdLineArgs(classpathArgumentIndex - 1, '@' + file.getAbsolutePath());
addProcessTempFile(file);
}
private void shortenModulePathUsingModulePathArgumentFile(int modulePathArgumentIndex) throws CoreException {
String modulePath = cmdLine.get(modulePathArgumentIndex);
File file = createModulePathArgumentFile(modulePath);
removeCmdLineArgs(modulePathArgumentIndex - 1, 2);
addCmdLineArgs(modulePathArgumentIndex - 1, '@' + file.getAbsolutePath());
addProcessTempFile(file);
}
private void shortenClasspathUsingClasspathOnlyJar(int classpathArgumentIndex) throws CoreException {
String classpath = cmdLine.get(classpathArgumentIndex);
File classpathOnlyJar = createClasspathOnlyJar(classpath);
removeCmdLineArgs(classpathArgumentIndex, 1);
addCmdLineArgs(classpathArgumentIndex, classpathOnlyJar.getAbsolutePath());
addProcessTempFile(classpathOnlyJar);
}
protected void addProcessTempFile(File file) {
processTempFiles.add(file);
}
protected boolean handleClasspathTooLongStatus() throws CoreException {
IStatus status = new Status(IStatus.ERROR, LaunchingPlugin.getUniqueIdentifier(), IJavaLaunchConfigurationConstants.ERR_CLASSPATH_TOO_LONG, "", null); //$NON-NLS-1$
IStatusHandler handler = DebugPlugin.getDefault().getStatusHandler(status);
if (handler == null) {
return false;
}
Object result = handler.handleStatus(status, launch);
if (!(result instanceof Boolean)) {
return false;
}
return (boolean) result;
}
private File createClasspathOnlyJar(String classpath) throws CoreException {
try {
String timeStamp = getLaunchTimeStamp();
File jarFile = new File(processTempFilesDir, String.format(LAUNCH_TEMP_FILE_PREFIX
+ "%s-classpathOnly-%s.jar", getLaunchConfigurationName(), timeStamp)); //$NON-NLS-1$
URI workingDirUri = processTempFilesDir.toURI();
StringBuilder manifestClasspath = new StringBuilder();
String[] classpathArray = getClasspathAsArray(classpath);
for (int i = 0; i < classpathArray.length; i++) {
if (i != 0) {
manifestClasspath.append(' ');
}
File file = new File(classpathArray[i]);
String relativePath = URIUtil.makeRelative(file.toURI(), workingDirUri).toString();
manifestClasspath.append(relativePath);
}
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); //$NON-NLS-1$
manifest.getMainAttributes().put(Attributes.Name.CLASS_PATH, manifestClasspath.toString());
try (JarOutputStream target = new JarOutputStream(new FileOutputStream(jarFile), manifest)) {
target.hashCode(); // avoid warning that target is unused
}
return jarFile;
} catch (IOException e) {
throw new CoreException(new Status(IStatus.ERROR, LaunchingPlugin.getUniqueIdentifier(), IStatus.ERROR, "Cannot create classpath only jar", e)); // $NON-NLS-1$ //$NON-NLS-1$
}
}
private String[] getClasspathAsArray(String classpath) {
return classpath.split("" + getPathSeparatorChar()); //$NON-NLS-1$
}
protected char getPathSeparatorChar() {
char separator = ':';
if (os.equals(Platform.OS_WIN32)) {
separator = ';';
}
return separator;
}
protected String getLaunchConfigurationName() {
return launch.getLaunchConfiguration().getName();
}
private File createClassPathArgumentFile(String classpath) throws CoreException {
try {
String timeStamp = getLaunchTimeStamp();
File classPathFile = new File(processTempFilesDir, String.format(LAUNCH_TEMP_FILE_PREFIX
+ "%s-classpath-arg-%s.txt", getLaunchConfigurationName(), timeStamp)); //$NON-NLS-1$
byte[] bytes = ("-classpath " + quoteWindowsPath(classpath)).getBytes(StandardCharsets.UTF_8); //$NON-NLS-1$
Files.write(classPathFile.toPath(), bytes);
return classPathFile;
} catch (IOException e) {
throw new CoreException(new Status(IStatus.ERROR, LaunchingPlugin.getUniqueIdentifier(), IStatus.ERROR, "Cannot create classpath argument file", e)); //$NON-NLS-1$
}
}
private File createModulePathArgumentFile(String modulePath) throws CoreException {
try {
String timeStamp = getLaunchTimeStamp();
File modulePathFile = new File(processTempFilesDir, String.format(LAUNCH_TEMP_FILE_PREFIX
+ "%s-module-path-arg-%s.txt", getLaunchConfigurationName(), timeStamp)); //$NON-NLS-1$
byte[] bytes = ("--module-path " + quoteWindowsPath(modulePath)).getBytes(StandardCharsets.UTF_8); //$NON-NLS-1$
Files.write(modulePathFile.toPath(), bytes);
return modulePathFile;
} catch (IOException e) {
throw new CoreException(new Status(IStatus.ERROR, LaunchingPlugin.getUniqueIdentifier(), IStatus.ERROR, "Cannot create module-path argument file", e)); //$NON-NLS-1$
}
}
protected String getLaunchTimeStamp() {
String timeStamp = launch.getAttribute(DebugPlugin.ATTR_LAUNCH_TIMESTAMP);
if (timeStamp == null) {
timeStamp = Long.toString(System.currentTimeMillis());
}
return timeStamp;
}
private String[] getEnvpFromNativeEnvironment() {
Map<String, String> nativeEnvironment = getNativeEnvironment();
String[] envp = new String[nativeEnvironment.size()];
int idx = 0;
for (Entry<String, String> entry : nativeEnvironment.entrySet()) {
String value = entry.getValue();
if (value == null) {
value = ""; //$NON-NLS-1$
}
String key = entry.getKey();
envp[idx] = key + '=' + value;
idx++;
}
return envp;
}
protected Map<String, String> getNativeEnvironment() {
return DebugPlugin.getDefault().getLaunchManager().getNativeEnvironment();
}
private void shortenClasspathUsingClasspathEnvVariable(int classpathArgumentIndex) {
String classpath = cmdLine.get(classpathArgumentIndex);
if (envp == null) {
envp = getEnvpFromNativeEnvironment();
}
String classpathEnvVar = CLASSPATH_ENV_VAR_PREFIX + classpath;
int index = getEnvClasspathIndex(envp);
if (index < 0) {
envp = Arrays.copyOf(envp, envp.length + 1);
envp[envp.length - 1] = classpathEnvVar;
} else {
envp[index] = classpathEnvVar;
}
removeCmdLineArgs(classpathArgumentIndex - 1, 2);
}
private void removeCmdLineArgs(int index, int length) {
for (int i = 0; i < length; i++) {
cmdLine.remove(index);
lastJavaArgumentIndex--;
}
}
private void addCmdLineArgs(int index, String... newArgs) {
cmdLine.addAll(index, Arrays.asList(newArgs));
lastJavaArgumentIndex += newArgs.length;
}
/**
* Returns the index in the given array for the CLASSPATH variable
*
* @param env
* the environment array or <code>null</code>
* @return -1 or the index of the CLASSPATH variable
*/
private int getEnvClasspathIndex(String[] env) {
if (env != null) {
for (int i = 0; i < env.length; i++) {
if (env[i].regionMatches(true, 0, CLASSPATH_ENV_VAR_PREFIX, 0, 10)) {
return i;
}
}
}
return -1;
}
public String quoteWindowsPath(String path) {
if (os.equals(Platform.OS_WIN32)) {
int length = path.length();
StringBuilder newPath = new StringBuilder(length);
boolean insideQuote = false;
for (int i = 0; i < length; i++) {
char c = path.charAt(i);
if (c == ' ' && !insideQuote) {
newPath.append('"');
insideQuote = true;
} else if (insideQuote) {
newPath.append('"');
insideQuote = false;
}
newPath.append(c);
}
return newPath.toString();
}
return path;
}
}