diff --git a/debug/org.eclipse.debug.core/META-INF/MANIFEST.MF b/debug/org.eclipse.debug.core/META-INF/MANIFEST.MF index cdd768c6fb7..ccc9689c272 100644 --- a/debug/org.eclipse.debug.core/META-INF/MANIFEST.MF +++ b/debug/org.eclipse.debug.core/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.debug.core; singleton:=true -Bundle-Version: 3.22.100.qualifier +Bundle-Version: 3.23.0.qualifier Bundle-Activator: org.eclipse.debug.core.DebugPlugin Bundle-Vendor: %providerName Bundle-Localization: plugin 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 c80b4f9f131..97d1175de4a 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 @@ -22,9 +22,12 @@ import java.nio.charset.StandardCharsets; import java.text.MessageFormat; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.FactoryConfigurationError; @@ -170,6 +173,14 @@ public class DebugPlugin extends Plugin { */ public static final String EXTENSION_POINT_PROCESS_FACTORIES = "processFactories"; //$NON-NLS-1$ + /** + * Simple identifier constant (value "execFactories") for the + * exec factories extension point. + * + * @since 3.23 + */ + public static final String EXTENSION_POINT_EXEC_FACTORIES = "execFactories"; //$NON-NLS-1$ + /** * Simple identifier constant (value "logicalStructureTypes") for the * logical structure types extension point. @@ -438,6 +449,11 @@ public class DebugPlugin extends Plugin { */ private HashMap fProcessFactories = null; + /** + * List of exec factories. + */ + private List execFactories; + /** * Service tracker for the workspace service */ @@ -981,7 +997,24 @@ public static Process exec(String[] cmdLine, File workingDirectory, String[] env * @since 3.14 */ public static Process exec(String[] cmdLine, File workingDirectory, String[] envp, boolean mergeOutput) throws CoreException { - Process p = null; + List factories = DebugPlugin.getDefault().getExecFactories(); + Optional directory = shortenWindowsPath(workingDirectory); + Optional> envMap = Optional.ofNullable(envp).map(array -> { + Map map = new LinkedHashMap<>(); + for (String e : array) { + int index = e.indexOf('='); + if (index != -1) { + map.put(e.substring(0, index), e.substring(index + 1)); + } + } + return Map.copyOf(map); + }); + for (ExecFactoryFacade holder : factories) { + Optional exec = holder.exec(cmdLine.clone(), directory, envMap, mergeOutput); + if (exec.isPresent()) { + return exec.get(); + } + } try { // starting with and without merged output could be done with the // same process builder approach but since the handling of @@ -990,25 +1023,18 @@ public static Process exec(String[] cmdLine, File workingDirectory, String[] env // builder to not break existing caller of this method if (mergeOutput) { ProcessBuilder pb = new ProcessBuilder(cmdLine); - if (workingDirectory != null) { - pb.directory(shortenWindowsPath(workingDirectory)); - } + directory.ifPresent(pb::directory); pb.redirectErrorStream(mergeOutput); - if (envp != null) { + if (envMap.isPresent()) { Map env = pb.environment(); env.clear(); - for (String e : envp) { - int index = e.indexOf('='); - if (index != -1) { - env.put(e.substring(0, index), e.substring(index + 1)); - } - } + env.putAll(envMap.get()); } - p = pb.start(); - } else if (workingDirectory == null) { - p = Runtime.getRuntime().exec(cmdLine, envp); + return pb.start(); + } else if (directory.isEmpty()) { + return Runtime.getRuntime().exec(cmdLine, envp); } else { - p = Runtime.getRuntime().exec(cmdLine, envp, shortenWindowsPath(workingDirectory)); + return Runtime.getRuntime().exec(cmdLine, envp, directory.get()); } } catch (IOException e) { Status status = new Status(IStatus.ERROR, getUniqueIdentifier(), ERROR, DebugCoreMessages.DebugPlugin_0, e); @@ -1021,18 +1047,18 @@ public static Process exec(String[] cmdLine, File workingDirectory, String[] env if (handler != null) { Object result = handler.handleStatus(status, null); if (result instanceof Boolean resultValue && resultValue) { - p = exec(cmdLine, null); + return exec(cmdLine, null); } } } - return p; + return null; } // https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation private static final int WINDOWS_MAX_PATH = 258; - private static File shortenWindowsPath(File path) { - if (path.getPath().length() > WINDOWS_MAX_PATH && Platform.OS.isWindows()) { + private static Optional shortenWindowsPath(File path) { + if (path != null && path.getPath().length() > WINDOWS_MAX_PATH && Platform.OS.isWindows()) { // When spawning new processes on Windows, there is no uniform way // to use long working directory paths that exceed the default path // length limit, like for example using the raw path prefix '\\?\' @@ -1042,12 +1068,12 @@ private static File shortenWindowsPath(File path) { @SuppressWarnings("restriction") String shortPath = org.eclipse.core.internal.filesystem.local.Win32Handler.getShortPathName(path.toString()); if (shortPath != null) { - return new File(shortPath); + return Optional.of(new File(shortPath)); } else { log(Status.warning("Working directory of process to create exceeds Window's MAX_PATH limit and shortening the path failed.")); //$NON-NLS-1$ } } - return path; + return Optional.ofNullable(path); } /** @@ -1189,6 +1215,39 @@ private void initializeProcessFactories() { } } + private synchronized List getExecFactories() { + if (execFactories == null) { + IExtensionPoint extensionPoint = Platform.getExtensionRegistry().getExtensionPoint(DebugPlugin.PI_DEBUG_CORE, EXTENSION_POINT_EXEC_FACTORIES); + IConfigurationElement[] infos = extensionPoint.getConfigurationElements(); + List list = new ArrayList<>(); + for (IConfigurationElement configurationElement : infos) { + String clz = configurationElement.getAttribute("class"); //$NON-NLS-1$ + if (clz != null) { + int priority; + String attribute = configurationElement.getAttribute("priority"); //$NON-NLS-1$ + if (attribute == null) { + priority = 0; + } + try { + priority = Integer.parseInt(attribute); + } catch (NumberFormatException e) { + log(new Status(IStatus.ERROR, DebugPlugin.PI_DEBUG_CORE, ERROR, MessageFormat.format(DebugCoreMessages.DebugPlugin_invalid_exec_factory, new Object[] { + configurationElement.getContributor().getName() }), null)); + priority = 0; + } + list.add(new ExecFactoryFacade(configurationElement, priority)); + } else { + String badDefiner = configurationElement.getContributor().getName(); + log(new Status(IStatus.ERROR, DebugPlugin.PI_DEBUG_CORE, ERROR, MessageFormat.format(DebugCoreMessages.DebugPlugin_invalid_exec_factory, new Object[] { + badDefiner }), null)); + } + } + list.sort(Comparator.comparingInt(ExecFactoryFacade::getPriority).reversed()); + execFactories = List.copyOf(list); + } + return execFactories; + } + private void invalidStatusHandler(Exception e, String id) { log(new Status(IStatus.ERROR, DebugPlugin.PI_DEBUG_CORE, ERROR, MessageFormat.format(DebugCoreMessages.DebugPlugin_5, new Object[] { id }), e)); } @@ -1221,6 +1280,34 @@ public boolean equals(Object obj) { } } + private class ExecFactoryFacade implements ExecFactory { + + private IConfigurationElement element; + private int priority; + + ExecFactoryFacade(IConfigurationElement element, int priority) { + this.element = element; + this.priority = priority; + } + + public int getPriority() { + return priority; + } + + @Override + public Optional exec(String[] cmdLine, Optional workingDirectory, Optional> environment, boolean mergeOutput) throws CoreException { + ExecFactory extension; + try { + extension = (ExecFactory) element.createExecutableExtension(IConfigurationElementConstants.CLASS); + } catch (CoreException e) { + log(e); + return Optional.empty(); + } + return extension.exec(cmdLine, workingDirectory, environment, mergeOutput); + } + + } + /** * Executes runnables after event dispatch is complete. * diff --git a/debug/org.eclipse.debug.core/core/org/eclipse/debug/core/ExecFactory.java b/debug/org.eclipse.debug.core/core/org/eclipse/debug/core/ExecFactory.java new file mode 100644 index 00000000000..56d2efdcb4d --- /dev/null +++ b/debug/org.eclipse.debug.core/core/org/eclipse/debug/core/ExecFactory.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2025 Christoph Läubrich 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: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.debug.core; + +import java.io.File; +import java.util.Map; +import java.util.Optional; + +import org.eclipse.core.runtime.CoreException; + +/** + * A {@link ExecFactory} can be used to control how Eclipse forks a new + * {@link Process}. + * + * @since 3.23 + */ +public interface ExecFactory { + + /** + * Executes the given command with the provided working directory and + * environment + * + * @param cmdLine the commandline to execute + * @param workingDirectory an optional working directory to be used + * otherwise the process factory must use its default + * @param environment the environment to use, if empty the process factory + * must use its defaults + * @param mergeOutput true if standard error and standard out + * should be merged + * @return an {@link Optional} describing the new process created, if an + * empty {@link Optional} is returned it is assumed that this + * factory is not capable of executing the provided command with the + * requested settings + * @throws CoreException if the factory is capable of execution but the + * creation of a process itself has failed + */ + Optional exec(String[] cmdLine, Optional workingDirectory, Optional> environment, boolean mergeOutput) throws CoreException; + +} diff --git a/debug/org.eclipse.debug.core/core/org/eclipse/debug/internal/core/DebugCoreMessages.java b/debug/org.eclipse.debug.core/core/org/eclipse/debug/internal/core/DebugCoreMessages.java index cd2547a7062..988cb9db7f6 100644 --- a/debug/org.eclipse.debug.core/core/org/eclipse/debug/internal/core/DebugCoreMessages.java +++ b/debug/org.eclipse.debug.core/core/org/eclipse/debug/internal/core/DebugCoreMessages.java @@ -31,6 +31,8 @@ public class DebugCoreMessages extends NLS { public static String DebugPlugin_2; public static String DebugPlugin_3; public static String DebugPlugin_4; + public static String DebugPlugin_invalid_exec_factory; + public static String DebugPlugin_invalid_exec_factory_priority; public static String DebugPlugin_5; public static String DebugPlugin_6; public static String DebugPlugin_7; diff --git a/debug/org.eclipse.debug.core/core/org/eclipse/debug/internal/core/DebugCoreMessages.properties b/debug/org.eclipse.debug.core/core/org/eclipse/debug/internal/core/DebugCoreMessages.properties index a5db689a246..0b9e50a3319 100644 --- a/debug/org.eclipse.debug.core/core/org/eclipse/debug/internal/core/DebugCoreMessages.properties +++ b/debug/org.eclipse.debug.core/core/org/eclipse/debug/internal/core/DebugCoreMessages.properties @@ -26,6 +26,8 @@ DebugPlugin_0=Exception occurred executing command line. DebugPlugin_2=Internal message logged from Debug Core: {0} DebugPlugin_3=Error logged from Debug Core: DebugPlugin_4=Invalid process factory extension contributed by {0}; id: {1} +DebugPlugin_invalid_exec_factory=Invalid exec factory extension contributed by {0}, required class attribute is missing +DebugPlugin_invalid_exec_factory_priority=Invalid exec factory extension contributed by {0}, optional priority attribute is not a number DebugPlugin_5=Invalid status handler extension: {0} DebugPlugin_6=An exception occurred in asynchronous runnable. DebugPlugin_7=An exception occurred while filtering debug events. diff --git a/debug/org.eclipse.debug.core/plugin.properties b/debug/org.eclipse.debug.core/plugin.properties index be348fa6bbe..a2f4e7510ad 100644 --- a/debug/org.eclipse.debug.core/plugin.properties +++ b/debug/org.eclipse.debug.core/plugin.properties @@ -26,6 +26,7 @@ launchDelegatesExtensionPointName=Launch Delegates breakpointImportParticipantsExtensionPoint.name = Breakpoint Import Participant watchExpressionDelegatesName= Watch Expression Delegates processFactoriesExtensionPointName=Process Factories +execFactoriesExtensionPointName=Exec Factories logicalStructureTypesExtensionPointName=Logical Structure Types logicalStructureProvidersExtensionPointName=Logical Structure Providers stepFiltersExtensionPointName=Step Filters diff --git a/debug/org.eclipse.debug.core/plugin.xml b/debug/org.eclipse.debug.core/plugin.xml index 0c89b1b50f5..fe3c4ff6108 100644 --- a/debug/org.eclipse.debug.core/plugin.xml +++ b/debug/org.eclipse.debug.core/plugin.xml @@ -28,6 +28,7 @@ + diff --git a/debug/org.eclipse.debug.core/schema/execFactories.exsd b/debug/org.eclipse.debug.core/schema/execFactories.exsd new file mode 100644 index 00000000000..b220a3ed184 --- /dev/null +++ b/debug/org.eclipse.debug.core/schema/execFactories.exsd @@ -0,0 +1,137 @@ + + + + + + + + + This extension point provides a mechanism for specifying an exec factory to be used with a launch configuration to create the appropriate instance of <b>Process</b>. + used to create the <b>IProcess</b> + + + + + + + + + + + + + + + + + a fully qualified identifier of the target extension point + + + + + + + an optional identifier of the extension instance + + + + + + + an optional name of the extension instance + + + + + + + + + + + + + + + + + specifies a unique identifier for this process factory. + + + + + + + specifies the fully qualified name of the Java class that implements <code>ExecFactory</code>. + + + + + + + + + + Describe the priority of this factory, higher values mean that is factory is asked before others with a lower value to start the process. + + + + + + + + + + + + 3.23 + + + + + + + + + The following is an example of an exec factory extension point: + +<p> +<pre> + <extension point="org.eclipse.debug.core.execFactories"> + <processFactory + id="com.example.ExampleIdentifier" + class="com.example.ExampleExecFactory"> + </processFactory> + </extension> +</pre> +</p> + + + + + + + + + Value of the attribute <b>class</b> must be a fully qualified name of a Java class that implements the interface <b>org.eclipse.debug.core.ExecFactory</b>. + + + + + + + + + + Copyright (c) 2025 Christoph Läubrich and others.<br> + +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 +<a href="https://www.eclipse.org/legal/epl-2.0">https://www.eclipse.org/legal/epl-v20.html</a> + +SPDX-License-Identifier: EPL-2.0 + + + +