Skip to content

Commit 7af000d

Browse files
committed
Add a new ExecFactory extension point to support alternative processes
Currently we already have a ProcessFactory that allows the creation of alternative IProcess for a launch, but something similar for creation of the native process is currently not possible. This adds a new extension point to supply an ExecFactory to allow controlling the creation of native processes. The use case for this might be to launch inside a terminal session, but also using a chroot environment or containerization would be possible.
1 parent 0d9f518 commit 7af000d

File tree

8 files changed

+302
-22
lines changed

8 files changed

+302
-22
lines changed

debug/org.eclipse.debug.core/META-INF/MANIFEST.MF

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Manifest-Version: 1.0
22
Bundle-ManifestVersion: 2
33
Bundle-Name: %pluginName
44
Bundle-SymbolicName: org.eclipse.debug.core; singleton:=true
5-
Bundle-Version: 3.22.100.qualifier
5+
Bundle-Version: 3.23.0.qualifier
66
Bundle-Activator: org.eclipse.debug.core.DebugPlugin
77
Bundle-Vendor: %providerName
88
Bundle-Localization: plugin

debug/org.eclipse.debug.core/core/org/eclipse/debug/core/DebugPlugin.java

+108-21
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@
2222
import java.nio.charset.StandardCharsets;
2323
import java.text.MessageFormat;
2424
import java.util.ArrayList;
25+
import java.util.Comparator;
2526
import java.util.HashMap;
27+
import java.util.LinkedHashMap;
2628
import java.util.List;
2729
import java.util.Map;
30+
import java.util.Optional;
2831

2932
import javax.xml.parsers.DocumentBuilder;
3033
import javax.xml.parsers.FactoryConfigurationError;
@@ -170,6 +173,14 @@ public class DebugPlugin extends Plugin {
170173
*/
171174
public static final String EXTENSION_POINT_PROCESS_FACTORIES = "processFactories"; //$NON-NLS-1$
172175

176+
/**
177+
* Simple identifier constant (value <code>"execFactories"</code>) for the
178+
* exec factories extension point.
179+
*
180+
* @since 3.23
181+
*/
182+
public static final String EXTENSION_POINT_EXEC_FACTORIES = "execFactories"; //$NON-NLS-1$
183+
173184
/**
174185
* Simple identifier constant (value <code>"logicalStructureTypes"</code>) for the
175186
* logical structure types extension point.
@@ -438,6 +449,11 @@ public class DebugPlugin extends Plugin {
438449
*/
439450
private HashMap<String, IConfigurationElement> fProcessFactories = null;
440451

452+
/**
453+
* List of exec factories.
454+
*/
455+
private List<ExecFactoryFacade> execFactories;
456+
441457
/**
442458
* Service tracker for the workspace service
443459
*/
@@ -981,7 +997,24 @@ public static Process exec(String[] cmdLine, File workingDirectory, String[] env
981997
* @since 3.14
982998
*/
983999
public static Process exec(String[] cmdLine, File workingDirectory, String[] envp, boolean mergeOutput) throws CoreException {
984-
Process p = null;
1000+
List<ExecFactoryFacade> factories = DebugPlugin.getDefault().getExecFactories();
1001+
Optional<File> directory = shortenWindowsPath(workingDirectory);
1002+
Optional<Map<String, String>> envMap = Optional.ofNullable(envp).map(array -> {
1003+
Map<String, String> map = new LinkedHashMap<>();
1004+
for (String e : array) {
1005+
int index = e.indexOf('=');
1006+
if (index != -1) {
1007+
map.put(e.substring(0, index), e.substring(index + 1));
1008+
}
1009+
}
1010+
return Map.copyOf(map);
1011+
});
1012+
for (ExecFactoryFacade holder : factories) {
1013+
Optional<Process> exec = holder.exec(cmdLine.clone(), directory, envMap, mergeOutput);
1014+
if (exec.isPresent()) {
1015+
return exec.get();
1016+
}
1017+
}
9851018
try {
9861019
// starting with and without merged output could be done with the
9871020
// same process builder approach but since the handling of
@@ -990,25 +1023,18 @@ public static Process exec(String[] cmdLine, File workingDirectory, String[] env
9901023
// builder to not break existing caller of this method
9911024
if (mergeOutput) {
9921025
ProcessBuilder pb = new ProcessBuilder(cmdLine);
993-
if (workingDirectory != null) {
994-
pb.directory(shortenWindowsPath(workingDirectory));
995-
}
1026+
directory.ifPresent(pb::directory);
9961027
pb.redirectErrorStream(mergeOutput);
997-
if (envp != null) {
1028+
if (envMap.isPresent()) {
9981029
Map<String, String> env = pb.environment();
9991030
env.clear();
1000-
for (String e : envp) {
1001-
int index = e.indexOf('=');
1002-
if (index != -1) {
1003-
env.put(e.substring(0, index), e.substring(index + 1));
1004-
}
1005-
}
1031+
env.putAll(envMap.get());
10061032
}
1007-
p = pb.start();
1008-
} else if (workingDirectory == null) {
1009-
p = Runtime.getRuntime().exec(cmdLine, envp);
1033+
return pb.start();
1034+
} else if (directory.isEmpty()) {
1035+
return Runtime.getRuntime().exec(cmdLine, envp);
10101036
} else {
1011-
p = Runtime.getRuntime().exec(cmdLine, envp, shortenWindowsPath(workingDirectory));
1037+
return Runtime.getRuntime().exec(cmdLine, envp, directory.get());
10121038
}
10131039
} catch (IOException e) {
10141040
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
10211047
if (handler != null) {
10221048
Object result = handler.handleStatus(status, null);
10231049
if (result instanceof Boolean resultValue && resultValue) {
1024-
p = exec(cmdLine, null);
1050+
return exec(cmdLine, null);
10251051
}
10261052
}
10271053
}
1028-
return p;
1054+
return null;
10291055
}
10301056

10311057
// https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
10321058
private static final int WINDOWS_MAX_PATH = 258;
10331059

1034-
private static File shortenWindowsPath(File path) {
1035-
if (path.getPath().length() > WINDOWS_MAX_PATH && Platform.OS.isWindows()) {
1060+
private static Optional<File> shortenWindowsPath(File path) {
1061+
if (path != null && path.getPath().length() > WINDOWS_MAX_PATH && Platform.OS.isWindows()) {
10361062
// When spawning new processes on Windows, there is no uniform way
10371063
// to use long working directory paths that exceed the default path
10381064
// length limit, like for example using the raw path prefix '\\?\'
@@ -1042,12 +1068,12 @@ private static File shortenWindowsPath(File path) {
10421068
@SuppressWarnings("restriction")
10431069
String shortPath = org.eclipse.core.internal.filesystem.local.Win32Handler.getShortPathName(path.toString());
10441070
if (shortPath != null) {
1045-
return new File(shortPath);
1071+
return Optional.of(new File(shortPath));
10461072
} else {
10471073
log(Status.warning("Working directory of process to create exceeds Window's MAX_PATH limit and shortening the path failed.")); //$NON-NLS-1$
10481074
}
10491075
}
1050-
return path;
1076+
return Optional.ofNullable(path);
10511077
}
10521078

10531079
/**
@@ -1189,6 +1215,39 @@ private void initializeProcessFactories() {
11891215
}
11901216
}
11911217

1218+
private synchronized List<ExecFactoryFacade> getExecFactories() {
1219+
if (execFactories == null) {
1220+
IExtensionPoint extensionPoint = Platform.getExtensionRegistry().getExtensionPoint(DebugPlugin.PI_DEBUG_CORE, EXTENSION_POINT_EXEC_FACTORIES);
1221+
IConfigurationElement[] infos = extensionPoint.getConfigurationElements();
1222+
List<ExecFactoryFacade> list = new ArrayList<>();
1223+
for (IConfigurationElement configurationElement : infos) {
1224+
String clz = configurationElement.getAttribute("class"); //$NON-NLS-1$
1225+
if (clz != null) {
1226+
int priority;
1227+
String attribute = configurationElement.getAttribute("priority"); //$NON-NLS-1$
1228+
if (attribute == null) {
1229+
priority = 0;
1230+
}
1231+
try {
1232+
priority = Integer.parseInt(attribute);
1233+
} catch (NumberFormatException e) {
1234+
log(new Status(IStatus.ERROR, DebugPlugin.PI_DEBUG_CORE, ERROR, MessageFormat.format(DebugCoreMessages.DebugPlugin_invalid_exec_factory, new Object[] {
1235+
configurationElement.getContributor().getName() }), null));
1236+
priority = 0;
1237+
}
1238+
list.add(new ExecFactoryFacade(configurationElement, priority));
1239+
} else {
1240+
String badDefiner = configurationElement.getContributor().getName();
1241+
log(new Status(IStatus.ERROR, DebugPlugin.PI_DEBUG_CORE, ERROR, MessageFormat.format(DebugCoreMessages.DebugPlugin_invalid_exec_factory, new Object[] {
1242+
badDefiner }), null));
1243+
}
1244+
}
1245+
list.sort(Comparator.comparingInt(ExecFactoryFacade::getPriority).reversed());
1246+
execFactories = List.copyOf(list);
1247+
}
1248+
return execFactories;
1249+
}
1250+
11921251
private void invalidStatusHandler(Exception e, String id) {
11931252
log(new Status(IStatus.ERROR, DebugPlugin.PI_DEBUG_CORE, ERROR, MessageFormat.format(DebugCoreMessages.DebugPlugin_5, new Object[] { id }), e));
11941253
}
@@ -1221,6 +1280,34 @@ public boolean equals(Object obj) {
12211280
}
12221281
}
12231282

1283+
private class ExecFactoryFacade implements ExecFactory {
1284+
1285+
private IConfigurationElement element;
1286+
private int priority;
1287+
1288+
ExecFactoryFacade(IConfigurationElement element, int priority) {
1289+
this.element = element;
1290+
this.priority = priority;
1291+
}
1292+
1293+
public int getPriority() {
1294+
return priority;
1295+
}
1296+
1297+
@Override
1298+
public Optional<Process> exec(String[] cmdLine, Optional<File> workingDirectory, Optional<Map<String, String>> environment, boolean mergeOutput) throws CoreException {
1299+
ExecFactory extension;
1300+
try {
1301+
extension = (ExecFactory) element.createExecutableExtension(IConfigurationElementConstants.CLASS);
1302+
} catch (CoreException e) {
1303+
log(e);
1304+
return Optional.empty();
1305+
}
1306+
return extension.exec(cmdLine, workingDirectory, environment, mergeOutput);
1307+
}
1308+
1309+
}
1310+
12241311
/**
12251312
* Executes runnables after event dispatch is complete.
12261313
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Christoph Läubrich and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Christoph Läubrich - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.debug.core;
15+
16+
import java.io.File;
17+
import java.util.Map;
18+
import java.util.Optional;
19+
20+
import org.eclipse.core.runtime.CoreException;
21+
22+
/**
23+
* A {@link ExecFactory} can be used to control how Eclipse forks a new
24+
* {@link Process}.
25+
*
26+
* @since 3.23
27+
*/
28+
public interface ExecFactory {
29+
30+
/**
31+
* Executes the given command with the provided working directory and
32+
* environment
33+
*
34+
* @param cmdLine the commandline to execute
35+
* @param workingDirectory an optional working directory to be used
36+
* otherwise the process factory must use its default
37+
* @param environment the environment to use, if empty the process factory
38+
* must use its defaults
39+
* @param mergeOutput <code>true</code> if standard error and standard out
40+
* should be merged
41+
* @return an {@link Optional} describing the new process created, if an
42+
* empty {@link Optional} is returned it is assumed that this
43+
* factory is not capable of executing the provided command with the
44+
* requested settings
45+
* @throws CoreException if the factory is capable of execution but the
46+
* creation of a process itself has failed
47+
*/
48+
Optional<Process> exec(String[] cmdLine, Optional<File> workingDirectory, Optional<Map<String, String>> environment, boolean mergeOutput) throws CoreException;
49+
50+
}

debug/org.eclipse.debug.core/core/org/eclipse/debug/internal/core/DebugCoreMessages.java

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public class DebugCoreMessages extends NLS {
3131
public static String DebugPlugin_2;
3232
public static String DebugPlugin_3;
3333
public static String DebugPlugin_4;
34+
public static String DebugPlugin_invalid_exec_factory;
35+
public static String DebugPlugin_invalid_exec_factory_priority;
3436
public static String DebugPlugin_5;
3537
public static String DebugPlugin_6;
3638
public static String DebugPlugin_7;

debug/org.eclipse.debug.core/core/org/eclipse/debug/internal/core/DebugCoreMessages.properties

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ DebugPlugin_0=Exception occurred executing command line.
2626
DebugPlugin_2=Internal message logged from Debug Core: {0}
2727
DebugPlugin_3=Error logged from Debug Core:
2828
DebugPlugin_4=Invalid process factory extension contributed by {0}; id: {1}
29+
DebugPlugin_invalid_exec_factory=Invalid exec factory extension contributed by {0}, required class attribute is missing
30+
DebugPlugin_invalid_exec_factory_priority=Invalid exec factory extension contributed by {0}, optional priority attribute is not a number
2931
DebugPlugin_5=Invalid status handler extension: {0}
3032
DebugPlugin_6=An exception occurred in asynchronous runnable.
3133
DebugPlugin_7=An exception occurred while filtering debug events.

debug/org.eclipse.debug.core/plugin.properties

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ launchDelegatesExtensionPointName=Launch Delegates
2626
breakpointImportParticipantsExtensionPoint.name = Breakpoint Import Participant
2727
watchExpressionDelegatesName= Watch Expression Delegates
2828
processFactoriesExtensionPointName=Process Factories
29+
execFactoriesExtensionPointName=Exec Factories
2930
logicalStructureTypesExtensionPointName=Logical Structure Types
3031
logicalStructureProvidersExtensionPointName=Logical Structure Providers
3132
stepFiltersExtensionPointName=Step Filters

debug/org.eclipse.debug.core/plugin.xml

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<extension-point id="statusHandlers" name="%statusHandlerExtensionPointName" schema="schema/statusHandlers.exsd"/>
2929
<extension-point id="watchExpressionDelegates" name="%watchExpressionDelegatesName" schema="schema/watchExpressionDelegates.exsd"/>
3030
<extension-point id="processFactories" name="%processFactoriesExtensionPointName" schema="schema/processFactories.exsd"/>
31+
<extension-point id="execFactories" name="%execFactoriesExtensionPointName" schema="schema/execFactories.exsd"/>
3132
<extension-point id="logicalStructureTypes" name="%logicalStructureTypesExtensionPointName" schema="schema/logicalStructureTypes.exsd"/>
3233
<extension-point id="sourceContainerTypes" name="%sourceContainerTypesName" schema="schema/sourceContainerTypes.exsd"/>
3334
<extension-point id="sourcePathComputers" name="%sourcePathComputersName" schema="schema/sourcePathComputers.exsd"/>

0 commit comments

Comments
 (0)