Bug 32205 - [console] Support output merging for standard VM launches

Change-Id: I34ffe0fbaef8b7e31d723ed2cd32ac6a241c642e
Signed-off-by: Paul Pazderski <paul-eclipse@ppazderski.de>
diff --git a/org.eclipse.jdt.debug.tests/META-INF/MANIFEST.MF b/org.eclipse.jdt.debug.tests/META-INF/MANIFEST.MF
index 88ad986..d692f5b 100644
--- a/org.eclipse.jdt.debug.tests/META-INF/MANIFEST.MF
+++ b/org.eclipse.jdt.debug.tests/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.jdt.debug.tests; singleton:=true
-Bundle-Version: 3.11.500.qualifier
+Bundle-Version: 3.11.600.qualifier
 Bundle-ClassPath: javadebugtests.jar
 Bundle-Activator: org.eclipse.jdt.debug.testplugin.JavaTestPlugin
 Bundle-Vendor: %providerName
diff --git a/org.eclipse.jdt.debug.tests/pom.xml b/org.eclipse.jdt.debug.tests/pom.xml
index b2547ab..3760844 100644
--- a/org.eclipse.jdt.debug.tests/pom.xml
+++ b/org.eclipse.jdt.debug.tests/pom.xml
@@ -18,7 +18,7 @@
   </parent>
   <groupId>org.eclipse.jdt</groupId>
   <artifactId>org.eclipse.jdt.debug.tests</artifactId>
-  <version>3.11.500-SNAPSHOT</version>
+  <version>3.11.600-SNAPSHOT</version>
   <packaging>eclipse-test-plugin</packaging>
   <properties>
     <code.ignoredWarnings>${tests.ignoredWarnings}</code.ignoredWarnings>
diff --git a/org.eclipse.jdt.debug.tests/testprograms/OutSync.java b/org.eclipse.jdt.debug.tests/testprograms/OutSync.java
new file mode 100644
index 0000000..bf25330
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/testprograms/OutSync.java
@@ -0,0 +1,21 @@
+/*******************************************************************************
+ * Copyright (c) 2019 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
+ *******************************************************************************/
+public class OutSync {
+	public static void main(String[] args) throws InterruptedException {
+		for (int i = 0; i < 50; i++) {
+			System.out.println("o");
+			System.err.println("e");
+		}
+	}
+}
diff --git a/org.eclipse.jdt.debug.tests/testprograms/OutSync2.java b/org.eclipse.jdt.debug.tests/testprograms/OutSync2.java
new file mode 100644
index 0000000..0e5914c
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/testprograms/OutSync2.java
@@ -0,0 +1,23 @@
+/*******************************************************************************
+ * Copyright (c) 2019 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
+ *******************************************************************************/
+public class OutSync2 {
+	public static void main(String[] args) throws InterruptedException {
+		for (int i = 0; i < 1000; i++) {
+			System.out.print("o");
+			System.out.flush();
+			System.err.print("e");
+			System.err.flush();
+		}
+	}
+}
diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java
index 786dc41..f0db582 100644
--- a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java
+++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java
@@ -207,7 +207,7 @@
 			"org.eclipse.debug.tests.targets.HcrClass5", "org.eclipse.debug.tests.targets.HcrClass6", "org.eclipse.debug.tests.targets.HcrClass7", "org.eclipse.debug.tests.targets.HcrClass8",
 			"org.eclipse.debug.tests.targets.HcrClass9", "TestContributedStepFilterClass", "TerminateAll_01", "TerminateAll_02", "StepResult1",
 			"StepResult2", "StepResult3", "StepUncaught", "TriggerPoint_01", "BulkThreadCreationTest", "MethodExitAndException",
-			"Bug534319earlyStart", "Bug534319lateStart", "Bug534319singleThread", "Bug534319startBetwen", "MethodCall", "Bug538303", "Bug540243" };
+			"Bug534319earlyStart", "Bug534319lateStart", "Bug534319singleThread", "Bug534319startBetwen", "MethodCall", "Bug538303", "Bug540243", "OutSync", "OutSync2" };
 
 	/**
 	 * the default timeout
diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/core/ConsoleTests.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/core/ConsoleTests.java
index be45210..b4565a4 100644
--- a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/core/ConsoleTests.java
+++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/core/ConsoleTests.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2015 IBM Corporation and others.
+ * Copyright (c) 2000, 2019 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -13,16 +13,28 @@
  *******************************************************************************/
 package org.eclipse.jdt.debug.tests.core;
 
+import java.util.Arrays;
+import java.util.Collections;
+
 import org.eclipse.debug.core.ILaunch;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.debug.core.ILaunchManager;
 import org.eclipse.debug.core.model.IProcess;
 import org.eclipse.debug.internal.ui.DebugUIPlugin;
 import org.eclipse.debug.ui.DebugUITools;
+import org.eclipse.debug.ui.IDebugUIConstants;
 import org.eclipse.jdt.debug.core.IJavaDebugTarget;
 import org.eclipse.jdt.debug.tests.AbstractDebugTest;
+import org.eclipse.jdt.debug.tests.TestUtil;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IDocumentPartitioner;
 import org.eclipse.ui.console.ConsolePlugin;
 import org.eclipse.ui.console.IConsole;
 import org.eclipse.ui.console.IConsoleManager;
 import org.eclipse.ui.console.MessageConsole;
+import org.eclipse.ui.console.TextConsole;
+import org.eclipse.ui.internal.console.IOConsolePartitioner;
 
 /**
  * Tests console line tracker.
@@ -112,4 +124,111 @@
 	    console.dispose();
 	}
 
+	/**
+	 * Test synchronization of standard and error output stream of started process.
+	 * <p>
+	 * This variant tests output on multiple lines and is launched in DEBUG mode.
+	 * </p>
+	 *
+	 * @throws Exception
+	 *             if test failed
+	 */
+	public void testConsoleOutputSynchronization() throws Exception {
+		String typeName = "OutSync";
+		IJavaDebugTarget target = null;
+		try {
+			ILaunchConfiguration launchConfig = getLaunchConfiguration(typeName);
+			ILaunchConfigurationWorkingCopy launchCopy = launchConfig.getWorkingCopy();
+			launchCopy.setAttribute(IDebugUIConstants.ATTR_MERGE_OUTPUT, true);
+			target = launchAndTerminate(launchCopy, DEFAULT_TIMEOUT);
+			String content = getConsoleContent(target.getProcess());
+			// normalize new lines to unix style
+			content = content.replace("\r\n", "\n").replace('\r', '\n');
+			String expectedOutput = String.join("", Collections.nCopies(content.length() / 4, "o\ne\n"));
+			assertEquals("Received wrong output. Probably not synchronized.", expectedOutput, content);
+		} finally {
+			if (target != null) {
+				terminateAndRemove(target);
+			}
+		}
+	}
+
+	/**
+	 * Test synchronization of standard and error output stream of started process.
+	 * <p>
+	 * This variant tests output on single line and is launched in RUN mode.
+	 * </p>
+	 *
+	 * @throws Exception
+	 *             if test failed
+	 */
+	public void testConsoleOutputSynchronization2() throws Exception {
+		String typeName = "OutSync2";
+		ILaunch launch = null;
+		try {
+			ILaunchConfiguration launchConfig = getLaunchConfiguration(typeName);
+			ILaunchConfigurationWorkingCopy launchCopy = launchConfig.getWorkingCopy();
+			launchCopy.setAttribute(IDebugUIConstants.ATTR_MERGE_OUTPUT, true);
+			launch = launchCopy.launch(ILaunchManager.RUN_MODE, null);
+			TestUtil.waitForJobs(getName(), 0, DEFAULT_TIMEOUT);
+			String content = getConsoleContent(launch.getProcesses()[0]);
+			String expectedOutput = String.join("", Collections.nCopies(content.length() / 2, "oe"));
+			assertEquals("Received wrong output. Probably not synchronized.", expectedOutput, content);
+		} finally {
+			if (launch != null) {
+				getLaunchManager().removeLaunch(launch);
+			}
+		}
+	}
+
+	/**
+	 * Test if process error output has another color in console than standard output.
+	 *
+	 * @throws Exception
+	 *             if test failed
+	 */
+	public void testConsoleErrorColoring() throws Exception {
+		String typeName = "OutSync";
+		IJavaDebugTarget target = null;
+		try {
+			target = launchAndTerminate(typeName);
+			final IProcess process = target.getProcess();
+			assertNotNull("Missing VM process.", process);
+			final IConsole console = DebugUITools.getConsole(process);
+			assertTrue("Console is not a TextConsole.", console instanceof TextConsole);
+			final TextConsole textConsole = (TextConsole) console;
+			final IDocumentPartitioner partitioner = textConsole.getDocument().getDocumentPartitioner();
+			assertTrue("Partitioner is not a IOConsolePartitioner.", partitioner instanceof IOConsolePartitioner);
+			final IOConsolePartitioner ioPartitioner = (IOConsolePartitioner) partitioner;
+			TestUtil.waitForJobs(getName(), 100, DEFAULT_TIMEOUT); // wait for output appending
+
+			final long numStyleTypes = Arrays.stream(ioPartitioner.getStyleRanges(0, textConsole.getDocument().getLength())).map((s) -> s.foreground).distinct().count();
+			assertTrue("Console partitioner did not distinct standard and error output.", numStyleTypes > 1);
+		} finally {
+			if (target != null) {
+				terminateAndRemove(target);
+			}
+		}
+	}
+
+	/**
+	 * Try to get the console content associated with given process.
+	 *
+	 * @param process
+	 *            the process on whose output we are interested
+	 * @return the raw content in console probably written by given process. Note: result may be affected by user input and console trimming.
+	 * @throws Exception
+	 *             if content retrieval failed
+	 */
+	private String getConsoleContent(IProcess process) throws Exception {
+		assertNotNull("Missing VM process.", process);
+		final IConsole console = DebugUITools.getConsole(process);
+		assertNotNull("Missing console", console);
+		assertTrue("Console is not a TextConsole.", console instanceof TextConsole);
+		final TextConsole textConsole = (TextConsole) console;
+		TestUtil.waitForJobs(getName(), 100, DEFAULT_TIMEOUT); // wait for output appending
+		assertEquals("Test program failed with error.", 0, process.getExitValue());
+		final IDocument consoleDocument = textConsole.getDocument();
+		return consoleDocument.get();
+	}
 }
diff --git a/org.eclipse.jdt.launching/META-INF/MANIFEST.MF b/org.eclipse.jdt.launching/META-INF/MANIFEST.MF
index 1f976a9..dfb4ded 100644
--- a/org.eclipse.jdt.launching/META-INF/MANIFEST.MF
+++ b/org.eclipse.jdt.launching/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.jdt.launching; singleton:=true
-Bundle-Version: 3.14.100.qualifier
+Bundle-Version: 3.15.0.qualifier
 Bundle-Activator: org.eclipse.jdt.internal.launching.LaunchingPlugin
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
@@ -16,12 +16,13 @@
  org.eclipse.jdt.launching.sourcelookup.containers
 Require-Bundle: org.eclipse.core.resources;bundle-version="[3.5.0,4.0.0)",
  org.eclipse.jdt.core;bundle-version="[3.18.0,4.0.0)",
- org.eclipse.debug.core;bundle-version="[3.13.200,4.0.0)",
+ org.eclipse.debug.core;bundle-version="[3.14.0,4.0.0)",
  org.eclipse.jdt.debug;bundle-version="[3.11.0,4.0.0)",
  org.eclipse.core.variables;bundle-version="[3.2.0,4.0.0)",
  org.eclipse.core.runtime;bundle-version="[3.11.0,4.0.0)",
  org.eclipse.osgi;bundle-version="[3.8.0,4.0.0)",
- org.eclipse.core.expressions;bundle-version="[3.4.0,4.0.0)"
+ org.eclipse.core.expressions;bundle-version="[3.4.0,4.0.0)",
+ org.eclipse.debug.ui;bundle-version="[3.14.0,4.0.0)"
 Bundle-ActivationPolicy: lazy
 Import-Package: com.ibm.icu.text
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
diff --git a/org.eclipse.jdt.launching/launching/org/eclipse/jdt/internal/launching/StandardVMDebugger.java b/org.eclipse.jdt.launching/launching/org/eclipse/jdt/internal/launching/StandardVMDebugger.java
index d97bb68..2b50a66 100644
--- a/org.eclipse.jdt.launching/launching/org/eclipse/jdt/internal/launching/StandardVMDebugger.java
+++ b/org.eclipse.jdt.launching/launching/org/eclipse/jdt/internal/launching/StandardVMDebugger.java
@@ -331,7 +331,7 @@
 
 				connector.startListening(map);
 
-				p = exec(cmdLine, cmdDetails.getWorkingDir(), cmdDetails.getEnvp());
+				p = exec(cmdLine, cmdDetails.getWorkingDir(), cmdDetails.getEnvp(), config.isMergeOutput());
 				if (p == null) {
 					return;
 				}
diff --git a/org.eclipse.jdt.launching/launching/org/eclipse/jdt/internal/launching/StandardVMRunner.java b/org.eclipse.jdt.launching/launching/org/eclipse/jdt/internal/launching/StandardVMRunner.java
index 010c2a6..3501323 100644
--- a/org.eclipse.jdt.launching/launching/org/eclipse/jdt/internal/launching/StandardVMRunner.java
+++ b/org.eclipse.jdt.launching/launching/org/eclipse/jdt/internal/launching/StandardVMRunner.java
@@ -504,8 +504,8 @@
 		IProgressMonitor subMonitor = SubMonitor.convert(monitor, 1);
 		subMonitor.beginTask(LaunchingMessages.StandardVMRunner_Launching_VM____1, 2);
 		subMonitor.subTask(LaunchingMessages.StandardVMRunner_Starting_virtual_machine____3);
-		Process p= null;
-		p = exec(cmdLine, cmdDetails.getWorkingDir(), cmdDetails.getEnvp());
+		Process p = null;
+		p = exec(cmdLine, cmdDetails.getWorkingDir(), cmdDetails.getEnvp(), config.isMergeOutput());
 		if (p == null) {
 			return;
 		}
diff --git a/org.eclipse.jdt.launching/launching/org/eclipse/jdt/launching/AbstractVMRunner.java b/org.eclipse.jdt.launching/launching/org/eclipse/jdt/launching/AbstractVMRunner.java
index 07084f3..e6ef2e6 100644
--- a/org.eclipse.jdt.launching/launching/org/eclipse/jdt/launching/AbstractVMRunner.java
+++ b/org.eclipse.jdt.launching/launching/org/eclipse/jdt/launching/AbstractVMRunner.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2018 IBM Corporation and others.
+ * Copyright (c) 2000, 2019 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -96,6 +96,28 @@
 	}
 
 	/**
+	 * Executes the given command line using the given working directory and environment
+	 *
+	 * @param cmdLine
+	 *            the command line
+	 * @param workingDirectory
+	 *            the working directory
+	 * @param envp
+	 *            the environment
+	 * @param mergeOutput
+	 *            if <code>true</code> the error stream will be merged with standard output stream and both can be read through the same output stream
+	 * @return the {@link Process}
+	 * @throws CoreException
+	 *             is the execution fails
+	 * @since 3.15
+	 * @see DebugPlugin#exec(String[], File, String[])
+	 */
+	protected Process exec(String[] cmdLine, File workingDirectory, String[] envp, boolean mergeOutput) throws CoreException {
+		cmdLine = quoteWindowsArgs(cmdLine);
+		return DebugPlugin.exec(cmdLine, workingDirectory, envp, mergeOutput);
+	}
+
+	/**
 	 * @since 3.11
 	 */
 	protected static String[] quoteWindowsArgs(String[] cmdLine) {
diff --git a/org.eclipse.jdt.launching/launching/org/eclipse/jdt/launching/JavaLaunchDelegate.java b/org.eclipse.jdt.launching/launching/org/eclipse/jdt/launching/JavaLaunchDelegate.java
index ccd4a4a..edb94cb 100644
--- a/org.eclipse.jdt.launching/launching/org/eclipse/jdt/launching/JavaLaunchDelegate.java
+++ b/org.eclipse.jdt.launching/launching/org/eclipse/jdt/launching/JavaLaunchDelegate.java
@@ -21,6 +21,7 @@
 import org.eclipse.core.runtime.NullProgressMonitor;
 import org.eclipse.debug.core.ILaunch;
 import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.ui.IDebugUIConstants;
 import org.eclipse.jdt.core.IJavaProject;
 import org.eclipse.jdt.core.IModuleDescription;
 import org.eclipse.jdt.internal.launching.LaunchingMessages;
@@ -131,6 +132,8 @@
 				runConfig.setOverrideDependencies(getModuleCLIOptions(configuration));
 			}
 		}
+		runConfig.setMergeOutput(configuration.getAttribute(IDebugUIConstants.ATTR_MERGE_OUTPUT, false));
+
 		// check for cancellation
 		if (monitor.isCanceled()) {
 			return null;
diff --git a/org.eclipse.jdt.launching/launching/org/eclipse/jdt/launching/VMRunnerConfiguration.java b/org.eclipse.jdt.launching/launching/org/eclipse/jdt/launching/VMRunnerConfiguration.java
index 4215ed4..c43a96e 100644
--- a/org.eclipse.jdt.launching/launching/org/eclipse/jdt/launching/VMRunnerConfiguration.java
+++ b/org.eclipse.jdt.launching/launching/org/eclipse/jdt/launching/VMRunnerConfiguration.java
@@ -41,6 +41,7 @@
 	private Boolean fPreviewEnabled = false;
 	private Map<String, Object> fVMSpecificAttributesMap;
 	private boolean fResume = true;
+	private boolean fMergeOutput = false;
 
 	private static final String[] fgEmpty= new String[0];
 
@@ -355,4 +356,24 @@
 		this.fPreviewEnabled = fPreviewEnabled;
 	}
 
+	/**
+	 * Gets the fMergeOutput.
+	 *
+	 * @return the fMergeOutput
+	 * @since 3.15
+	 */
+	public boolean isMergeOutput() {
+		return fMergeOutput;
+	}
+
+	/**
+	 * Sets the fMergeOutput. If <code>true</code> the VM will be run with redirectErrorStream(true) to merge error and standard output.
+	 *
+	 * @param fMergeOutput
+	 *            the fMergeOutput to set
+	 * @since 3.15
+	 */
+	public void setMergeOutput(boolean fMergeOutput) {
+		this.fMergeOutput = fMergeOutput;
+	}
 }
diff --git a/org.eclipse.jdt.launching/plugin.xml b/org.eclipse.jdt.launching/plugin.xml
index 3bd9d83..2e36a20 100644
--- a/org.eclipse.jdt.launching/plugin.xml
+++ b/org.eclipse.jdt.launching/plugin.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?eclipse version="3.0"?>
 <!--
-     Copyright (c) 2005, 2018 IBM Corporation and others.
+     Copyright (c) 2005, 2019 IBM Corporation and others.
 
      This program and the accompanying materials
      are made available under the terms of the Eclipse Public License 2.0
@@ -50,6 +50,7 @@
       <launchConfigurationType
             allowPrototypes="true"
             allowCommandLine="true"
+            allowOutputMerging="true"
             delegate="org.eclipse.jdt.launching.sourcelookup.advanced.AdvancedJavaLaunchDelegate"
             delegateDescription="%localJavaApplicationDelegate.description"
             delegateName="%eclipseJDTLauncher.name"
diff --git a/org.eclipse.jdt.launching/pom.xml b/org.eclipse.jdt.launching/pom.xml
index 2c8af17..679c53b 100644
--- a/org.eclipse.jdt.launching/pom.xml
+++ b/org.eclipse.jdt.launching/pom.xml
@@ -18,7 +18,7 @@
   </parent>
   <groupId>org.eclipse.jdt</groupId>
   <artifactId>org.eclipse.jdt.launching</artifactId>
-  <version>3.14.100-SNAPSHOT</version>
+  <version>3.15.0-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
   
   <build>