Bug 570480 - Make termination of descendants configurable

Change-Id: I6f77f33c27028c06c4afb58eae9604eeab317866
Signed-off-by: Hannes Wellmann <wellmann.hannes1@gmx.net>
diff --git a/org.eclipse.debug.core/META-INF/MANIFEST.MF b/org.eclipse.debug.core/META-INF/MANIFEST.MF
index 916303b..08fab6d 100644
--- a/org.eclipse.debug.core/META-INF/MANIFEST.MF
+++ b/org.eclipse.debug.core/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.debug.core; singleton:=true
-Bundle-Version: 3.17.100.qualifier
+Bundle-Version: 3.18.0.qualifier
 Bundle-ClassPath: .
 Bundle-Activator: org.eclipse.debug.core.DebugPlugin
 Bundle-Vendor: %providerName
diff --git a/org.eclipse.debug.core/core/org/eclipse/debug/core/DebugPlugin.java b/org.eclipse.debug.core/core/org/eclipse/debug/core/DebugPlugin.java
index f35c70e..0a9b712 100644
--- a/org.eclipse.debug.core/core/org/eclipse/debug/core/DebugPlugin.java
+++ b/org.eclipse.debug.core/core/org/eclipse/debug/core/DebugPlugin.java
@@ -356,6 +356,21 @@
 	public static final String ATTR_PATH = PI_DEBUG_CORE + ".ATTR_PATH"; //$NON-NLS-1$
 
 	/**
+	 * Launch configuration attribute that designates whether or not the
+	 * descendants of the {@link IProcess} associated to a launch of this
+	 * configuration should be terminated if the main-process is terminated. The
+	 * descendants (also called child- or sub-processes) of a operating system
+	 * process are the processes started by that process.
+	 *
+	 * Value is a string representing a boolean - <code>true</code> or
+	 * <code>false</code>. When unspecified, the default value is considered
+	 * <code>true</code>.
+	 *
+	 * @since 3.18
+	 */
+	public static final String ATTR_TERMINATE_DESCENDANTS = PI_DEBUG_CORE + ".TERMINATE_DESCENDANTS"; //$NON-NLS-1$
+
+	/**
 	 * The singleton debug plug-in instance.
 	 */
 	private static DebugPlugin fgDebugPlugin= null;
diff --git a/org.eclipse.debug.core/core/org/eclipse/debug/core/model/RuntimeProcess.java b/org.eclipse.debug.core/core/org/eclipse/debug/core/model/RuntimeProcess.java
index bef4482..98122a8 100644
--- a/org.eclipse.debug.core/core/org/eclipse/debug/core/model/RuntimeProcess.java
+++ b/org.eclipse.debug.core/core/org/eclipse/debug/core/model/RuntimeProcess.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2020 IBM Corporation and others.
+ * Copyright (c) 2000, 2021 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -25,6 +25,7 @@
 import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
 
+import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.PlatformObject;
 import org.eclipse.core.runtime.Status;
@@ -102,6 +103,11 @@
 	private boolean fCaptureOutput = true;
 
 	/**
+	 * Whether the descendants of this process should be terminated too
+	 */
+	private boolean fTerminateDescendants = true;
+
+	/**
 	 * Constructs a RuntimeProcess on the given system process
 	 * with the given name, adding this process to the given
 	 * launch.
@@ -127,6 +133,15 @@
 		String captureOutput = launch.getAttribute(DebugPlugin.ATTR_CAPTURE_OUTPUT);
 		fCaptureOutput = !("false".equals(captureOutput)); //$NON-NLS-1$
 
+		try {
+			ILaunchConfiguration launchConfiguration = launch.getLaunchConfiguration();
+			if (launchConfiguration != null) {
+				fTerminateDescendants = launchConfiguration.getAttribute(DebugPlugin.ATTR_TERMINATE_DESCENDANTS, true);
+			}
+		} catch (CoreException e) {
+			DebugPlugin.log(e);
+		}
+
 		fStreamsProxy = createStreamsProxy();
 		fMonitor = new ProcessMonitorThread();
 		fMonitor.start();
@@ -209,12 +224,13 @@
 				return;
 			}
 
-			List<ProcessHandle> descendants; // only a snapshot!
-			try {
-				descendants = process.descendants().collect(Collectors.toList());
-			} catch (UnsupportedOperationException e) {
-				// JVM may not support toHandle() -> assume no descendants
-				descendants = Collections.emptyList();
+			List<ProcessHandle> descendants = Collections.emptyList();
+			if (fTerminateDescendants) {
+				try { // List of descendants of process is only a snapshot!
+					descendants = process.descendants().collect(Collectors.toList());
+				} catch (UnsupportedOperationException e) {
+					// JVM may not support toHandle() -> assume no descendants
+				}
 			}
 
 			process.destroy();
diff --git a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/RuntimeProcessTests.java b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/RuntimeProcessTests.java
index 79e4b68..bbcca06 100644
--- a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/RuntimeProcessTests.java
+++ b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/RuntimeProcessTests.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2020 Paul Pazderski and others.
+ * Copyright (c) 2020, 2021 Paul Pazderski and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -22,6 +22,7 @@
 import static org.junit.Assert.assertTrue;
 
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.eclipse.debug.core.DebugEvent;
@@ -32,7 +33,6 @@
 import org.eclipse.debug.tests.AbstractDebugTest;
 import org.eclipse.debug.tests.TestUtil;
 import org.junit.Test;
-import org.junit.function.ThrowingRunnable;
 
 public class RuntimeProcessTests extends AbstractDebugTest {
 
@@ -129,6 +129,31 @@
 	}
 
 	/**
+	 * Test {@link RuntimeProcess} terminating the wrapped process while not
+	 * terminating its descendants.
+	 */
+	@Test
+	public void testTerminateProcessWithoutTerminatingDescendents() throws Exception {
+
+		MockProcess childProcess = new MockProcess(MockProcess.RUN_FOREVER);
+
+		MockProcess mockProcess = new MockProcess(MockProcess.RUN_FOREVER);
+		mockProcess.setHandle(new MockProcessHandle(mockProcess, List.of(childProcess)));
+
+		RuntimeProcess runtimeProcess = mockProcess.toRuntimeProcess("MockProcess", Map.of(DebugPlugin.ATTR_TERMINATE_DESCENDANTS, false));
+
+		assertTrue("RuntimeProcess already terminated.", childProcess.isAlive());
+		assertFalse("RuntimeProcess already terminated.", runtimeProcess.isTerminated());
+
+		runtimeProcess.terminate();
+
+		assertFalse("RuntimeProcess failed to terminate wrapped process.", mockProcess.isAlive());
+		assertTrue("RuntimeProcess terminated child of wrapped process, unlike configured.", childProcess.isAlive());
+
+		TestUtil.waitWhile(p -> !p.isTerminated(), runtimeProcess, 1000, p -> "RuntimePocess not terminated.");
+	}
+
+	/**
 	 * Test {@link RuntimeProcess} terminating the wrapped process which does
 	 * not support {@link Process#toHandle()}.
 	 */
@@ -140,7 +165,7 @@
 		// implementation is called which throws an
 		// UnsupportedOperationException
 		mockProcess.setHandle(null);
-		assertThrows(UnsupportedOperationException.class, () -> mockProcess.toHandle());
+		assertThrows(UnsupportedOperationException.class, mockProcess::toHandle);
 		RuntimeProcess runtimeProcess = mockProcess.toRuntimeProcess();
 		runtimeProcess.terminate(); // must not throw, even toHandle() does
 
@@ -158,9 +183,8 @@
 		mockProcess.setTerminationDelay(6000);
 
 		RuntimeProcess runtimeProcess = mockProcess.toRuntimeProcess();
-		ThrowingRunnable termianteProcess = () -> runtimeProcess.terminate();
 
-		DebugException timeoutException = assertThrows(DebugException.class, termianteProcess);
+		DebugException timeoutException = assertThrows(DebugException.class, runtimeProcess::terminate);
 		assertThat(timeoutException.getMessage(), is(DebugCoreMessages.RuntimeProcess_terminate_failed));
 	}
 
@@ -178,9 +202,8 @@
 		mockProcess.setHandle(new MockProcessHandle(mockProcess, List.of(childProcess)));
 
 		RuntimeProcess runtimeProcess = mockProcess.toRuntimeProcess();
-		ThrowingRunnable termianteProcess = () -> runtimeProcess.terminate();
 
-		DebugException timeoutException = assertThrows(DebugException.class, termianteProcess);
+		DebugException timeoutException = assertThrows(DebugException.class, runtimeProcess::terminate);
 		assertThat(timeoutException.getMessage(), is(DebugCoreMessages.RuntimeProcess_terminate_failed));
 	}
 }
diff --git a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationsMessages.java b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationsMessages.java
index d2ed415..84a0802 100644
--- a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationsMessages.java
+++ b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationsMessages.java
@@ -63,6 +63,7 @@
 	public static String CommonTab_AttributeLabel_AppendToFile;
 	public static String CommonTab_AttributeLabel_LaunchInBackground;
 	public static String CommonTab_AttributeLabel_FavoriteGroups;
+	public static String CommonTab_AttributeLabel_TerminateDescendants;
 
 	public static String CompileErrorProjectPromptStatusHandler_0;
 	public static String CompileErrorProjectPromptStatusHandler_1;
diff --git a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationsMessages.properties b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationsMessages.properties
index a26a4d1..5a9456a 100644
--- a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationsMessages.properties
+++ b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationsMessages.properties
@@ -1,5 +1,5 @@
 ###############################################################################
-#  Copyright (c) 2000, 2019 IBM Corporation and others.
+#  Copyright (c) 2000, 2021 IBM Corporation and others.
 #
 #  This program and the accompanying materials
 #  are made available under the terms of the Eclipse Public License 2.0
@@ -55,6 +55,7 @@
 CommonTab_AttributeLabel_AppendToFile=Append to file
 CommonTab_AttributeLabel_LaunchInBackground=Launch in background
 CommonTab_AttributeLabel_FavoriteGroups=Favorite groups
+CommonTab_AttributeLabel_TerminateDescendants=&Terminate child-processes if terminating the launched process
 
 CompileErrorPromptStatusHandler_0=Errors in Workspace
 CompileErrorPromptStatusHandler_1=Errors exist in a required project. Continue launch?
diff --git a/org.eclipse.debug.ui/ui/org/eclipse/debug/ui/CommonTab.java b/org.eclipse.debug.ui/ui/org/eclipse/debug/ui/CommonTab.java
index 5e34555..405867a 100644
--- a/org.eclipse.debug.ui/ui/org/eclipse/debug/ui/CommonTab.java
+++ b/org.eclipse.debug.ui/ui/org/eclipse/debug/ui/CommonTab.java
@@ -124,6 +124,7 @@
 	private Text fSharedLocationText;
 	private Button fSharedLocationButton;
 	private Button fLaunchInBackgroundButton;
+	private Button fTerminateDescendantsButton;
 	private Button fDefaultEncodingButton;
 	private Button fAltEncodingButton;
 	private Combo fEncodingCombo;
@@ -173,6 +174,7 @@
 		createEncodingComponent(comp);
 		createOutputCaptureComponent(comp);
 		createLaunchInBackgroundComponent(comp);
+		createTerminateDescendantsButtonComponent(comp);
 	}
 
 	/**
@@ -501,6 +503,22 @@
 	}
 
 	/**
+	 * Creates the controls needed to edit the terminate descendants attribute of an
+	 * external tool
+	 *
+	 * @param parent the composite to create the controls in
+	 */
+	private void createTerminateDescendantsButtonComponent(Composite parent) {
+		fTerminateDescendantsButton = createCheckButton(parent,
+				LaunchConfigurationsMessages.CommonTab_AttributeLabel_TerminateDescendants);
+		GridData data = new GridData(GridData.HORIZONTAL_ALIGN_FILL);
+		data.horizontalSpan = 2;
+		fTerminateDescendantsButton.setLayoutData(data);
+		fTerminateDescendantsButton.setFont(parent.getFont());
+		fTerminateDescendantsButton.addSelectionListener(widgetSelectedAdapter(e -> updateLaunchConfigurationDialog()));
+	}
+
+	/**
 	 * handles the shared radio button being selected
 	 */
 	private void handleSharedRadioButtonSelected() {
@@ -621,6 +639,9 @@
 		updateLaunchInBackground(configuration);
 		updateEncoding(configuration);
 		updateConsoleOutput(configuration);
+
+		boolean terminateDescendants = getAttribute(configuration, DebugPlugin.ATTR_TERMINATE_DESCENDANTS, true);
+		fTerminateDescendantsButton.setSelection(terminateDescendants);
 	}
 
 	/**
@@ -934,7 +955,13 @@
 	public void performApply(ILaunchConfigurationWorkingCopy configuration) {
 		updateConfigFromLocalShared(configuration);
 		updateConfigFromFavorites(configuration);
-		setAttribute(IDebugUIConstants.ATTR_LAUNCH_IN_BACKGROUND, configuration, fLaunchInBackgroundButton.getSelection(), true);
+
+		boolean launchInBackground = fLaunchInBackgroundButton.getSelection();
+		setAttribute(IDebugUIConstants.ATTR_LAUNCH_IN_BACKGROUND, configuration, launchInBackground, true);
+
+		boolean terminateDescendants = fTerminateDescendantsButton.getSelection();
+		setAttribute(DebugPlugin.ATTR_TERMINATE_DESCENDANTS, configuration, terminateDescendants, true);
+
 		String encoding = null;
 		if(fAltEncodingButton.getSelection()) {
 			encoding = fEncodingCombo.getText().trim();
@@ -1022,6 +1049,7 @@
 		getAttributesLabelsForPrototype().put(IDebugUIConstants.ATTR_APPEND_TO_FILE, LaunchConfigurationsMessages.CommonTab_AttributeLabel_AppendToFile);
 		getAttributesLabelsForPrototype().put(IDebugUIConstants.ATTR_LAUNCH_IN_BACKGROUND, LaunchConfigurationsMessages.CommonTab_AttributeLabel_LaunchInBackground);
 		getAttributesLabelsForPrototype().put(IDebugUIConstants.ATTR_FAVORITE_GROUPS, LaunchConfigurationsMessages.CommonTab_AttributeLabel_FavoriteGroups);
+		getAttributesLabelsForPrototype().put(DebugPlugin.ATTR_TERMINATE_DESCENDANTS, LaunchConfigurationsMessages.CommonTab_AttributeLabel_TerminateDescendants);
 	}
 
 	/**