From deb7b9e5b3904c8445847f6d6a2c2fea6228ec0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 09:40:49 +0000 Subject: [PATCH 1/2] Handle long executable paths in Windows process launching Add shortenWindowsCommandLine() to shorten all command line arguments that exceed Windows MAX_PATH limit, not just the working directory. This fixes issues where Eclipse is unable to launch processes with long executable paths on Windows. The fix uses Windows GetShortPathName API via Win32Handler to convert long paths to their 8.3 short form before passing them to ProcessBuilder or Runtime.exec(). Co-authored-by: laeubi <1331477+laeubi@users.noreply.github.com> --- .../org/eclipse/debug/core/DebugPlugin.java | 53 +++++++++++++++++-- .../debug/tests/launching/LaunchTests.java | 43 ++++++++++++++- 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/debug/org.eclipse.debug.core/core/org/eclipse/debug/core/DebugPlugin.java b/debug/org.eclipse.debug.core/core/org/eclipse/debug/core/DebugPlugin.java index 497aba4a8d9..06ffe1c8dd0 100644 --- a/debug/org.eclipse.debug.core/core/org/eclipse/debug/core/DebugPlugin.java +++ b/debug/org.eclipse.debug.core/core/org/eclipse/debug/core/DebugPlugin.java @@ -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 factories = DebugPlugin.getDefault().getExecFactories(); Optional directory = shortenWindowsPath(workingDirectory); + String[] shortenedCmdLine = shortenWindowsCommandLine(cmdLine); Optional> envMap = Optional.ofNullable(envp).map(array -> { Map map = new LinkedHashMap<>(); for (String e : array) { @@ -1017,7 +1018,7 @@ public static Process exec(String[] cmdLine, File workingDirectory, String[] env return Map.copyOf(map); }); for (ExecFactoryFacade holder : factories) { - Optional exec = holder.exec(cmdLine.clone(), directory, envMap, mergeOutput); + Optional exec = holder.exec(shortenedCmdLine.clone(), directory, envMap, mergeOutput); if (exec.isPresent()) { return exec.get(); } @@ -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()) { @@ -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); @@ -1083,6 +1084,50 @@ private static Optional 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. diff --git a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/launching/LaunchTests.java b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/launching/LaunchTests.java index 07cd3af313a..ec28429bad7 100644 --- a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/launching/LaunchTests.java +++ b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/launching/LaunchTests.java @@ -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; @@ -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); @@ -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 cmdLine, File workingDirectory, boolean mergeOutput, String expectedOutput) throws CoreException, IOException { Process process = DebugPlugin.exec(cmdLine.toArray(String[]::new), workingDirectory, null, mergeOutput); String output; From a62f59cd2c1d74bd4368bd3127db8c4ea76eecb2 Mon Sep 17 00:00:00 2001 From: Eclipse Platform Bot Date: Thu, 30 Oct 2025 10:18:16 +0000 Subject: [PATCH 2/2] Version bump(s) for 4.38 stream --- debug/org.eclipse.debug.tests/META-INF/MANIFEST.MF | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debug/org.eclipse.debug.tests/META-INF/MANIFEST.MF b/debug/org.eclipse.debug.tests/META-INF/MANIFEST.MF index 2e9d0678fb5..9ec58f99085 100644 --- a/debug/org.eclipse.debug.tests/META-INF/MANIFEST.MF +++ b/debug/org.eclipse.debug.tests/META-INF/MANIFEST.MF @@ -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)",