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
/******************************************************************************* * 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.
/** * 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<>();
Params:
  • vmInstall – the vm installation
  • launch – the launch
  • cmdLine – the command line (java executable + VM arguments + program arguments)
  • lastJavaArgumentIndex – the index of the last java argument in cmdLine (next arguments if any are program arguments)
  • workingDir – the working dir to use for the launched VM or null
  • 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.
/** * * @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)
Params:
  • processTempFilesDir –
/** * 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
Returns:environment variables in the format name=value or null
/** * 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
Returns:the command line (java executable + VM arguments + program arguments)
/** * 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
Returns:created files
/** * 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 IJavaLaunchConfigurationConstants.ATTR_USE_CLASSPATH_ONLY_JAR is set, a classpath-only jar is used (without asking confirmation).
Returns:true if command line has been shortened or false if it was not necessary or not possible. Use getCmdLine() and getEnvp() to get the new command line/environment.
/** * 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)) { } 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 + quoteWindowsPath(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
Params:
  • env – the environment array or null
Returns:-1 or the index of the CLASSPATH variable
/** * 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)) { return "\"" + path + "\""; //$NON-NLS-1$ //$NON-NLS-2$ } return path; } }