From e57e0b0bfacea51275332feae60614a53a95591b Mon Sep 17 00:00:00 2001 From: Cristian Adam Date: Tue, 19 Mar 2024 20:17:13 +0100 Subject: [PATCH] Terminal: Fix terminal resizing on Windows using ConPty The official ConPty API doesn't give the ability to forward "-- resizeQuirk" to "conhost.exe". Fixing this involved taking parts from the WinConPty implementation (https://github.com/microsoft/terminal/tree/main/src/ winconpty) and porting them to work inside ptyqt. Fixes: QTCREATORBUG-30007 Fixes: QTCREATORBUG-30558 Change-Id: I45e81fa167c88a85b44958eade0d85f7680e8075 Reviewed-by: Cristian Adam Reviewed-by: Marcus Tillmanns --- qt_attributions.json | 13 + src/libs/3rdparty/libptyqt/LICENSE-CONPTY | 21 + src/libs/3rdparty/libptyqt/conptyprocess.cpp | 785 ++++++++++++++++++- 3 files changed, 794 insertions(+), 25 deletions(-) create mode 100644 src/libs/3rdparty/libptyqt/LICENSE-CONPTY diff --git a/qt_attributions.json b/qt_attributions.json index 0a9e762dae1..65a92cecc88 100644 --- a/qt_attributions.json +++ b/qt_attributions.json @@ -453,6 +453,19 @@ "LicenseFile": "src/libs/3rdparty/libptyqt/LICENSE", "Copyright": "Copyright (c) 2019 Vitaly Petrov" }, + { + "Id": "ptyqt-conpty", + "Name": "Pty-Qt/ConPty", + "QDocModule": "qtcreator", + "QtParts": ["tools"], + "QtUsage": "Used for the integrated terminal.", + "Path": "src/libs/3rdparty/libptyqt", + "Description": "Major parts of conptyprocess.cpp have been ported from microsofts terminal source code and integrated into ptyqt's class.", + "Homepage": "https://github.com/microsoft/terminal", + "License": "MIT License", + "LicenseFile": "src/libs/3rdparty/libptyqt/LICENSE-CONPTY", + "Copyright": "Copyright (c) Microsoft Corporation" + }, { "Id": "libvterm", "Name": "libvterm", diff --git a/src/libs/3rdparty/libptyqt/LICENSE-CONPTY b/src/libs/3rdparty/libptyqt/LICENSE-CONPTY new file mode 100644 index 00000000000..8cb179cdb69 --- /dev/null +++ b/src/libs/3rdparty/libptyqt/LICENSE-CONPTY @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/libs/3rdparty/libptyqt/conptyprocess.cpp b/src/libs/3rdparty/libptyqt/conptyprocess.cpp index 6464dbeb347..94bb53b5832 100644 --- a/src/libs/3rdparty/libptyqt/conptyprocess.cpp +++ b/src/libs/3rdparty/libptyqt/conptyprocess.cpp @@ -10,8 +10,740 @@ #include +#ifdef QTCREATOR_PCH_H +#ifndef IN +#define IN +#endif +#ifndef OUT +#define OUT +#endif +#endif + +#include + #define READ_INTERVAL_MSEC 500 +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#define FILE_SYNCHRONOUS_IO_NONALERT 0x00000020 + +//////////////////////////////////////////////////////////////////////////////////////// +/// Ported from wil/result_macros.h + +#define FAILED_NTSTATUS(status) (((NTSTATUS)(status)) < 0) +#define SUCCEEDED_NTSTATUS(status) (((NTSTATUS)(status)) >= 0) + +#define RETURN_IF_NTSTATUS_FAILED(call) if(FAILED_NTSTATUS(call)) return E_FAIL; +#define RETURN_IF_WIN32_BOOL_FALSE(call) if((call) == FALSE) return E_FAIL; +#define RETURN_IF_NULL_ALLOC(call) if((call) == nullptr) return E_OUTOFMEMORY; +#define RETURN_IF_FAILED(call) {HRESULT hr = (call); if(hr != S_OK)return hr; } + +//! Set zero or more bitflags specified by `flags` in the variable `var`. +#define WI_SetAllFlags(var, flags) ((var) |= (flags)) +//! Set a single compile-time constant `flag` in the variable `var`. +#define WI_SetFlag(var, flag) WI_SetAllFlags(var,flag) + +//////////////////////////////////////////////////////////////////////////////////////// +/// Ported from wil +class unique_hmodule : public std::unique_ptr, decltype(&FreeLibrary)> +{ +public: + unique_hmodule() : std::unique_ptr, decltype(&FreeLibrary)>(nullptr, FreeLibrary) {} + unique_hmodule(HMODULE module) : std::unique_ptr, decltype(&FreeLibrary)>(module, FreeLibrary) {} +}; + +class unique_handle : public std::unique_ptr, decltype(&CloseHandle)> +{ +public: + unique_handle() : std::unique_ptr, decltype(&CloseHandle)>(nullptr, CloseHandle) {} + unique_handle(HANDLE module) : std::unique_ptr, decltype(&CloseHandle)>(module, CloseHandle) {} + + + class AddressOf { + public: + AddressOf(unique_handle &h) : m_h(h) {} + ~AddressOf() { + m_h.reset(m_dest); + } + + operator PHANDLE() {return &m_dest;} + + HANDLE m_dest{INVALID_HANDLE_VALUE}; + unique_handle &m_h; + }; + + AddressOf addressof() { + return AddressOf(*this); + } +}; + +class unique_process_information : public PROCESS_INFORMATION +{ +public: + unique_process_information() { + hProcess = 0; + hThread = 0; + dwProcessId = 0; + dwThreadId = 0; + } + ~unique_process_information() { + if (hProcess) + { + CloseHandle(hProcess); + } + + if (hThread) + { + CloseHandle(hThread); + } + } + + PROCESS_INFORMATION* addressof() { + return this; + } +}; + +template +class on_scope_exit { +public: + TLambda m_func; + bool m_call{true}; + on_scope_exit(TLambda &&func) : m_func(std::move(func)) {} + ~on_scope_exit() {if(m_call)m_func();} + + void release() {m_call = false;} +}; + +template +[[nodiscard]] inline auto scope_exit(TLambda&& lambda) noexcept +{ + return on_scope_exit(std::forward(lambda)); +} + +//////////////////////////////////////////////////////////////////////////////////////// + + + +class WinNTControl +{ +public: + [[nodiscard]] static NTSTATUS NtOpenFile(_Out_ PHANDLE FileHandle, + _In_ ACCESS_MASK DesiredAccess, + _In_ POBJECT_ATTRIBUTES ObjectAttributes, + _Out_ PIO_STATUS_BLOCK IoStatusBlock, + _In_ ULONG ShareAccess, + _In_ ULONG OpenOptions); + +private: + WinNTControl(); + + WinNTControl(WinNTControl const&) = delete; + void operator=(WinNTControl const&) = delete; + + static WinNTControl& GetInstance(); + + unique_hmodule const _NtDllDll; + + typedef NTSTATUS(NTAPI* PfnNtOpenFile)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, PIO_STATUS_BLOCK, ULONG, ULONG); + PfnNtOpenFile const _NtOpenFile; +}; + +WinNTControl::WinNTControl() : + // NOTE: Use LoadLibraryExW with LOAD_LIBRARY_SEARCH_SYSTEM32 flag below to avoid unneeded directory traversal. + // This has triggered CPG boot IO warnings in the past. + _NtDllDll(/*THROW_LAST_ERROR_IF_NULL*/(LoadLibraryExW(L"ntdll.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32))), + _NtOpenFile(reinterpret_cast(/*THROW_LAST_ERROR_IF_NULL*/(GetProcAddress(_NtDllDll.get(), "NtOpenFile")))) +{ +} + +// Routine Description: +// - Provides the singleton pattern for WinNT control. Stores the single instance and returns it. +// Arguments: +// - +// Return Value: +// - Reference to the single instance of NTDLL.dll wrapped methods. +WinNTControl& WinNTControl::GetInstance() +{ + static WinNTControl Instance; + return Instance; +} + +// Routine Description: +// - Provides access to the NtOpenFile method documented at: +// https://msdn.microsoft.com/en-us/library/bb432381(v=vs.85).aspx +// Arguments: +// - See definitions at MSDN +// Return Value: +// - See definitions at MSDN +[[nodiscard]] NTSTATUS WinNTControl::NtOpenFile(_Out_ PHANDLE FileHandle, + _In_ ACCESS_MASK DesiredAccess, + _In_ POBJECT_ATTRIBUTES ObjectAttributes, + _Out_ PIO_STATUS_BLOCK IoStatusBlock, + _In_ ULONG ShareAccess, + _In_ ULONG OpenOptions) +{ + return GetInstance()._NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, ShareAccess, OpenOptions); +} + +namespace DeviceHandle { + +/*++ +Routine Description: +- This routine opens a handle to the console driver. + +Arguments: +- Handle - Receives the handle. +- DeviceName - Supplies the name to be used to open the console driver. +- DesiredAccess - Supplies the desired access mask. +- Parent - Optionally supplies the parent object. +- Inheritable - Supplies a boolean indicating if the new handle is to be made inheritable. +- OpenOptions - Supplies the open options to be passed to NtOpenFile. A common + option for clients is FILE_SYNCHRONOUS_IO_NONALERT, to make the handle + synchronous. + +Return Value: +- NTSTATUS indicating if the handle was successfully created. +--*/ +[[nodiscard]] NTSTATUS +_CreateHandle( + _Out_ PHANDLE Handle, + _In_ PCWSTR DeviceName, + _In_ ACCESS_MASK DesiredAccess, + _In_opt_ HANDLE Parent, + _In_ BOOLEAN Inheritable, + _In_ ULONG OpenOptions) + +{ + ULONG Flags = OBJ_CASE_INSENSITIVE; + + if (Inheritable) + { + WI_SetFlag(Flags, OBJ_INHERIT); + } + + UNICODE_STRING Name; +#pragma warning(suppress : 26492) // const_cast is prohibited, but we can't avoid it for filling UNICODE_STRING. + Name.Buffer = const_cast(DeviceName); + //Name.Length = gsl::narrow_cast((wcslen(DeviceName) * sizeof(wchar_t))); + Name.Length = static_cast((wcslen(DeviceName) * sizeof(wchar_t))); + Name.MaximumLength = Name.Length + sizeof(wchar_t); + + OBJECT_ATTRIBUTES ObjectAttributes; +#pragma warning(suppress : 26477) // The QOS part of this macro in the define is 0. Can't fix that. + InitializeObjectAttributes(&ObjectAttributes, + &Name, + Flags, + Parent, + nullptr); + + IO_STATUS_BLOCK IoStatus; + return WinNTControl::NtOpenFile(Handle, + DesiredAccess, + &ObjectAttributes, + &IoStatus, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + OpenOptions); +} + +/*++ +Routine Description: +- This routine creates a handle to an input or output client of the given + server. No control io is sent to the server as this request must be coming + from the server itself. + +Arguments: +- Handle - Receives a handle to the new client. +- ServerHandle - Supplies a handle to the server to which to attach the + newly created client. +- Name - Supplies the name of the client object. +- Inheritable - Supplies a flag indicating if the handle must be inheritable. + +Return Value: +- NTSTATUS indicating if the client was successfully created. +--*/ +[[nodiscard]] NTSTATUS +CreateClientHandle( + _Out_ PHANDLE Handle, + _In_ HANDLE ServerHandle, + _In_ PCWSTR Name, + _In_ BOOLEAN Inheritable) +{ + return _CreateHandle(Handle, + Name, + GENERIC_WRITE | GENERIC_READ | SYNCHRONIZE, + ServerHandle, + Inheritable, + FILE_SYNCHRONOUS_IO_NONALERT); +} + +/*++ +Routine Description: +- This routine creates a new server on the driver and returns a handle to it. + +Arguments: +- Handle - Receives a handle to the new server. +- Inheritable - Supplies a flag indicating if the handle must be inheritable. + +Return Value: +- NTSTATUS indicating if the console was successfully created. +--*/ +[[nodiscard]] NTSTATUS +CreateServerHandle( + _Out_ PHANDLE Handle, + _In_ BOOLEAN Inheritable) +{ + return _CreateHandle(Handle, + L"\\Device\\ConDrv\\Server", + GENERIC_ALL, + nullptr, + Inheritable, + 0); +} + + +} // namespace DeviceHandle + +[[nodiscard]] static inline NTSTATUS CreateClientHandle(PHANDLE Handle, HANDLE ServerHandle, PCWSTR Name, BOOLEAN Inheritable) +{ + return DeviceHandle::CreateClientHandle(Handle, ServerHandle, Name, Inheritable); +} + +[[nodiscard]] static inline NTSTATUS CreateServerHandle(PHANDLE Handle, BOOLEAN Inheritable) +{ + return DeviceHandle::CreateServerHandle(Handle, Inheritable); +} + +typedef struct _PseudoConsole +{ + HANDLE hSignal; + HANDLE hPtyReference; + HANDLE hConPtyProcess; +} PseudoConsole; + +// Signals +// These are not defined publicly, but are used for controlling the conpty via +// the signal pipe. +#define PTY_SIGNAL_CLEAR_WINDOW (2u) +#define PTY_SIGNAL_RESIZE_WINDOW (8u) + +// CreatePseudoConsole Flags +// The other flag (PSEUDOCONSOLE_INHERIT_CURSOR) is actually defined in consoleapi.h in the OS repo +#ifndef PSEUDOCONSOLE_INHERIT_CURSOR +#define PSEUDOCONSOLE_INHERIT_CURSOR (0x1) +#endif +#define PSEUDOCONSOLE_RESIZE_QUIRK (0x2) +#define PSEUDOCONSOLE_WIN32_INPUT_MODE (0x4) + +static QString qSystemDirectory() +{ + static const QString result = []() -> QString { + QVarLengthArray fullPath = {}; + UINT retLen = ::GetSystemDirectoryW(fullPath.data(), MAX_PATH); + if (retLen > MAX_PATH) { + fullPath.resize(retLen); + retLen = ::GetSystemDirectoryW(fullPath.data(), retLen); + } + // in some rare cases retLen might be 0 + return QString::fromWCharArray(fullPath.constData(), int(retLen)); + }(); + return result; +} + +// Function Description: +// - Returns the path to conhost.exe as a process heap string. +static QString _InboxConsoleHostPath() +{ + return QString("\\\\?\\%1\\conhost.exe").arg(qSystemDirectory()); +} + +// Function Description: +// - Returns the path to either conhost.exe or the side-by-side OpenConsole, depending on whether this +// module is building with Windows and OpenConsole could be found. +// Return Value: +// - A pointer to permanent storage containing the path to the console host. +static const wchar_t* _ConsoleHostPath() +{ + // Use the magic of magic statics to only calculate this once. + static QString consoleHostPath = _InboxConsoleHostPath(); + return reinterpret_cast(consoleHostPath.utf16()); +} + +static bool _HandleIsValid(HANDLE h) noexcept +{ + return (h != INVALID_HANDLE_VALUE) && (h != nullptr); +} + + + +HRESULT _CreatePseudoConsole(const HANDLE hToken, + const COORD size, + const HANDLE hInput, + const HANDLE hOutput, + const DWORD dwFlags, + _Inout_ PseudoConsole* pPty) +{ + if (pPty == nullptr) + { + return E_INVALIDARG; + } + if (size.X == 0 || size.Y == 0) + { + return E_INVALIDARG; + } + + unique_handle serverHandle; + RETURN_IF_NTSTATUS_FAILED(CreateServerHandle(serverHandle.addressof(), TRUE)); + + unique_handle signalPipeConhostSide; + unique_handle signalPipeOurSide; + + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof(sa); + // Mark inheritable for signal handle when creating. It'll have the same value on the other side. + sa.bInheritHandle = FALSE; + sa.lpSecurityDescriptor = nullptr; + + RETURN_IF_WIN32_BOOL_FALSE(CreatePipe(signalPipeConhostSide.addressof(), signalPipeOurSide.addressof(), &sa, 0)); + RETURN_IF_WIN32_BOOL_FALSE(SetHandleInformation(signalPipeConhostSide.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); + + // GH4061: Ensure that the path to executable in the format is escaped so C:\Program.exe cannot collide with C:\Program Files + const wchar_t* pwszFormat = L"\"%s\" --headless %s%s%s--width %hu --height %hu --signal 0x%x --server 0x%x"; + // This is plenty of space to hold the formatted string + wchar_t cmd[MAX_PATH]{}; + const BOOL bInheritCursor = (dwFlags & PSEUDOCONSOLE_INHERIT_CURSOR) == PSEUDOCONSOLE_INHERIT_CURSOR; + const BOOL bResizeQuirk = (dwFlags & PSEUDOCONSOLE_RESIZE_QUIRK) == PSEUDOCONSOLE_RESIZE_QUIRK; + const BOOL bWin32InputMode = (dwFlags & PSEUDOCONSOLE_WIN32_INPUT_MODE) == PSEUDOCONSOLE_WIN32_INPUT_MODE; + swprintf_s(cmd, + MAX_PATH, + pwszFormat, + _ConsoleHostPath(), + bInheritCursor ? L"--inheritcursor " : L"", + bWin32InputMode ? L"--win32input " : L"", + bResizeQuirk ? L"--resizeQuirk " : L"", + size.X, + size.Y, + signalPipeConhostSide.get(), + serverHandle.get()); + + STARTUPINFOEXW siEx{ 0 }; + siEx.StartupInfo.cb = sizeof(STARTUPINFOEXW); + siEx.StartupInfo.hStdInput = hInput; + siEx.StartupInfo.hStdOutput = hOutput; + siEx.StartupInfo.hStdError = hOutput; + siEx.StartupInfo.dwFlags |= STARTF_USESTDHANDLES; + + // Only pass the handles we actually want the conhost to know about to it: + const size_t INHERITED_HANDLES_COUNT = 4; + HANDLE inheritedHandles[INHERITED_HANDLES_COUNT]; + inheritedHandles[0] = serverHandle.get(); + inheritedHandles[1] = hInput; + inheritedHandles[2] = hOutput; + inheritedHandles[3] = signalPipeConhostSide.get(); + + // Get the size of the attribute list. We need one attribute, the handle list. + SIZE_T listSize = 0; + InitializeProcThreadAttributeList(nullptr, 1, 0, &listSize); + + // I have to use a HeapAlloc here because kernelbase can't link new[] or delete[] + PPROC_THREAD_ATTRIBUTE_LIST attrList = static_cast(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, listSize)); + RETURN_IF_NULL_ALLOC(attrList); + auto attrListDelete = scope_exit([&]() noexcept { + HeapFree(GetProcessHeap(), 0, attrList); + }); + + siEx.lpAttributeList = attrList; + RETURN_IF_WIN32_BOOL_FALSE(InitializeProcThreadAttributeList(siEx.lpAttributeList, 1, 0, &listSize)); + // Set cleanup data for ProcThreadAttributeList when successful. + auto cleanupProcThreadAttribute = scope_exit([&]() noexcept { + DeleteProcThreadAttributeList(siEx.lpAttributeList); + }); + RETURN_IF_WIN32_BOOL_FALSE(UpdateProcThreadAttribute(siEx.lpAttributeList, + 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + inheritedHandles, + (INHERITED_HANDLES_COUNT * sizeof(HANDLE)), + nullptr, + nullptr)); + unique_process_information pi; + { // wow64 disabled filesystem redirection scope + if (hToken == INVALID_HANDLE_VALUE || hToken == nullptr) + { + // Call create process + RETURN_IF_WIN32_BOOL_FALSE(CreateProcessW(_ConsoleHostPath(), + cmd, + nullptr, + nullptr, + TRUE, + EXTENDED_STARTUPINFO_PRESENT, + nullptr, + nullptr, + &siEx.StartupInfo, + pi.addressof())); + } + else + { + // Call create process + RETURN_IF_WIN32_BOOL_FALSE(CreateProcessAsUserW(hToken, + _ConsoleHostPath(), + cmd, + nullptr, + nullptr, + TRUE, + EXTENDED_STARTUPINFO_PRESENT, + nullptr, + nullptr, + &siEx.StartupInfo, + pi.addressof())); + } + } + + // Move the process handle out of the PROCESS_INFORMATION into out Pseudoconsole + pPty->hConPtyProcess = pi.hProcess; + pi.hProcess = nullptr; + + RETURN_IF_NTSTATUS_FAILED(CreateClientHandle(&pPty->hPtyReference, + serverHandle.get(), + L"\\Reference", + FALSE)); + + pPty->hSignal = signalPipeOurSide.release(); + + return S_OK; +} + +// Function Description: +// - Resizes the conpty +// Arguments: +// - hSignal: A signal pipe as returned by CreateConPty. +// - size: The new dimensions of the conpty, in characters. +// Return Value: +// - S_OK if the call succeeded, else an appropriate HRESULT for failing to +// write the resize message to the pty. +HRESULT _ResizePseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const COORD size) +{ + if (pPty == nullptr || size.X < 0 || size.Y < 0) + { + return E_INVALIDARG; + } + + unsigned short signalPacket[3]; + signalPacket[0] = PTY_SIGNAL_RESIZE_WINDOW; + signalPacket[1] = size.X; + signalPacket[2] = size.Y; + + const BOOL fSuccess = WriteFile(pPty->hSignal, signalPacket, sizeof(signalPacket), nullptr, nullptr); + return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError()); +} + +// Function Description: +// - Clears the conpty +// Arguments: +// - hSignal: A signal pipe as returned by CreateConPty. +// Return Value: +// - S_OK if the call succeeded, else an appropriate HRESULT for failing to +// write the clear message to the pty. +HRESULT _ClearPseudoConsole(_In_ const PseudoConsole* const pPty) +{ + if (pPty == nullptr) + { + return E_INVALIDARG; + } + + unsigned short signalPacket[1]; + signalPacket[0] = PTY_SIGNAL_CLEAR_WINDOW; + + const BOOL fSuccess = WriteFile(pPty->hSignal, signalPacket, sizeof(signalPacket), nullptr, nullptr); + return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError()); +} + +// Function Description: +// - This closes each of the members of a PseudoConsole. It does not free the +// data associated with the PseudoConsole. This is helpful for testing, +// where we might stack allocate a PseudoConsole (instead of getting a +// HPCON via the API). +// Arguments: +// - pPty: A pointer to a PseudoConsole struct. +// Return Value: +// - +void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty) +{ + if (pPty != nullptr) + { + // See MSFT:19918626 + // First break the signal pipe - this will trigger conhost to tear itself down + if (_HandleIsValid(pPty->hSignal)) + { + CloseHandle(pPty->hSignal); + pPty->hSignal = nullptr; + } + // Then, wait on the conhost process before killing it. + // We do this to make sure the conhost finishes flushing any output it + // has yet to send before we hard kill it. + if (_HandleIsValid(pPty->hConPtyProcess)) + { + // If the conhost is already dead, then that's fine. Presumably + // it's finished flushing it's output already. + DWORD dwExit = 0; + // If GetExitCodeProcess failed, it's likely conhost is already dead + // If so, skip waiting regardless of whatever error + // GetExitCodeProcess returned. + // We'll just go straight to killing conhost. + if (GetExitCodeProcess(pPty->hConPtyProcess, &dwExit) && dwExit == STILL_ACTIVE) + { + WaitForSingleObject(pPty->hConPtyProcess, INFINITE); + } + + TerminateProcess(pPty->hConPtyProcess, 0); + CloseHandle(pPty->hConPtyProcess); + pPty->hConPtyProcess = nullptr; + } + // Then take care of the reference handle. + // TODO GH#1810: Closing the reference handle late leaves conhost thinking + // that we have an outstanding connected client. + if (_HandleIsValid(pPty->hPtyReference)) + { + CloseHandle(pPty->hPtyReference); + pPty->hPtyReference = nullptr; + } + } +} + +// Function Description: +// - This closes each of the members of a PseudoConsole, and HeapFree's the +// memory allocated to it. This should be used to cleanup any +// PseudoConsoles that were created with CreatePseudoConsole. +// Arguments: +// - pPty: A pointer to a PseudoConsole struct. +// Return Value: +// - +VOID _ClosePseudoConsole(_In_ PseudoConsole* pPty) +{ + if (pPty != nullptr) + { + _ClosePseudoConsoleMembers(pPty); + HeapFree(GetProcessHeap(), 0, pPty); + } +} + +extern "C" HRESULT ConptyCreatePseudoConsoleAsUser(_In_ HANDLE hToken, + _In_ COORD size, + _In_ HANDLE hInput, + _In_ HANDLE hOutput, + _In_ DWORD dwFlags, + _Out_ HPCON* phPC) +{ + if (phPC == nullptr) + { + return E_INVALIDARG; + } + *phPC = nullptr; + if ((!_HandleIsValid(hInput)) && (!_HandleIsValid(hOutput))) + { + return E_INVALIDARG; + } + + PseudoConsole* pPty = (PseudoConsole*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PseudoConsole)); + RETURN_IF_NULL_ALLOC(pPty); + auto cleanupPty = scope_exit([&]() noexcept { + _ClosePseudoConsole(pPty); + }); + + unique_handle duplicatedInput; + unique_handle duplicatedOutput; + RETURN_IF_WIN32_BOOL_FALSE(DuplicateHandle(GetCurrentProcess(), hInput, GetCurrentProcess(), duplicatedInput.addressof(), 0, TRUE, DUPLICATE_SAME_ACCESS)); + RETURN_IF_WIN32_BOOL_FALSE(DuplicateHandle(GetCurrentProcess(), hOutput, GetCurrentProcess(), duplicatedOutput.addressof(), 0, TRUE, DUPLICATE_SAME_ACCESS)); + + RETURN_IF_FAILED(_CreatePseudoConsole(hToken, size, duplicatedInput.get(), duplicatedOutput.get(), dwFlags, pPty)); + + *phPC = (HPCON)pPty; + cleanupPty.release(); + + return S_OK; +} + +// These functions are defined in the console l1 apiset, which is generated from +// the consoleapi.apx file in minkernel\apiset\libs\Console. + +// Function Description: +// Creates a "Pseudo-console" (conpty) with dimensions (in characters) +// provided by the `size` parameter. The caller should provide two handles: +// - `hInput` is used for writing input to the pty, encoded as UTF-8 and VT sequences. +// - `hOutput` is used for reading the output of the pty, encoded as UTF-8 and VT sequences. +// Once the call completes, `phPty` will receive a token value to identify this +// conpty object. This value should be used in conjunction with the other +// Pseudoconsole API's. +// `dwFlags` is used to specify optional behavior to the created pseudoconsole. +// The flags can be combinations of the following values: +// INHERIT_CURSOR: This will cause the created conpty to attempt to inherit the +// cursor position of the parent terminal application. This can be useful +// for applications like `ssh`, where ssh (currently running in a terminal) +// might want to create a pseudoterminal session for an child application +// and the child inherit the cursor position of ssh. +// The created conpty will immediately emit a "Device Status Request" VT +// sequence to hOutput, that should be replied to on hInput in the format +// "\x1b[;R", where `` is the row and `` is the column of the +// cursor position. +// This requires a cooperating terminal application - if a caller does not +// reply to this message, the conpty will not process any input until it +// does. Most *nix terminals and the Windows Console (after Windows 10 +// Anniversary Update) will be able to handle such a message. + +extern "C" HRESULT WINAPI ConptyCreatePseudoConsole(_In_ COORD size, + _In_ HANDLE hInput, + _In_ HANDLE hOutput, + _In_ DWORD dwFlags, + _Out_ HPCON* phPC) +{ + return ConptyCreatePseudoConsoleAsUser(INVALID_HANDLE_VALUE, size, hInput, hOutput, dwFlags, phPC); +} + +// Function Description: +// Resizes the given conpty to the specified size, in characters. +extern "C" HRESULT WINAPI ConptyResizePseudoConsole(_In_ HPCON hPC, _In_ COORD size) +{ + const PseudoConsole* const pPty = (PseudoConsole*)hPC; + HRESULT hr = pPty == nullptr ? E_INVALIDARG : S_OK; + if (SUCCEEDED(hr)) + { + hr = _ResizePseudoConsole(pPty, size); + } + return hr; +} + +// Function Description: +// - Clear the contents of the conpty buffer, leaving the cursor row at the top +// of the viewport. +// - This is used exclusively by ConPTY to support GH#1193, GH#1882. This allows +// a terminal to clear the contents of the ConPTY buffer, which is important +// if the user would like to be able to clear the terminal-side buffer. +extern "C" HRESULT WINAPI ConptyClearPseudoConsole(_In_ HPCON hPC) +{ + const PseudoConsole* const pPty = (PseudoConsole*)hPC; + HRESULT hr = pPty == nullptr ? E_INVALIDARG : S_OK; + if (SUCCEEDED(hr)) + { + hr = _ClearPseudoConsole(pPty); + } + return hr; +} + +// Function Description: +// Closes the conpty and all associated state. +// Client applications attached to the conpty will also behave as though the +// console window they were running in was closed. +// This can fail if the conhost hosting the pseudoconsole failed to be +// terminated, or if the pseudoconsole was already terminated. +extern "C" VOID WINAPI ConptyClosePseudoConsole(_In_ HPCON hPC) +{ + PseudoConsole* const pPty = (PseudoConsole*)hPC; + if (pPty != nullptr) + { + _ClosePseudoConsole(pPty); + } +} + + //ConPTY is available only on Windows 10 released after 1903 (19H1) Windows release class WindowsContext { @@ -38,30 +770,9 @@ public: bool init() { - //already initialized - if (createPseudoConsole) - return true; - - //try to load symbols from library - //if it fails -> we can't use ConPty API - HANDLE kernel32Handle = LoadLibraryExW(L"kernel32.dll", 0, 0); - - if (kernel32Handle != nullptr) - { - createPseudoConsole = (CreatePseudoConsolePtr)GetProcAddress((HMODULE)kernel32Handle, "CreatePseudoConsole"); - resizePseudoConsole = (ResizePseudoConsolePtr)GetProcAddress((HMODULE)kernel32Handle, "ResizePseudoConsole"); - closePseudoConsole = (ClosePseudoConsolePtr)GetProcAddress((HMODULE)kernel32Handle, "ClosePseudoConsole"); - if (createPseudoConsole == NULL || resizePseudoConsole == NULL || closePseudoConsole == NULL) - { - m_lastError = QString("WindowsContext/ConPty error: %1").arg("Invalid on load API functions"); - return false; - } - } - else - { - m_lastError = QString("WindowsContext/ConPty error: %1").arg("Unable to load kernel32"); - return false; - } + createPseudoConsole = (CreatePseudoConsolePtr)ConptyCreatePseudoConsole; + resizePseudoConsole = (ResizePseudoConsolePtr)ConptyResizePseudoConsole; + closePseudoConsole = (ClosePseudoConsolePtr)ConptyClosePseudoConsole; return true; } @@ -81,6 +792,30 @@ private: QString m_lastError; }; +static bool checkConHostHasResizeQuirkOption() +{ + static bool hasResizeQuirk = std::invoke([](){ + QFile f(_InboxConsoleHostPath()); + if (!f.open(QIODevice::ReadOnly)) { + qWarning() << "couldn't open conhost.exe, assuming no resizeQuirk."; + return false; + } + // Conhost.exe should be around 1 MB + if (f.size() > 5 * 1024 * 1024) { + qWarning() << "conhost.exe is > 5MB, assuming no resizeQuirk."; + return false; + } + QByteArray content = f.readAll(); + QString searchString("--resizeQuirk"); + QByteArrayView v((const char*)searchString.data(), searchString.length()*2); + bool result = content.contains(v); + if (!result) + qDebug() << "No resizeQuirk option found in conhost."; + return result; + }); + + return hasResizeQuirk; +} HRESULT ConPtyProcess::createPseudoConsoleAndPipes(HPCON* phPC, HANDLE* phPipeIn, HANDLE* phPipeOut, qint16 cols, qint16 rows) { @@ -93,7 +828,7 @@ HRESULT ConPtyProcess::createPseudoConsoleAndPipes(HPCON* phPC, HANDLE* phPipeIn CreatePipe(phPipeIn, &hPipePTYOut, NULL, 0)) { // Create the Pseudo Console of the required size, attached to the PTY-end of the pipes - hr = WindowsContext::instance().createPseudoConsole({cols, rows}, hPipePTYIn, hPipePTYOut, 0, phPC); + hr = WindowsContext::instance().createPseudoConsole({cols, rows}, hPipePTYIn, hPipePTYOut, checkConHostHasResizeQuirkOption() ? PSEUDOCONSOLE_RESIZE_QUIRK : 0, phPC); // Note: We can close the handles to the PTY-end of the pipes here // because the handles are dup'ed into the ConHost and will be released