Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,7 @@ public static Process exec(String[] cmdLine, File workingDirectory, String[] env
public static Process exec(String[] cmdLine, File workingDirectory, String[] envp, boolean mergeOutput) throws CoreException {
List<ExecFactoryFacade> factories = DebugPlugin.getDefault().getExecFactories();
Optional<File> directory = shortenWindowsPath(workingDirectory);
String[] shortenedCmdLine = shortenWindowsCommandLine(cmdLine);
Optional<Map<String, String>> envMap = Optional.ofNullable(envp).map(array -> {
Map<String, String> map = new LinkedHashMap<>();
for (String e : array) {
Expand All @@ -1017,7 +1018,7 @@ public static Process exec(String[] cmdLine, File workingDirectory, String[] env
return Map.copyOf(map);
});
for (ExecFactoryFacade holder : factories) {
Optional<Process> exec = holder.exec(cmdLine.clone(), directory, envMap, mergeOutput);
Optional<Process> exec = holder.exec(shortenedCmdLine.clone(), directory, envMap, mergeOutput);
if (exec.isPresent()) {
return exec.get();
}
Expand All @@ -1029,7 +1030,7 @@ public static Process exec(String[] cmdLine, File workingDirectory, String[] env
// ProcessBuilder and Runtime.exec only the new option uses process
// builder to not break existing caller of this method
if (mergeOutput) {
ProcessBuilder pb = new ProcessBuilder(cmdLine);
ProcessBuilder pb = new ProcessBuilder(shortenedCmdLine);
directory.ifPresent(pb::directory);
pb.redirectErrorStream(mergeOutput);
if (envMap.isPresent()) {
Expand All @@ -1039,9 +1040,9 @@ public static Process exec(String[] cmdLine, File workingDirectory, String[] env
}
return pb.start();
} else if (directory.isEmpty()) {
return Runtime.getRuntime().exec(cmdLine, envp);
return Runtime.getRuntime().exec(shortenedCmdLine, envp);
} else {
return Runtime.getRuntime().exec(cmdLine, envp, directory.get());
return Runtime.getRuntime().exec(shortenedCmdLine, envp, directory.get());
}
} catch (IOException e) {
Status status = new Status(IStatus.ERROR, getUniqueIdentifier(), ERROR, DebugCoreMessages.DebugPlugin_0, e);
Expand Down Expand Up @@ -1083,6 +1084,50 @@ private static Optional<File> shortenWindowsPath(File path) {
return Optional.ofNullable(path);
}

/**
* Shortens the command line elements if they exceed Windows MAX_PATH limit.
* This is necessary because Windows process creation APIs have problems with
* long paths, even when launching executables or passing file arguments.
*
* @param cmdLine the command line array
* @return the potentially shortened command line array
*/
private static String[] shortenWindowsCommandLine(String[] cmdLine) {
if (cmdLine == null || cmdLine.length == 0 || !Platform.OS.isWindows()) {
return cmdLine;
}

String[] result = cmdLine.clone();
boolean modified = false;

// Check and shorten each path-like argument in the command line
// The first element is typically the executable path, which is most critical
for (int i = 0; i < result.length; i++) {
String arg = result[i];
if (arg != null && arg.length() > WINDOWS_MAX_PATH) {
// Check if this looks like a file path
File file = new File(arg);
if (file.isAbsolute()) {
@SuppressWarnings("restriction")
String shortPath = org.eclipse.core.internal.filesystem.local.Win32Handler.getShortPathName(arg);
if (shortPath != null) {
result[i] = shortPath;
modified = true;
if (i == 0) {
// Log only for the executable (first argument)
logDebugMessage("Shortened executable path from " + arg.length() + " to " + shortPath.length() + " characters"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
} else if (i == 0) {
// Only warn for the executable path, as that's the most critical
log(Status.warning("Executable path exceeds Window's MAX_PATH limit and shortening the path failed: " + arg)); //$NON-NLS-1$
}
}
}
}

return modified ? result : cmdLine;
}

/**
* Returns whether this plug-in is in the process of
* being shutdown.
Expand Down
2 changes: 1 addition & 1 deletion debug/org.eclipse.debug.tests/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %pluginName
Bundle-SymbolicName: org.eclipse.debug.tests;singleton:=true
Bundle-Version: 3.15.100.qualifier
Bundle-Version: 3.15.200.qualifier
Bundle-Localization: plugin
Require-Bundle: org.eclipse.ui;bundle-version="[3.6.0,4.0.0)",
org.eclipse.core.runtime;bundle-version="[3.29.0,4.0.0)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
Expand All @@ -29,7 +30,9 @@
import java.util.Locale;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.JarOutputStream;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
Expand All @@ -55,6 +58,18 @@
*/
public class LaunchTests extends AbstractLaunchTest {

/**
* Windows MAX_PATH limit for file paths. See
* https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
*/
private static final int WINDOWS_MAX_PATH = 258;

/**
* Target length for long path tests. This should be well above MAX_PATH to
* ensure the tests exercise the long path handling code.
*/
private static final int LONG_PATH_LENGTH_TARGET = 400;

private InvocationHandler handler;
private Runnable readIsTerminatedTask;
private Runnable readIsDisconnectedTask;
Expand Down Expand Up @@ -146,17 +161,38 @@ public void testProcessLaunchWithLongWorkingDirectory() throws CoreException, IO
assumeTrue(Platform.OS.isWindows());

int rootLength = tempFolder.getRoot().toString().length();
String subPathElementsName = "subfolder-with-relativly-long-name";
String[] segments = Collections.nCopies((400 - rootLength) / subPathElementsName.length(), subPathElementsName).toArray(String[]::new);
String subPathElementsName = "subfolder-with-relatively-long-name";
String[] segments = Collections.nCopies((LONG_PATH_LENGTH_TARGET - rootLength) / subPathElementsName.length(), subPathElementsName).toArray(String[]::new);
File workingDirectory = tempFolder.newFolder(segments);
assertTrue(workingDirectory.toString().length() > 300);
assertTrue(workingDirectory.toString().length() > WINDOWS_MAX_PATH);

// Just launch any process in a directory with a path longer than
// Window's MAX_PATH length limit
startProcessAndAssertOutputContains(List.of("java", "--version"), workingDirectory, false, "jdk");
startProcessAndAssertOutputContains(List.of("java", "--version"), workingDirectory, true, "jdk");
}

@Test
public void testProcessLaunchWithLongExecutablePath() throws CoreException, IOException {
assumeTrue(Platform.OS.isWindows());

int rootLength = tempFolder.getRoot().toString().length();
String subPathElementsName = "another-one-with-a-long-path-name-2";
String[] segments = Collections.nCopies((LONG_PATH_LENGTH_TARGET - rootLength) / subPathElementsName.length(), subPathElementsName).toArray(String[]::new);
File workingDirectory = tempFolder.newFolder(segments);
assertTrue(workingDirectory.toString().length() > WINDOWS_MAX_PATH);
File jar = new File(workingDirectory, "dummy.jar");
try (JarOutputStream stream = new JarOutputStream(new FileOutputStream(jar))) {
stream.putNextEntry(new ZipEntry("TEST"));
stream.write(1);
}

// Just launch any process in a directory with a path longer than
// Window's MAX_PATH length limit and an argument that is even longer!
startProcessAndAssertOutputContains(List.of("java", "--version", "-cp", jar.getAbsolutePath()), workingDirectory, false, "jdk");
startProcessAndAssertOutputContains(List.of("java", "--version", "-cp", jar.getAbsolutePath()), workingDirectory, true, "jdk");
}

private static void startProcessAndAssertOutputContains(List<String> cmdLine, File workingDirectory, boolean mergeOutput, String expectedOutput) throws CoreException, IOException {
Process process = DebugPlugin.exec(cmdLine.toArray(String[]::new), workingDirectory, null, mergeOutput);
String output;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public static String getShortPathName(String longPath) {
int newLength = com.sun.jna.platform.win32.Kernel32.INSTANCE.GetShortPathName(longPath, buffer, buffer.length);
if (0 < newLength && newLength < buffer.length) { // zero means error
int offset = longPath.startsWith(WIN32_UNC_RAW_PATH_PREFIX) ? WIN32_UNC_RAW_PATH_PREFIX.length() : WIN32_RAW_PATH_PREFIX.length();
return new String(buffer, offset, newLength);
return new String(buffer, offset, newLength - offset);
}
return null;
}
Expand Down
Loading