Skip to content
Open
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 @@ -55,6 +55,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,8 +158,8 @@ 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);

Expand All @@ -157,6 +169,33 @@ public void testProcessLaunchWithLongWorkingDirectory() throws CoreException, IO
startProcessAndAssertOutputContains(List.of("java", "--version"), workingDirectory, true, "jdk");
}

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

// Create a directory with a very long path
int rootLength = tempFolder.getRoot().toString().length();
String subPathElementsName = "subfolder-with-relatively-long-name";
String[] segments = Collections.nCopies((LONG_PATH_LENGTH_TARGET - rootLength) / subPathElementsName.length(), subPathElementsName).toArray(String[]::new);
File longPathDir = tempFolder.newFolder(segments);
assertTrue(longPathDir.toString().length() > 300);

// Copy a system executable (java) to the long path
String javaHome = System.getProperty("java.home");
File javaExe = new File(javaHome, "bin/java.exe");
File longPathExe = new File(longPathDir, "java.exe");

// Copy the executable
java.nio.file.Files.copy(javaExe.toPath(), longPathExe.toPath());
assertTrue(longPathExe.exists());
String longExePath = longPathExe.getAbsolutePath();
assertTrue("Executable path should exceed MAX_PATH", longExePath.length() > WINDOWS_MAX_PATH);

// Launch the executable from the long path
startProcessAndAssertOutputContains(List.of(longExePath, "--version"), tempFolder.getRoot(), false, "jdk");
startProcessAndAssertOutputContains(List.of(longExePath, "--version"), tempFolder.getRoot(), 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
Loading