Skip to content

Commit 25fcf45

Browse files
committed
Add support for launching Eclipse Process in a system terminal session
Currently processes are launched as a forked process without a terminal session attached to them. Some features of processes require a terminal session (e.g. autocompletion) and currently not work when running from within eclipse (e.g. the gogo-shell). This is an attempt to bring terminal session support to Eclipse to support real terminal application similar to what is supported by IntelliJ.
1 parent 0d9f518 commit 25fcf45

26 files changed

+893
-24
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

+114-23
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
*/
@@ -841,10 +857,13 @@ public static IProcess newProcess(ILaunch launch, Process process, String label)
841857
*/
842858
public static IProcess newProcess(ILaunch launch, Process process, String label, Map<String, String> attributes) {
843859
ILaunchConfiguration config= launch.getLaunchConfiguration();
844-
String processFactoryID= null;
860+
String processFactoryID = null;
861+
// FIXME hack for demo purpose we need an UI for the user to decide if a
862+
// terminal
863+
// is wanted
845864
if (config != null) {
846865
try {
847-
processFactoryID= config.getAttribute(ATTR_PROCESS_FACTORY_ID, (String)null);
866+
processFactoryID = config.getAttribute(ATTR_PROCESS_FACTORY_ID, "org.eclipse.debug.terminal.processFactory.cdt"); //$NON-NLS-1$
848867
} catch (CoreException e) {
849868
}
850869
}
@@ -981,34 +1000,45 @@ public static Process exec(String[] cmdLine, File workingDirectory, String[] env
9811000
* @since 3.14
9821001
*/
9831002
public static Process exec(String[] cmdLine, File workingDirectory, String[] envp, boolean mergeOutput) throws CoreException {
984-
Process p = null;
1003+
List<ExecFactoryFacade> factories = DebugPlugin.getDefault().getExecFactories();
1004+
Optional<File> directory = shortenWindowsPath(workingDirectory);
1005+
Optional<Map<String, String>> envMap = Optional.ofNullable(envp).map(array -> {
1006+
Map<String, String> map = new LinkedHashMap<>();
1007+
for (String e : array) {
1008+
int index = e.indexOf('=');
1009+
if (index != -1) {
1010+
map.put(e.substring(0, index), e.substring(index + 1));
1011+
}
1012+
}
1013+
return Map.copyOf(map);
1014+
});
1015+
for (ExecFactoryFacade holder : factories) {
1016+
Optional<Process> exec = holder.exec(cmdLine.clone(), directory, envMap, mergeOutput);
1017+
if (exec.isPresent()) {
1018+
return exec.get();
1019+
}
1020+
}
9851021
try {
9861022
// starting with and without merged output could be done with the
9871023
// same process builder approach but since the handling of
9881024
// environment variables is slightly different between
9891025
// ProcessBuilder and Runtime.exec only the new option uses process
9901026
// builder to not break existing caller of this method
1027+
9911028
if (mergeOutput) {
9921029
ProcessBuilder pb = new ProcessBuilder(cmdLine);
993-
if (workingDirectory != null) {
994-
pb.directory(shortenWindowsPath(workingDirectory));
995-
}
1030+
directory.ifPresent(pb::directory);
9961031
pb.redirectErrorStream(mergeOutput);
997-
if (envp != null) {
1032+
if (envMap.isPresent()) {
9981033
Map<String, String> env = pb.environment();
9991034
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-
}
1035+
env.putAll(envMap.get());
10061036
}
1007-
p = pb.start();
1008-
} else if (workingDirectory == null) {
1009-
p = Runtime.getRuntime().exec(cmdLine, envp);
1037+
return pb.start();
1038+
} else if (directory.isEmpty()) {
1039+
return Runtime.getRuntime().exec(cmdLine, envp);
10101040
} else {
1011-
p = Runtime.getRuntime().exec(cmdLine, envp, shortenWindowsPath(workingDirectory));
1041+
return Runtime.getRuntime().exec(cmdLine, envp, directory.get());
10121042
}
10131043
} catch (IOException e) {
10141044
Status status = new Status(IStatus.ERROR, getUniqueIdentifier(), ERROR, DebugCoreMessages.DebugPlugin_0, e);
@@ -1021,18 +1051,18 @@ public static Process exec(String[] cmdLine, File workingDirectory, String[] env
10211051
if (handler != null) {
10221052
Object result = handler.handleStatus(status, null);
10231053
if (result instanceof Boolean resultValue && resultValue) {
1024-
p = exec(cmdLine, null);
1054+
return exec(cmdLine, null);
10251055
}
10261056
}
10271057
}
1028-
return p;
1058+
return null;
10291059
}
10301060

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

1034-
private static File shortenWindowsPath(File path) {
1035-
if (path.getPath().length() > WINDOWS_MAX_PATH && Platform.OS.isWindows()) {
1064+
private static Optional<File> shortenWindowsPath(File path) {
1065+
if (path != null && path.getPath().length() > WINDOWS_MAX_PATH && Platform.OS.isWindows()) {
10361066
// When spawning new processes on Windows, there is no uniform way
10371067
// to use long working directory paths that exceed the default path
10381068
// length limit, like for example using the raw path prefix '\\?\'
@@ -1042,12 +1072,12 @@ private static File shortenWindowsPath(File path) {
10421072
@SuppressWarnings("restriction")
10431073
String shortPath = org.eclipse.core.internal.filesystem.local.Win32Handler.getShortPathName(path.toString());
10441074
if (shortPath != null) {
1045-
return new File(shortPath);
1075+
return Optional.of(new File(shortPath));
10461076
} else {
10471077
log(Status.warning("Working directory of process to create exceeds Window's MAX_PATH limit and shortening the path failed.")); //$NON-NLS-1$
10481078
}
10491079
}
1050-
return path;
1080+
return Optional.ofNullable(path);
10511081
}
10521082

10531083
/**
@@ -1189,6 +1219,39 @@ private void initializeProcessFactories() {
11891219
}
11901220
}
11911221

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

1287+
private class ExecFactoryFacade implements ExecFactory {
1288+
1289+
private IConfigurationElement element;
1290+
private int priority;
1291+
1292+
ExecFactoryFacade(IConfigurationElement element, int priority) {
1293+
this.element = element;
1294+
this.priority = priority;
1295+
}
1296+
1297+
public int getPriority() {
1298+
return priority;
1299+
}
1300+
1301+
@Override
1302+
public Optional<Process> exec(String[] cmdLine, Optional<File> workingDirectory, Optional<Map<String, String>> environment, boolean mergeOutput) throws CoreException {
1303+
ExecFactory extension;
1304+
try {
1305+
extension = (ExecFactory) element.createExecutableExtension(IConfigurationElementConstants.CLASS);
1306+
} catch (CoreException e) {
1307+
log(e);
1308+
return Optional.empty();
1309+
}
1310+
return extension.exec(cmdLine, workingDirectory, environment, mergeOutput);
1311+
}
1312+
1313+
}
1314+
12241315
/**
12251316
* Executes runnables after event dispatch is complete.
12261317
*
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)