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
This commit is contained in:
Jonah Graham 2021-05-21 14:56:58 -04:00
parent 2443cfbeff
commit 4e92239952
13 changed files with 752 additions and 19 deletions

View file

@ -10,7 +10,9 @@ Export-Package: org.eclipse.cdt.core;native=split;mandatory:=native,
org.eclipse.cdt.utils;native=split;mandatory:=native, org.eclipse.cdt.utils;native=split;mandatory:=native,
org.eclipse.cdt.utils.pty;version="5.7", org.eclipse.cdt.utils.pty;version="5.7",
org.eclipse.cdt.utils.spawner;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-ActivationPolicy: lazy
Bundle-RequiredExecutionEnvironment: JavaSE-11 Bundle-RequiredExecutionEnvironment: JavaSE-11
Automatic-Module-Name: org.eclipse.cdt.core.native Automatic-Module-Name: org.eclipse.cdt.core.native

View file

@ -16,6 +16,8 @@ package org.eclipse.cdt.internal.core.natives;
import org.eclipse.osgi.util.NLS; import org.eclipse.osgi.util.NLS;
public class Messages extends 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_cannotCreatePty;
public static String Util_exception_cannotSetTerminalSize; public static String Util_exception_cannotSetTerminalSize;
public static String Util_error_cannotRun; public static String Util_error_cannotRun;

View file

@ -13,6 +13,8 @@
# Martin Oberhuber (Wind River) - [303083] Split from CCorePluginResources.properties # 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_cannotCreatePty=Cannot create pty
Util_exception_closeError=close error Util_exception_closeError=close error
Util_exception_cannotSetTerminalSize=Setting terminal size is not supported Util_exception_cannotSetTerminalSize=Setting terminal size is not supported

View file

@ -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('"');
}
}
}

View file

@ -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$
}
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}

View file

@ -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;
}
}
}

View file

@ -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 * This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0 * are made available under the terms of the Eclipse Public License 2.0
@ -40,19 +40,36 @@ public class PTY {
} }
final Mode mode; final Mode mode;
/**
* Unused in conPTY.
* Created, but never read in winPTY.
* Important for Posix PTY.
*/
final String slave; final String slave;
final PTYInputStream in; final PTYInputStream in;
final PTYOutputStream out; final PTYOutputStream out;
/** /**
* NOTE: Field is accessed by the native layer. Do not refactor! * NOTE: Field is accessed by the native layer. Do not refactor!
* This field is NOT used by ConPTY layer.
*/ */
int master; int master;
private static boolean hasPTY; 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 isWinPTY;
private static boolean isConsoleModeSupported; private static boolean isConsoleModeSupported;
private static boolean setTerminalSizeErrorAlreadyLogged; private static boolean setTerminalSizeErrorAlreadyLogged;
private ConPTY conPTY;
/** /**
* The master fd is used on two streams. We need to wrap the fd * The master fd is used on two streams. We need to wrap the fd
@ -119,14 +136,40 @@ public class PTY {
if (isConsole() && !isConsoleModeSupported) { if (isConsole() && !isConsoleModeSupported) {
throw new IOException(Messages.Util_exception_cannotCreatePty); throw new IOException(Messages.Util_exception_cannotCreatePty);
} }
slave = hasPTY ? openMaster(isConsole()) : null; PTYInputStream inInit = null;
PTYOutputStream outInit = null;
if (slave == null) { String slaveInit = null;
throw new IOException(Messages.Util_exception_cannotCreatePty); 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()); // fall through in exception case as well as normal non-conPTY
out = new PTYOutputStream(new MasterFD(), !isWinPTY); 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 class PTY {
*/ */
public final void setTerminalSize(int width, int height) { public final void setTerminalSize(int width, int height) {
try { try {
change_window_size(master, width, height); if (isConPTY == IS_CONPTY.CONPTY_YES) {
} catch (UnsatisfiedLinkError ule) { conPTY.setTerminalSize(width, height);
} else {
change_window_size(master, width, height);
}
} catch (UnsatisfiedLinkError | IOException e) {
if (!setTerminalSizeErrorAlreadyLogged) { if (!setTerminalSizeErrorAlreadyLogged) {
setTerminalSizeErrorAlreadyLogged = true; setTerminalSizeErrorAlreadyLogged = true;
CNativePlugin.log(Messages.Util_exception_cannotSetTerminalSize, ule); CNativePlugin.log(Messages.Util_exception_cannotSetTerminalSize, e);
} }
} }
} }
@ -200,7 +248,9 @@ public class PTY {
*/ */
public int exec_pty(Spawner spawner, String[] cmdarray, String[] envp, String dir, IChannel[] chan) public int exec_pty(Spawner spawner, String[] cmdarray, String[] envp, String dir, IChannel[] chan)
throws IOException { 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()); return exec2(cmdarray, envp, dir, chan, slave, master, isConsole());
} else { } else {
return spawner.exec2(cmdarray, envp, dir, chan, slave, master, isConsole()); return spawner.exec2(cmdarray, envp, dir, chan, slave, master, isConsole());
@ -212,7 +262,9 @@ public class PTY {
* @since 5.6 * @since 5.6
*/ */
public int waitFor(Spawner spawner, int pid) { 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); return waitFor(master, pid);
} else { } else {
return spawner.waitFor(pid); return spawner.waitFor(pid);
@ -236,8 +288,19 @@ public class PTY {
static { static {
try { try {
isWinPTY = Platform.OS_WIN32.equals(Platform.getOS()); boolean isWindows = Platform.OS_WIN32.equals(Platform.getOS());
if (isWinPTY) { 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 // 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 // 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. // manually load winpty first.

View file

@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2 Bundle-ManifestVersion: 2
Bundle-Name: %pluginName Bundle-Name: %pluginName
Bundle-SymbolicName: org.eclipse.tm.terminal.view.ui;singleton:=true 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-Activator: org.eclipse.tm.terminal.view.ui.activator.UIPlugin
Bundle-Vendor: %providerName Bundle-Vendor: %providerName
Require-Bundle: org.eclipse.core.expressions;bundle-version="3.4.400", Require-Bundle: org.eclipse.core.expressions;bundle-version="3.4.400",

View file

@ -160,6 +160,19 @@ public abstract class AbstractStreamsConnector extends TerminalConnectorImpl {
@Override @Override
protected void doDisconnect() { 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 // Dispose the streams
if (stdInMonitor != null) { if (stdInMonitor != null) {
stdInMonitor.dispose(); stdInMonitor.dispose();

View file

@ -81,6 +81,11 @@ public class InputStreamMonitor extends OutputStream implements IDisposable {
private int replacement; private int replacement;
/**
* @see #disposalComing()
*/
private boolean disposalComing;
/** /**
* Constructor. * Constructor.
* *
@ -154,6 +159,8 @@ public class InputStreamMonitor extends OutputStream implements IDisposable {
if (disposed) if (disposed)
return; return;
disposalComing();
// Mark the monitor disposed // Mark the monitor disposed
disposed = true; disposed = true;
@ -250,7 +257,7 @@ public class InputStreamMonitor extends OutputStream implements IDisposable {
} }
} catch (IOException e) { } catch (IOException e) {
// IOException received. If this is happening when already disposed -> ignore // IOException received. If this is happening when already disposed -> ignore
if (!disposed) { if (!disposed && !disposalComing) {
IStatus status = new Status(IStatus.ERROR, UIPlugin.getUniqueIdentifier(), IStatus status = new Status(IStatus.ERROR, UIPlugin.getUniqueIdentifier(),
NLS.bind(Messages.InputStreamMonitor_error_writingToStream, e.getLocalizedMessage()), NLS.bind(Messages.InputStreamMonitor_error_writingToStream, e.getLocalizedMessage()),
e); e);
@ -355,4 +362,13 @@ public class InputStreamMonitor extends OutputStream implements IDisposable {
return bytes; 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;
}
} }

View file

@ -63,6 +63,11 @@ public class OutputStreamMonitor implements IDisposable {
// The list of registered listener // The list of registered listener
private final ListenerList<ITerminalServiceOutputStreamMonitorListener> listeners; private final ListenerList<ITerminalServiceOutputStreamMonitorListener> listeners;
/**
* @see #disposalComing()
*/
private boolean disposalComing;
/** /**
* Constructor. * Constructor.
* *
@ -132,6 +137,8 @@ public class OutputStreamMonitor implements IDisposable {
if (disposed) if (disposed)
return; return;
disposalComing();
// Mark the monitor disposed // Mark the monitor disposed
disposed = true; disposed = true;
@ -208,7 +215,7 @@ public class OutputStreamMonitor implements IDisposable {
} }
} catch (IOException e) { } catch (IOException e) {
// IOException received. If this is happening when already disposed -> ignore // IOException received. If this is happening when already disposed -> ignore
if (!disposed) { if (!disposed && !disposalComing) {
IStatus status = new Status(IStatus.ERROR, UIPlugin.getUniqueIdentifier(), IStatus status = new Status(IStatus.ERROR, UIPlugin.getUniqueIdentifier(),
NLS.bind(Messages.OutputStreamMonitor_error_readingFromStream, e.getLocalizedMessage()), e); NLS.bind(Messages.OutputStreamMonitor_error_readingFromStream, e.getLocalizedMessage()), e);
UIPlugin.getDefault().getLog().log(status); UIPlugin.getDefault().getLog().log(status);
@ -217,7 +224,7 @@ public class OutputStreamMonitor implements IDisposable {
} catch (NullPointerException e) { } catch (NullPointerException e) {
// killing the stream monitor while reading can cause an NPE // killing the stream monitor while reading can cause an NPE
// when reading from the stream // when reading from the stream
if (!disposed && thread != null) { if (!disposed && thread != null && !disposalComing) {
IStatus status = new Status(IStatus.ERROR, UIPlugin.getUniqueIdentifier(), IStatus status = new Status(IStatus.ERROR, UIPlugin.getUniqueIdentifier(),
NLS.bind(Messages.OutputStreamMonitor_error_readingFromStream, e.getLocalizedMessage()), e); NLS.bind(Messages.OutputStreamMonitor_error_readingFromStream, e.getLocalizedMessage()), e);
UIPlugin.getDefault().getLog().log(status); UIPlugin.getDefault().getLog().log(status);
@ -318,4 +325,13 @@ public class OutputStreamMonitor implements IDisposable {
return byteBuffer; 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;
}
} }