Bug 562776: Use Windows ConPTY API instead of WinPTY
There are lots of bugs in WinPTY, while upgrading WinPTY would
resolve some of them, there are others that are unresolvable. See
https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/
for a backgrounder on the general subject.
In this first version ConPTY won't be enabled by default, it requires
system property
org.eclipse.cdt.core.conpty_enabled=true
to be set. i.e. start Eclipse with:
-vmargs -Dorg.eclipse.cdt.core.conpty_enabled=true
In a future version the default will change to on if available,
so to force it off use:
org.eclipse.cdt.core.conpty_enabled=false
Change-Id: Ib2df0095027c23f20daa6aa044d9e5f0b0443164
diff --git a/core/org.eclipse.cdt.core.native/META-INF/MANIFEST.MF b/core/org.eclipse.cdt.core.native/META-INF/MANIFEST.MF
index f5e9bc3..95b98ef 100644
--- a/core/org.eclipse.cdt.core.native/META-INF/MANIFEST.MF
+++ b/core/org.eclipse.cdt.core.native/META-INF/MANIFEST.MF
@@ -10,7 +10,9 @@
org.eclipse.cdt.utils;native=split;mandatory:=native,
org.eclipse.cdt.utils.pty;version="5.7",
org.eclipse.cdt.utils.spawner;version="5.7"
-Require-Bundle: org.eclipse.core.runtime;bundle-version="[3.1.0,4.0.0)"
+Require-Bundle: org.eclipse.core.runtime;bundle-version="[3.1.0,4.0.0)",
+ com.sun.jna;bundle-version="[5.6.0,6.0.0)";resolution:=optional,
+ com.sun.jna.platform;bundle-version="[5.6.0,6.0.0)";resolution:=optional
Bundle-ActivationPolicy: lazy
Bundle-RequiredExecutionEnvironment: JavaSE-11
Automatic-Module-Name: org.eclipse.cdt.core.native
diff --git a/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/internal/core/natives/Messages.java b/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/internal/core/natives/Messages.java
index 5874288e..b72519e 100644
--- a/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/internal/core/natives/Messages.java
+++ b/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/internal/core/natives/Messages.java
@@ -16,6 +16,8 @@
import org.eclipse.osgi.util.NLS;
public class Messages extends NLS {
+ public static String PTY_FailedToStartConPTY;
+ public static String PTY_NoClassDefFoundError;
public static String Util_exception_cannotCreatePty;
public static String Util_exception_cannotSetTerminalSize;
public static String Util_error_cannotRun;
diff --git a/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/internal/core/natives/Messages.properties b/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/internal/core/natives/Messages.properties
index 9eb988a..8e91c7b 100644
--- a/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/internal/core/natives/Messages.properties
+++ b/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/internal/core/natives/Messages.properties
@@ -13,6 +13,8 @@
# Martin Oberhuber (Wind River) - [303083] Split from CCorePluginResources.properties
###############################################################################
+PTY_FailedToStartConPTY=Failed start a new style ConPTY. This is expected on Windows machines before Windows 10 1904 version and will fall back to WinPTY. Please consider upgrading to a newer version of Windows 10 to take advantage of the new improved console behavior.
+PTY_NoClassDefFoundError=Failed start a new style ConPTY due to NoClassDefFoundError which probably means that the optional dependency on JNA is not available. Consider updating your product to include JNA to enable the ConPTY.
Util_exception_cannotCreatePty=Cannot create pty
Util_exception_closeError=close error
Util_exception_cannotSetTerminalSize=Setting terminal size is not supported
diff --git a/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/WindowsArgumentQuoter.java b/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/WindowsArgumentQuoter.java
new file mode 100644
index 0000000..fcfda8ed
--- /dev/null
+++ b/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/WindowsArgumentQuoter.java
@@ -0,0 +1,75 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Kichwa Coders Canada Inc. 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
+ *******************************************************************************/
+package org.eclipse.cdt.utils;
+
+import java.util.regex.Pattern;
+
+/**
+ * Implementation of proper Windows quoting based on blog post:
+ * https://docs.microsoft.com/en-ca/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
+ *
+ * @noreference This class is not intended to be referenced by clients.
+ */
+public class WindowsArgumentQuoter {
+
+ public static String quoteArgv(String[] cmdarray, boolean force) {
+ StringBuilder quoted = new StringBuilder();
+ for (String arg : cmdarray) {
+ quoteArg(arg, quoted, force);
+ quoted.append(' ');
+ }
+ quoted.deleteCharAt(quoted.length() - 1);
+ return quoted.toString();
+ }
+
+ private static Pattern spaces = Pattern.compile(".*\\s.*"); //$NON-NLS-1$
+
+ private static void quoteArg(String arg, StringBuilder quoted, boolean force) {
+
+ // Unless we're told otherwise, don't quote unless we actually
+ // need to do so --- hopefully avoid problems if programs won't
+ // parse quotes properly
+
+ if (!force && !arg.isEmpty() && !spaces.matcher(arg).matches()) {
+ quoted.append(arg);
+ } else {
+ quoted.append('"');
+ for (int i = 0; i < arg.length(); i++) {
+ int numberBackslashes = 0;
+
+ while (i < arg.length() && arg.charAt(i) == '\\') {
+ i++;
+ numberBackslashes++;
+ }
+
+ if (i == arg.length()) {
+ // Escape all backslashes, but let the terminating
+ // double quotation mark we add below be interpreted
+ // as a metacharacter.
+ quoted.append("\\".repeat(numberBackslashes * 2)); //$NON-NLS-1$
+ break;
+ } else if (arg.charAt(i) == '"') {
+
+ // Escape all backslashes and the following
+ // double quotation mark.
+ quoted.append("\\".repeat(numberBackslashes)); //$NON-NLS-1$
+ quoted.append('"');
+ } else {
+ // Backslashes aren't special here.
+ quoted.append("\\".repeat(numberBackslashes)); //$NON-NLS-1$
+ quoted.append(arg.charAt(i));
+ }
+ }
+ quoted.append('"');
+ }
+ }
+
+}
diff --git a/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/pty/ConPTY.java b/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/pty/ConPTY.java
new file mode 100644
index 0000000..ec57782
--- /dev/null
+++ b/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/pty/ConPTY.java
@@ -0,0 +1,357 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Kichwa Coders Canada Inc. 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
+ *******************************************************************************/
+package org.eclipse.cdt.utils.pty;
+
+import static com.sun.jna.platform.win32.WinBase.CREATE_UNICODE_ENVIRONMENT;
+import static com.sun.jna.platform.win32.WinBase.EXTENDED_STARTUPINFO_PRESENT;
+import static com.sun.jna.platform.win32.WinBase.INFINITE;
+import static com.sun.jna.platform.win32.WinBase.STARTF_USESTDHANDLES;
+import static com.sun.jna.platform.win32.WinBase.WAIT_OBJECT_0;
+import static com.sun.jna.platform.win32.WinError.S_OK;
+import static com.sun.jna.platform.win32.WinNT.PROCESS_QUERY_INFORMATION;
+import static com.sun.jna.platform.win32.WinNT.SYNCHRONIZE;
+import static org.eclipse.cdt.utils.pty.ConPTYKernel32.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.cdt.utils.WindowsArgumentQuoter;
+
+import com.sun.jna.Memory;
+import com.sun.jna.Native;
+import com.sun.jna.Pointer;
+import com.sun.jna.platform.win32.BaseTSD.DWORD_PTR;
+import com.sun.jna.platform.win32.BaseTSD.SIZE_T;
+import com.sun.jna.platform.win32.Kernel32;
+import com.sun.jna.platform.win32.Kernel32Util;
+import com.sun.jna.platform.win32.WinBase;
+import com.sun.jna.platform.win32.WinBase.PROCESS_INFORMATION;
+import com.sun.jna.platform.win32.WinDef;
+import com.sun.jna.platform.win32.WinDef.DWORD;
+import com.sun.jna.platform.win32.WinDef.PVOID;
+import com.sun.jna.platform.win32.WinError;
+import com.sun.jna.platform.win32.WinNT;
+import com.sun.jna.platform.win32.WinNT.HANDLE;
+import com.sun.jna.platform.win32.WinNT.HANDLEByReference;
+import com.sun.jna.platform.win32.WinNT.HRESULT;
+import com.sun.jna.ptr.IntByReference;
+
+/**
+ * A JNA implementation for ConPTY to provide a Windows native (as opposed to WinPTY)
+ * implementation of a PTY.
+ *
+ * This class should be accessed/created via the PTY class which will use ConPTY when it
+ * is available.
+ *
+ * @noreference This class is not intended to be referenced by clients.
+ */
+public class ConPTY {
+
+ private Handles handles = new Handles();
+
+ /**
+ * The handles that need to be closed when the PTY is done
+ */
+ private static class Handles {
+ private HANDLEByReference pseudoConsole;
+ private ConPTYKernel32.STARTUPINFOEX startupInfo;
+ private Memory threadAttributeListMemory;
+ private WinBase.PROCESS_INFORMATION processInformation;
+ private HANDLEByReference pipeOut;
+ private HANDLEByReference pipeIn;
+
+ /** Saved for convenience to make it easier to identify/find the process in process explorer */
+ public int pid;
+ }
+
+ /**
+ * Create a new Windows Pseudo Console (ConPTY) that an application can be attached to.
+ */
+ public ConPTY() throws IOException {
+ handles.pseudoConsole = new HANDLEByReference();
+ handles.pipeIn = new HANDLEByReference();
+ handles.pipeOut = new HANDLEByReference();
+
+ var phPipePTYIn = new WinNT.HANDLEByReference();
+ var phPipePTYOut = new WinNT.HANDLEByReference();
+
+ boolean res;
+ res = ConPTYKernel32.INSTANCE.CreatePipe(phPipePTYIn, handles.pipeOut, null, 0);
+ checkErr(res, "CreatePipe"); //$NON-NLS-1$
+
+ res = ConPTYKernel32.INSTANCE.CreatePipe(handles.pipeIn, phPipePTYOut, null, 0);
+ checkErr(res, "CreatePipe"); //$NON-NLS-1$
+
+ // The console will be resized later with ResizePseudoConsole, start with the old classic size!
+ var consoleSize = new ConPTYKernel32.COORD_ByValue();
+ consoleSize.X = (short) 80;
+ consoleSize.Y = (short) 24;
+
+ var hr = ConPTYKernel32.INSTANCE.CreatePseudoConsole(consoleSize, phPipePTYIn.getValue(),
+ phPipePTYOut.getValue(), new WinDef.DWORD(0), handles.pseudoConsole);
+ checkErr(hr, "CreatePseudoConsole"); //$NON-NLS-1$
+
+ res = ConPTYKernel32.INSTANCE.CloseHandle(phPipePTYOut.getValue());
+ checkErr(res, "CloseHandle"); //$NON-NLS-1$
+ res = ConPTYKernel32.INSTANCE.CloseHandle(phPipePTYIn.getValue());
+ checkErr(res, "CloseHandle"); //$NON-NLS-1$
+ }
+
+ /**
+ * Executes the given process in the PTY
+ *
+ * @param cmdarray Command and arguments that will be quotes using standard Windows rules to make a
+ * command line. See {@link WindowsArgumentQuoter}
+ * @param envp
+ * @param dir
+ * @return the PID
+ * @throws IOException
+ */
+ public int exec(String[] cmdarray, String[] envp, String dir) throws IOException {
+ String quoted = WindowsArgumentQuoter.quoteArgv(cmdarray, false);
+ handles.startupInfo = new ConPTYKernel32.STARTUPINFOEX();
+ handles.threadAttributeListMemory = PrepareStartupInformation(handles.startupInfo, handles.pseudoConsole);
+ handles.processInformation = new PROCESS_INFORMATION();
+
+ var status = ConPTYKernel32.INSTANCE.CreateProcess(null, quoted, null, null, false,
+ new DWORD(CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT), toByteArray(envp), dir,
+ handles.startupInfo, handles.processInformation);
+ checkErr(status, "CreateProcess"); //$NON-NLS-1$
+ return getPID();
+ }
+
+ /**
+ * Convert envp to a byte array, encoding UTF_16LE. Remember to pass CREATE_UNICODE_ENVIRONMENT
+ * to CreateProcess
+ */
+ public static Memory toByteArray(String[] envp) throws IOException {
+ if (envp == null) {
+ return null;
+ }
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ for (String string : envp) {
+ bos.write(string.getBytes(StandardCharsets.UTF_16LE));
+ // Terminate each variable with two zero bytes
+ bos.write(0);
+ bos.write(0);
+ }
+ // Terminate the whole block with two additional zero bytes
+ bos.write(0);
+ bos.write(0);
+ byte[] byteArray = bos.toByteArray();
+ Memory memory = new Memory(byteArray.length);
+ memory.write(0, byteArray, 0, byteArray.length);
+ return memory;
+
+ }
+
+ public int getPID() {
+ handles.pid = handles.processInformation.dwProcessId.intValue();
+ return handles.pid;
+ }
+
+ private static Memory PrepareStartupInformation(ConPTYKernel32.STARTUPINFOEX pStartupInfo, HANDLEByReference phPC)
+ throws IOException {
+ pStartupInfo.StartupInfo.cb = new DWORD(pStartupInfo.size());
+
+ pStartupInfo.StartupInfo.hStdOutput = new HANDLE();
+ pStartupInfo.StartupInfo.hStdError = new HANDLE();
+ pStartupInfo.StartupInfo.hStdInput = new HANDLE();
+ pStartupInfo.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
+
+ boolean res;
+
+ var attrListSize = new ConPTYKernel32.SIZE_TByReference();
+ res = ConPTYKernel32.INSTANCE.InitializeProcThreadAttributeList(Pointer.NULL, new DWORD(1), new DWORD(0),
+ attrListSize);
+ Kernel32.INSTANCE.SetLastError(0);
+ var memory = new Memory(attrListSize.getValue().longValue());
+
+ res = ConPTYKernel32.INSTANCE.InitializeProcThreadAttributeList(memory, new DWORD(1), new DWORD(0),
+ attrListSize);
+ checkErr(res, "InitializeProcThreadAttributeList"); //$NON-NLS-1$
+
+ var dwPROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = new DWORD_PTR(PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE);
+
+ res = ConPTYKernel32.INSTANCE.UpdateProcThreadAttribute(memory, new DWORD(0),
+ dwPROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, new PVOID(phPC.getValue().getPointer()),
+ new SIZE_T(Native.POINTER_SIZE), null, null);
+ checkErr(res, "UpdateProcThreadAttribute"); //$NON-NLS-1$
+
+ pStartupInfo.lpAttributeList = memory.share(0);
+ return memory;
+ }
+
+ /**
+ * Implements the contract of {@link Process#waitFor()}. This is used by {@link PTY#waitFor(org.eclipse.cdt.utils.spawner.Spawner, int),
+ * but in the Spawner case the PID is passed around unnecessarily. This method therefore waits for the process it created only,
+ * like how Process#waitFor() behaves.
+ *
+ * @see Process#waitFor()
+ * @see PTY#waitFor(org.eclipse.cdt.utils.spawner.Spawner, int)
+ */
+ public int waitFor() {
+ try {
+ int what = 0;
+ HANDLE hProc;
+
+ hProc = Kernel32.INSTANCE.OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION, false, getPID());
+ checkErr(hProc, "OpenProcess"); //$NON-NLS-1$
+
+ what = Kernel32.INSTANCE.WaitForSingleObject(hProc, INFINITE);
+
+ IntByReference exit_code = new IntByReference(0);
+ if (what == WAIT_OBJECT_0) {
+ Kernel32.INSTANCE.GetExitCodeProcess(hProc, exit_code);
+ }
+
+ boolean closeHandle = Kernel32.INSTANCE.CloseHandle(hProc);
+ checkErr(closeHandle, "CloseHandle"); //$NON-NLS-1$
+ return exit_code.getValue();
+ } catch (IOException e) {
+ // Returning -1 is the equivalent of what was done
+ // in error handling in the JNI versions of waitFor
+ return -1;
+ }
+ }
+
+ /**
+ * Closes the entire PTY session. This will have the side effect of closing all the IO
+ * channels at once. The process will also be terminated when the console is closed if
+ * it isn't closed already. If the console's host (conhost) is closed then the process
+ * won't be automatically terminated. This happens if conhost crashes and the behaviour
+ * with winpty is the same in that case.
+ */
+ public synchronized void close() throws IOException {
+ if (handles == null) {
+ return;
+ }
+ boolean res;
+
+ res = ConPTYKernel32.INSTANCE.CloseHandle(handles.processInformation.hThread);
+ checkErr(res, "CloseHandle processInformation.hThread"); //$NON-NLS-1$
+
+ res = ConPTYKernel32.INSTANCE.CloseHandle(handles.processInformation.hProcess);
+ checkErr(res, "CloseHandle processInformation.hProcess"); //$NON-NLS-1$
+
+ ConPTYKernel32.INSTANCE.DeleteProcThreadAttributeList(handles.startupInfo.lpAttributeList);
+ handles.threadAttributeListMemory.clear();
+
+ ConPTYKernel32.INSTANCE.ClosePseudoConsole(handles.pseudoConsole.getValue());
+
+ res = ConPTYKernel32.INSTANCE.CancelIoEx(handles.pipeIn.getValue(), Pointer.NULL);
+ int err = Native.getLastError();
+ if (err != WinError.ERROR_NOT_FOUND) {
+ checkErr(res, "CancelIoEx"); //$NON-NLS-1$
+ }
+
+ res = ConPTYKernel32.INSTANCE.CloseHandle(handles.pipeOut.getValue());
+ checkErr(res, "CloseHandle pipeOut"); //$NON-NLS-1$
+
+ res = ConPTYKernel32.INSTANCE.CloseHandle(handles.pipeIn.getValue());
+ checkErr(res, "CloseHandle pipeIn"); //$NON-NLS-1$
+
+ handles = null;
+ }
+
+ /**
+ * Implements contract of {@link InputStream#read(byte[])}
+ * @see InputStream#read(byte[])
+ */
+ public int read(byte[] buf) throws IOException {
+ if (handles == null) {
+ throw new IOException("ConPTY is closed."); //$NON-NLS-1$
+ }
+
+ var pipe = handles.pipeIn;
+
+ IntByReference dwBytesRead = new IntByReference(0);
+ boolean fRead = false;
+
+ fRead = Kernel32.INSTANCE.ReadFile(pipe.getValue(), buf, buf.length, dwBytesRead, null);
+ checkErr(fRead, "ReadFile"); //$NON-NLS-1$
+ int value = dwBytesRead.getValue();
+ if (value == 0) {
+ // We are at EOF because we are doing Synchronous and non-overlapped operation
+ // Implementation note: I don't know how to get this with terminal programs, so
+ // I have not seen this happen in development.
+ return -1;
+ }
+ return value;
+ }
+
+ /**
+ * Implements the contract of {@link OutputStream#write(byte[])}
+ * @see OutputStream#write(byte[])
+ */
+ public void write(byte[] buf) throws IOException {
+ if (handles == null) {
+ throw new IOException("ConPTY is closed."); //$NON-NLS-1$
+ }
+ IntByReference dwBytesWritten = new IntByReference(0);
+ boolean fWritten = false;
+ fWritten = Kernel32.INSTANCE.WriteFile(handles.pipeOut.getValue(), buf, buf.length, dwBytesWritten, null);
+ checkErr(fWritten, "WriteFile"); //$NON-NLS-1$
+ }
+
+ /**
+ * Implements the contract of {@link PTY#setTerminalSize(int, int)}, but throws exceptions
+ * that PTY logs.
+ * @see PTY#setTerminalSize(int, int)
+ */
+ public void setTerminalSize(int width, int height) throws IOException {
+ if (handles == null) {
+ throw new IOException("ConPTY is closed."); //$NON-NLS-1$
+ }
+
+ var consoleSize = new ConPTYKernel32.COORD_ByValue();
+ consoleSize.X = (short) width;
+ consoleSize.Y = (short) height;
+
+ HRESULT result = ConPTYKernel32.INSTANCE.ResizePseudoConsole(handles.pseudoConsole.getValue(), consoleSize);
+ checkErr(result, "ResizePseudoConsole"); //$NON-NLS-1$
+
+ }
+
+ /**
+ * Throw an IOException if hr is not S_OK.
+ */
+ private static void checkErr(WinNT.HRESULT hr, String method) throws IOException {
+ if (!S_OK.equals(hr)) {
+ String msg = Kernel32Util.getLastErrorMessage();
+ throw new IOException(String.format("%s: %s", method, msg)); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Throw an IOException if status is false.
+ */
+ private static void checkErr(boolean status, String method) throws IOException {
+ if (!status) {
+ int lastError = Native.getLastError();
+ String msg = Kernel32Util.formatMessage(lastError);
+ throw new IOException(String.format("%s: %s: %s", method, lastError, msg)); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Throw an IOException if handle is null.
+ */
+ private static void checkErr(HANDLE handle, String method) throws IOException {
+ if (handle == null) {
+ String msg = Kernel32Util.getLastErrorMessage();
+ throw new IOException(String.format("%s: %s", method, msg)); //$NON-NLS-1$
+ }
+ }
+}
diff --git a/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/pty/ConPTYInputStream.java b/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/pty/ConPTYInputStream.java
new file mode 100644
index 0000000..3274ec5
--- /dev/null
+++ b/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/pty/ConPTYInputStream.java
@@ -0,0 +1,67 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Kichwa Coders Canada Inc. 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
+ *******************************************************************************/
+package org.eclipse.cdt.utils.pty;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * @noreference This class is not intended to be referenced by clients.
+ */
+public class ConPTYInputStream extends PTYInputStream {
+
+ private ConPTY conPty;
+
+ public ConPTYInputStream(ConPTY conPty) {
+ super(null);
+ this.conPty = conPty;
+ }
+
+ /**
+ * @see InputStream#read(byte[], int, int)
+ */
+ @Override
+ public int read(byte[] buf, int off, int len) throws IOException {
+ if (buf == null) {
+ throw new NullPointerException();
+ } else if ((off < 0) || (off > buf.length) || (len < 0) || ((off + len) > buf.length) || ((off + len) < 0)) {
+ throw new IndexOutOfBoundsException();
+ } else if (len == 0) {
+ return 0;
+ }
+ byte[] tmpBuf = new byte[len];
+
+ len = conPty.read(tmpBuf);
+ if (len <= 0)
+ return -1;
+
+ System.arraycopy(tmpBuf, 0, buf, off, len);
+ return len;
+ }
+
+ /**
+ * Close the Reader
+ * @exception IOException on error.
+ */
+ @Override
+ public void close() throws IOException {
+ if (conPty == null) {
+ return;
+ }
+ try {
+ conPty.close();
+ } finally {
+ conPty = null;
+ }
+
+ }
+
+}
diff --git a/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/pty/ConPTYKernel32.java b/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/pty/ConPTYKernel32.java
new file mode 100644
index 0000000..bbcdb2d
--- /dev/null
+++ b/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/pty/ConPTYKernel32.java
@@ -0,0 +1,69 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Kichwa Coders Canada Inc. 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
+ *******************************************************************************/
+package org.eclipse.cdt.utils.pty;
+
+import com.sun.jna.Native;
+import com.sun.jna.Pointer;
+import com.sun.jna.Structure;
+import com.sun.jna.platform.win32.Kernel32;
+
+/**
+ * This class is an extension of JNA and everything here needs to be contributed back
+ * to JNA. This class was written against JNA 5.6
+ *
+ * @noreference This interface is not intended to be referenced by clients.
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface ConPTYKernel32 extends Kernel32 {
+ ConPTYKernel32 INSTANCE = Native.load("kernel32", ConPTYKernel32.class, //$NON-NLS-1$
+ com.sun.jna.win32.W32APIOptions.DEFAULT_OPTIONS);
+
+ int PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016;
+
+ class SIZE_TByReference extends ULONG_PTRByReference {
+ @Override
+ public SIZE_T getValue() {
+ return new SIZE_T(super.getValue().longValue());
+ }
+ }
+
+ boolean CancelIoEx(HANDLE hFile, Pointer lpOverlapped);
+
+ public static class COORD_ByValue extends COORD implements Structure.ByValue {
+ }
+
+ @Structure.FieldOrder({ "StartupInfo", "lpAttributeList" })
+ class STARTUPINFOEX extends Structure {
+ public STARTUPINFO StartupInfo;
+ public Pointer lpAttributeList;
+ }
+
+ HRESULT CreatePseudoConsole(COORD.ByValue size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags,
+ HANDLEByReference phPC);
+
+ HRESULT ResizePseudoConsole(HANDLE hPC, COORD.ByValue size);
+
+ void ClosePseudoConsole(HANDLE hPC);
+
+ boolean InitializeProcThreadAttributeList(Pointer lpAttributeList, DWORD dwAttributeCount, DWORD dwFlags,
+ SIZE_TByReference lpSize);
+
+ boolean UpdateProcThreadAttribute(Pointer lpAttributeList, DWORD dwFlags, DWORD_PTR Attribute, PVOID lpValue,
+ SIZE_T cbSize, PVOID lpPreviousValue, SIZE_TByReference lpReturnSize);
+
+ void DeleteProcThreadAttributeList(Pointer lpAttributeList);
+
+ boolean CreateProcess(String lpApplicationName, String lpCommandLine, SECURITY_ATTRIBUTES lpProcessAttributes,
+ SECURITY_ATTRIBUTES lpThreadAttributes, boolean bInheritHandles, DWORD dwCreationFlags,
+ Pointer lpEnvironment, String lpCurrentDirectory, STARTUPINFOEX lpStartupInfo,
+ PROCESS_INFORMATION lpProcessInformation);
+}
\ No newline at end of file
diff --git a/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/pty/ConPTYOutputStream.java b/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/pty/ConPTYOutputStream.java
new file mode 100644
index 0000000..5d6239d
--- /dev/null
+++ b/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/pty/ConPTYOutputStream.java
@@ -0,0 +1,51 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Kichwa Coders Canada Inc. 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
+ *******************************************************************************/
+package org.eclipse.cdt.utils.pty;
+
+import java.io.IOException;
+
+/**
+ * @noreference This class is not intended to be referenced by clients.
+ */
+public class ConPTYOutputStream extends PTYOutputStream {
+ private ConPTY conPty;
+
+ public ConPTYOutputStream(ConPTY conPty) {
+ super(null, false);
+ this.conPty = conPty;
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ if (b == null) {
+ throw new NullPointerException();
+ } else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) {
+ throw new IndexOutOfBoundsException();
+ } else if (len == 0) {
+ return;
+ }
+ byte[] tmpBuf = new byte[len];
+ System.arraycopy(b, off, tmpBuf, 0, len);
+ conPty.write(tmpBuf);
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (conPty == null) {
+ return;
+ }
+ try {
+ conPty.close();
+ } finally {
+ conPty = null;
+ }
+ }
+}
diff --git a/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/pty/PTY.java b/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/pty/PTY.java
index 9eabae7..2774902 100644
--- a/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/pty/PTY.java
+++ b/core/org.eclipse.cdt.core.native/src/org/eclipse/cdt/utils/pty/PTY.java
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2002, 2015 QNX Software Systems and others.
+ * Copyright (c) 2002, 2021 QNX Software Systems and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
@@ -40,19 +40,36 @@
}
final Mode mode;
+ /**
+ * Unused in conPTY.
+ * Created, but never read in winPTY.
+ * Important for Posix PTY.
+ */
final String slave;
final PTYInputStream in;
final PTYOutputStream out;
/**
* NOTE: Field is accessed by the native layer. Do not refactor!
+ * This field is NOT used by ConPTY layer.
*/
int master;
private static boolean hasPTY;
+
+ private enum IS_CONPTY {
+ CONPTY_UNKNOWN, CONPTY_YES, CONPTY_NO;
+ }
+
+ /**
+ * We don't know if we have conpty until we try - so this starts
+ * null and will be true or false once it is determined.
+ */
+ private static IS_CONPTY isConPTY = IS_CONPTY.CONPTY_UNKNOWN;
private static boolean isWinPTY;
private static boolean isConsoleModeSupported;
private static boolean setTerminalSizeErrorAlreadyLogged;
+ private ConPTY conPTY;
/**
* The master fd is used on two streams. We need to wrap the fd
@@ -119,14 +136,40 @@
if (isConsole() && !isConsoleModeSupported) {
throw new IOException(Messages.Util_exception_cannotCreatePty);
}
- slave = hasPTY ? openMaster(isConsole()) : null;
-
- if (slave == null) {
- throw new IOException(Messages.Util_exception_cannotCreatePty);
+ PTYInputStream inInit = null;
+ PTYOutputStream outInit = null;
+ String slaveInit = null;
+ if (isConPTY != IS_CONPTY.CONPTY_NO) {
+ try {
+ conPTY = new ConPTY();
+ isConPTY = IS_CONPTY.CONPTY_YES;
+ slaveInit = "conpty"; //$NON-NLS-1$
+ inInit = new ConPTYInputStream(conPTY);
+ outInit = new ConPTYOutputStream(conPTY);
+ } catch (RuntimeException e) {
+ isConPTY = IS_CONPTY.CONPTY_NO;
+ CNativePlugin.log(Messages.PTY_FailedToStartConPTY, e);
+ } catch (NoClassDefFoundError e) {
+ isConPTY = IS_CONPTY.CONPTY_NO;
+ CNativePlugin.log(Messages.PTY_NoClassDefFoundError, e);
+ }
}
- in = new PTYInputStream(new MasterFD());
- out = new PTYOutputStream(new MasterFD(), !isWinPTY);
+ // fall through in exception case as well as normal non-conPTY
+ if (isConPTY == IS_CONPTY.CONPTY_NO) {
+
+ slaveInit = hasPTY ? openMaster(isConsole()) : null;
+
+ if (slaveInit == null) {
+ throw new IOException(Messages.Util_exception_cannotCreatePty);
+ }
+
+ inInit = new PTYInputStream(new MasterFD());
+ outInit = new PTYOutputStream(new MasterFD(), !isWinPTY);
+ }
+ slave = slaveInit;
+ in = inInit;
+ out = outInit;
}
/**
@@ -185,12 +228,17 @@
*/
public final void setTerminalSize(int width, int height) {
try {
- change_window_size(master, width, height);
- } catch (UnsatisfiedLinkError ule) {
+ if (isConPTY == IS_CONPTY.CONPTY_YES) {
+ conPTY.setTerminalSize(width, height);
+ } else {
+ change_window_size(master, width, height);
+ }
+ } catch (UnsatisfiedLinkError | IOException e) {
if (!setTerminalSizeErrorAlreadyLogged) {
setTerminalSizeErrorAlreadyLogged = true;
- CNativePlugin.log(Messages.Util_exception_cannotSetTerminalSize, ule);
+ CNativePlugin.log(Messages.Util_exception_cannotSetTerminalSize, e);
}
+
}
}
@@ -200,7 +248,9 @@
*/
public int exec_pty(Spawner spawner, String[] cmdarray, String[] envp, String dir, IChannel[] chan)
throws IOException {
- if (isWinPTY) {
+ if (isConPTY == IS_CONPTY.CONPTY_YES) {
+ return conPTY.exec(cmdarray, envp, dir);
+ } else if (isWinPTY) {
return exec2(cmdarray, envp, dir, chan, slave, master, isConsole());
} else {
return spawner.exec2(cmdarray, envp, dir, chan, slave, master, isConsole());
@@ -212,7 +262,9 @@
* @since 5.6
*/
public int waitFor(Spawner spawner, int pid) {
- if (isWinPTY) {
+ if (isConPTY == IS_CONPTY.CONPTY_YES) {
+ return conPTY.waitFor();
+ } else if (isWinPTY) {
return waitFor(master, pid);
} else {
return spawner.waitFor(pid);
@@ -236,8 +288,19 @@
static {
try {
- isWinPTY = Platform.OS_WIN32.equals(Platform.getOS());
- if (isWinPTY) {
+ boolean isWindows = Platform.OS_WIN32.equals(Platform.getOS());
+ if (!isWindows) {
+ isConPTY = IS_CONPTY.CONPTY_NO;
+ }
+ // Force conpty off by default
+ // NOTE: to invert the default, the presence of the property must be checked too, not
+ // just the getBoolean return!
+ if (!Boolean.getBoolean("org.eclipse.cdt.core.winpty_console_mode")) { //$NON-NLS-1$
+ isConPTY = IS_CONPTY.CONPTY_NO;
+ }
+
+ isWinPTY = isWindows;
+ if (isWindows) {
// When we used to build with VC++ we used DelayLoadDLLs (See Gerrit 167674 and Bug 521515) so that the winpty
// could be found. When we ported to mingw we didn't port across this feature because it was simpler to just
// manually load winpty first.
diff --git a/terminal/plugins/org.eclipse.tm.terminal.view.ui/META-INF/MANIFEST.MF b/terminal/plugins/org.eclipse.tm.terminal.view.ui/META-INF/MANIFEST.MF
index d5a95f4..a8945c5 100644
--- a/terminal/plugins/org.eclipse.tm.terminal.view.ui/META-INF/MANIFEST.MF
+++ b/terminal/plugins/org.eclipse.tm.terminal.view.ui/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
Bundle-ManifestVersion: 2
Bundle-Name: %pluginName
Bundle-SymbolicName: org.eclipse.tm.terminal.view.ui;singleton:=true
-Bundle-Version: 4.8.100.qualifier
+Bundle-Version: 4.9.0.qualifier
Bundle-Activator: org.eclipse.tm.terminal.view.ui.activator.UIPlugin
Bundle-Vendor: %providerName
Require-Bundle: org.eclipse.core.expressions;bundle-version="3.4.400",
diff --git a/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/streams/AbstractStreamsConnector.java b/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/streams/AbstractStreamsConnector.java
index f687c3c..686c706 100644
--- a/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/streams/AbstractStreamsConnector.java
+++ b/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/streams/AbstractStreamsConnector.java
@@ -160,6 +160,19 @@
@Override
protected void doDisconnect() {
+ // First let all the monitors know they are about to be closed, this allows them
+ // to suppress errors if closing one stream causes other streams to all close
+ // as a side effect.
+ if (stdInMonitor != null) {
+ stdInMonitor.disposalComing();
+ }
+ if (stdOutMonitor != null) {
+ stdOutMonitor.disposalComing();
+ }
+ if (stdErrMonitor != null) {
+ stdErrMonitor.disposalComing();
+ }
+
// Dispose the streams
if (stdInMonitor != null) {
stdInMonitor.dispose();
diff --git a/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/streams/InputStreamMonitor.java b/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/streams/InputStreamMonitor.java
index e99c24f..3713ee3 100644
--- a/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/streams/InputStreamMonitor.java
+++ b/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/streams/InputStreamMonitor.java
@@ -82,6 +82,11 @@
private int replacement;
/**
+ * @see #disposalComing()
+ */
+ private boolean disposalComing;
+
+ /**
* Constructor.
*
* @param terminalControl The parent terminal control. Must not be <code>null</code>.
@@ -154,6 +159,8 @@
if (disposed)
return;
+ disposalComing();
+
// Mark the monitor disposed
disposed = true;
@@ -250,7 +257,7 @@
}
} catch (IOException e) {
// IOException received. If this is happening when already disposed -> ignore
- if (!disposed) {
+ if (!disposed && !disposalComing) {
IStatus status = new Status(IStatus.ERROR, UIPlugin.getUniqueIdentifier(),
NLS.bind(Messages.InputStreamMonitor_error_writingToStream, e.getLocalizedMessage()),
e);
@@ -355,4 +362,13 @@
return bytes;
}
+
+ /**
+ * Notify the receiver that the stream is about to be closed. This allows the stream to suppress error messages
+ * that are side effects of the asynchronous nature of the stream closing.
+ * @since 4.9
+ */
+ public void disposalComing() {
+ disposalComing = true;
+ }
}
diff --git a/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/streams/OutputStreamMonitor.java b/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/streams/OutputStreamMonitor.java
index 1772f5d..2362160 100644
--- a/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/streams/OutputStreamMonitor.java
+++ b/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/streams/OutputStreamMonitor.java
@@ -64,6 +64,11 @@
private final ListenerList<ITerminalServiceOutputStreamMonitorListener> listeners;
/**
+ * @see #disposalComing()
+ */
+ private boolean disposalComing;
+
+ /**
* Constructor.
*
* @param terminalControl The parent terminal control. Must not be <code>null</code>.
@@ -132,6 +137,8 @@
if (disposed)
return;
+ disposalComing();
+
// Mark the monitor disposed
disposed = true;
@@ -208,7 +215,7 @@
}
} catch (IOException e) {
// IOException received. If this is happening when already disposed -> ignore
- if (!disposed) {
+ if (!disposed && !disposalComing) {
IStatus status = new Status(IStatus.ERROR, UIPlugin.getUniqueIdentifier(),
NLS.bind(Messages.OutputStreamMonitor_error_readingFromStream, e.getLocalizedMessage()), e);
UIPlugin.getDefault().getLog().log(status);
@@ -217,7 +224,7 @@
} catch (NullPointerException e) {
// killing the stream monitor while reading can cause an NPE
// when reading from the stream
- if (!disposed && thread != null) {
+ if (!disposed && thread != null && !disposalComing) {
IStatus status = new Status(IStatus.ERROR, UIPlugin.getUniqueIdentifier(),
NLS.bind(Messages.OutputStreamMonitor_error_readingFromStream, e.getLocalizedMessage()), e);
UIPlugin.getDefault().getLog().log(status);
@@ -318,4 +325,13 @@
return byteBuffer;
}
+
+ /**
+ * Notify the receiver that the stream is about to be closed. This allows the stream to suppress error messages
+ * that are side effects of the asynchronous nature of the stream closing.
+ * @since 4.9
+ */
+ public void disposalComing() {
+ disposalComing = true;
+ }
}