Bug 550195 - Reduce wait time in runtime process termination

Java 8 added a waitFor method with timeout for the Process class. Using
this instead of the plain Thread.sleep can reduce waiting time in some
situations from 500 ms down to 10 ms.

For the case that Process is not a native process and has no optimized
waitFor implementation the situation still improves since the terminated
check is performed every 100 ms instead every 500 ms.

Change-Id: I0fc54981b53615fbae14d46a6c82a9b85d64b035
Signed-off-by: Paul Pazderski <paul-eclipse@ppazderski.de>
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 8ee0a09..1bfb08a 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
@@ -20,6 +20,7 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.concurrent.TimeUnit;
 
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.PlatformObject;
@@ -219,7 +220,11 @@
 				} catch (IllegalThreadStateException ie) {
 				}
 				try {
-					Thread.sleep(TIME_TO_WAIT_FOR_THREAD_DEATH);
+					if (process != null) {
+						process.waitFor(TIME_TO_WAIT_FOR_THREAD_DEATH, TimeUnit.MILLISECONDS);
+					} else {
+						Thread.sleep(TIME_TO_WAIT_FOR_THREAD_DEATH);
+					}
 				} catch (InterruptedException e) {
 				}
 				attempts++;
diff --git a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/AutomatedSuite.java b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/AutomatedSuite.java
index 9e3efa6..32e1ed3 100644
--- a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/AutomatedSuite.java
+++ b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/AutomatedSuite.java
@@ -23,6 +23,7 @@
 import org.eclipse.debug.tests.console.IOConsoleTests;
 import org.eclipse.debug.tests.console.ProcessConsoleManagerTests;
 import org.eclipse.debug.tests.console.ProcessConsoleTests;
+import org.eclipse.debug.tests.console.RuntimeProcessTests;
 import org.eclipse.debug.tests.console.StreamsProxyTests;
 import org.eclipse.debug.tests.console.TextConsoleViewerTest;
 import org.eclipse.debug.tests.launching.AcceleratorSubstitutionTests;
@@ -123,6 +124,7 @@
 		addTest(new TestSuite(ProcessConsoleTests.class));
 		addTest(new TestSuite(StreamsProxyTests.class));
 		addTest(new TestSuite(TextConsoleViewerTest.class));
+		addTest(new TestSuite(RuntimeProcessTests.class));
 
 		// Launch Groups
 		addTest(new TestSuite(LaunchGroupTests.class));
diff --git a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/MockProcess.java b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/MockProcess.java
index 7bd305e..aa84545 100644
--- a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/MockProcess.java
+++ b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/MockProcess.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2019 Paul Pazderski and others.
+ * Copyright (c) 2019, 2020 Paul Pazderski and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -17,6 +17,7 @@
 import java.io.ByteArrayOutputStream;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.eclipse.debug.core.DebugPlugin;
@@ -57,6 +58,8 @@
 	 * </p>
 	 */
 	private long endTime;
+	/** The simulated exit code. */
+	private int exitCode = 0;
 
 	/**
 	 * Create new silent mockup process which runs for a given amount of time.
@@ -171,7 +174,24 @@
 				}
 			}
 		}
-		return 0;
+		return exitCode;
+	}
+
+	@Override
+	public boolean waitFor(long timeout, TimeUnit unit) throws InterruptedException {
+		long remainingMs = unit.toMillis(timeout);
+		final long timeoutMs = System.currentTimeMillis() + remainingMs;
+		synchronized (waitForTerminationLock) {
+			while (!isTerminated() && remainingMs > 0) {
+				long waitTime = endTime == RUN_FOREVER ? Long.MAX_VALUE : endTime - System.currentTimeMillis();
+				waitTime = Math.min(waitTime, remainingMs);
+				if (waitTime > 0) {
+					waitForTerminationLock.wait(waitTime);
+				}
+				remainingMs = timeoutMs - System.currentTimeMillis();
+			}
+		}
+		return isTerminated();
 	}
 
 	@Override
@@ -180,13 +200,23 @@
 			final String end = (endTime == RUN_FOREVER ? "never." : "in " + (endTime - System.currentTimeMillis()) + " ms.");
 			throw new IllegalThreadStateException("Mockup process terminates " + end);
 		}
-		return 0;
+		return exitCode;
 	}
 
 	@Override
 	public void destroy() {
+		destroy(0);
+	}
+
+	/**
+	 * Simulate a delay for the mockup process shutdown.
+	 *
+	 * @param delay amount of milliseconds must pass after destroy was called
+	 *            and before the mockup process goes in terminated state
+	 */
+	public void destroy(int delay) {
 		synchronized (waitForTerminationLock) {
-			endTime = System.currentTimeMillis();
+			endTime = System.currentTimeMillis() + delay;
 			waitForTerminationLock.notifyAll();
 		}
 	}
@@ -201,6 +231,15 @@
 	}
 
 	/**
+	 * Set the exit code returned once the process is finished.
+	 *
+	 * @param exitCode new exit code
+	 */
+	public void setExitValue(int exitCode) {
+		this.exitCode = exitCode;
+	}
+
+	/**
 	 * Create a {@link RuntimeProcess} which wraps this {@link MockProcess}.
 	 * <p>
 	 * Note: the process will only be connected to a minimal dummy launch
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
new file mode 100644
index 0000000..c3d631b
--- /dev/null
+++ b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/RuntimeProcessTests.java
@@ -0,0 +1,96 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Paul Pazderski 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:
+ *     Paul Pazderski - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.debug.tests.console;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.debug.core.DebugEvent;
+import org.eclipse.debug.core.DebugPlugin;
+import org.eclipse.debug.core.IDebugEventSetListener;
+import org.eclipse.debug.core.model.RuntimeProcess;
+import org.eclipse.debug.tests.AbstractDebugTest;
+import org.eclipse.debug.tests.TestUtil;
+
+public class RuntimeProcessTests extends AbstractDebugTest {
+
+	public RuntimeProcessTests() {
+		super(RuntimeProcessTests.class.getSimpleName());
+	}
+
+	public RuntimeProcessTests(String name) {
+		super(name);
+	}
+
+	/**
+	 * Test behavior of {@link RuntimeProcess} if the wrapped process
+	 * terminates.
+	 */
+	public void testProcessTerminated() throws Exception {
+		AtomicInteger processTerminateEvents = new AtomicInteger();
+		DebugPlugin.getDefault().addDebugEventListener(new IDebugEventSetListener() {
+			@Override
+			public void handleDebugEvents(DebugEvent[] events) {
+				for (DebugEvent event : events) {
+					if (event.getKind() == DebugEvent.TERMINATE) {
+						processTerminateEvents.incrementAndGet();
+					}
+				}
+			}
+		});
+
+		MockProcess mockProcess = new MockProcess(MockProcess.RUN_FOREVER);
+		RuntimeProcess runtimeProcess = mockProcess.toRuntimeProcess();
+
+		assertFalse("RuntimeProcess already terminated.", runtimeProcess.isTerminated());
+		assertTrue(runtimeProcess.canTerminate());
+
+		mockProcess.setExitValue(1);
+		mockProcess.destroy();
+
+		TestUtil.waitWhile(p -> !p.isTerminated(), runtimeProcess, 1000, p -> "RuntimePocess not terminated.");
+		TestUtil.waitForJobs(getName(), 25, 500);
+		assertEquals("Wrong number of terminate events.", 1, processTerminateEvents.get());
+		assertEquals("RuntimeProcess reported wrong exit code.", 1, runtimeProcess.getExitValue());
+	}
+
+	/** Test {@link RuntimeProcess} terminating the wrapped process. */
+	public void testTerminateProcess() throws Exception {
+		AtomicInteger processTerminateEvents = new AtomicInteger();
+		DebugPlugin.getDefault().addDebugEventListener(new IDebugEventSetListener() {
+			@Override
+			public void handleDebugEvents(DebugEvent[] events) {
+				for (DebugEvent event : events) {
+					if (event.getKind() == DebugEvent.TERMINATE) {
+						processTerminateEvents.incrementAndGet();
+					}
+				}
+			}
+		});
+
+		MockProcess mockProcess = new MockProcess(MockProcess.RUN_FOREVER);
+		RuntimeProcess runtimeProcess = mockProcess.toRuntimeProcess();
+
+		assertFalse("RuntimeProcess already terminated.", runtimeProcess.isTerminated());
+		assertTrue(runtimeProcess.canTerminate());
+
+		mockProcess.setExitValue(1);
+		runtimeProcess.terminate();
+		assertFalse("RuntimeProcess failed to terminated wrapped process.", mockProcess.isAlive());
+
+		TestUtil.waitWhile(p -> !p.isTerminated(), runtimeProcess, 1000, p -> "RuntimePocess not terminated.");
+		TestUtil.waitForJobs(getName(), 25, 500);
+		assertEquals("Wrong number of terminate events.", 1, processTerminateEvents.get());
+		assertEquals("RuntimeProcess reported wrong exit code.", 1, runtimeProcess.getExitValue());
+	}
+}