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/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)", 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;