// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "PtySignalInputThread.hpp" #include "output.h" #include "handle.h" #include "_stream.h" #include "../interactivity/inc/ServiceLocator.hpp" using namespace Microsoft::Console; using namespace Microsoft::Console::Interactivity; using namespace Microsoft::Console::VirtualTerminal; // Constructor Description: // - Creates the PTY Signal Input Thread. // Arguments: // - hPipe - a handle to the file representing the read end of the VT pipe. PtySignalInputThread::PtySignalInputThread(wil::unique_hfile hPipe) : _hFile{ std::move(hPipe) }, _hThread{}, _api{ ServiceLocator::LocateGlobals().getConsoleInformation() }, _dwThreadId{ 0 }, _consoleConnected{ false } { THROW_HR_IF(E_HANDLE, _hFile.get() == INVALID_HANDLE_VALUE); } PtySignalInputThread::~PtySignalInputThread() { // Manually terminate our thread during unittesting. Otherwise, the test // will finish, but TAEF will not manually kill the test. #ifdef UNIT_TESTING TerminateThread(_hThread.get(), 0); #endif } // Function Description: // - Static function used for initializing an instance's ThreadProc. // Arguments: // - lpParameter - A pointer to the PtySignalInputTHread instance that should be called. // Return Value: // - The return value of the underlying instance's _InputThread DWORD WINAPI PtySignalInputThread::StaticThreadProc(_In_ LPVOID lpParameter) { const auto pInstance = reinterpret_cast(lpParameter); return pInstance->_InputThread(); } // Method Description: // - Tell us that there's a client attached to the console, so we can actually // do something with the messages we receive now. Before this is set, there // is no guarantee that a client has attached, so most parts of the console // (in and screen buffers) haven't yet been initialized. // - NOTE: Call under LockConsole() to ensure other threads have an opportunity // to set early-work state. // - We need to do this specifically on the thread with the message pump. If the // window is created on another thread, then the window won't have a message // pump associated with it, and a DPI change in the connected terminal could // end up HANGING THE CONPTY (for example). // Arguments: // - // Return Value: // - void PtySignalInputThread::ConnectConsole() noexcept { _consoleConnected = true; if (_earlyResize) { _DoResizeWindow(*_earlyResize); } if (_initialShowHide) { _DoShowHide(*_initialShowHide); } } // Method Description: // - Create our pseudo window. We're doing this here, instead of in // ConnectConsole, because the window is created in // ConsoleInputThreadProcWin32, before ConnectConsole is first called. Doing // this here ensures that the window is first created with the initial owner // set up (if so specified). // - Refer to GH#13066 for details. void PtySignalInputThread::CreatePseudoWindow() { ServiceLocator::LocatePseudoWindow(); } // Method Description: // - The ThreadProc for the PTY Signal Input Thread. // Return Value: // - S_OK if the thread runs to completion. // - Otherwise it may cause an application termination and never return. [[nodiscard]] HRESULT PtySignalInputThread::_InputThread() noexcept try { const auto shutdown = wil::scope_exit([this]() { _Shutdown(); }); for (;;) { PtySignal signalId; if (!_GetData(&signalId, sizeof(signalId))) { return S_OK; } switch (signalId) { case PtySignal::ShowHideWindow: { ShowHideData msg = { 0 }; if (!_GetData(&msg, sizeof(msg))) { return S_OK; } _DoShowHide(msg); break; } case PtySignal::ClearBuffer: { ClearBufferData msg = { 0 }; if (!_GetData(&msg, sizeof(msg))) { return S_OK; } _DoClearBuffer(msg.keepCursorRow != 0); break; } case PtySignal::ResizeWindow: { ResizeWindowData resizeMsg = { 0 }; if (!_GetData(&resizeMsg, sizeof(resizeMsg))) { return S_OK; } _DoResizeWindow(resizeMsg); break; } case PtySignal::SetParent: { SetParentData reparentMessage = { 0 }; if (!_GetData(&reparentMessage, sizeof(reparentMessage))) { return S_OK; } _DoSetWindowParent(reparentMessage); break; } default: THROW_HR(E_UNEXPECTED); } } } CATCH_LOG_RETURN_HR(S_OK) // Method Description: // - Dispatches a resize window message to the rest of the console code // Arguments: // - data - Packet information containing width/height (size) information // Return Value: // - void PtySignalInputThread::_DoResizeWindow(const ResizeWindowData& data) { LockConsole(); auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); // If the client app hasn't yet connected, stash the new size in the launchArgs. // We'll later use the value in launchArgs to set up the console buffer // We must be under lock here to ensure that someone else doesn't come in // and set with `ConnectConsole` while we're looking and modifying this. if (!_consoleConnected) { _earlyResize = data; return; } _api.ResizeWindow(data.sx, data.sy); } void PtySignalInputThread::_DoClearBuffer(const bool keepCursorRow) const { LockConsole(); auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); // If the client app hasn't yet connected, stash the new size in the launchArgs. // We'll later use the value in launchArgs to set up the console buffer // We must be under lock here to ensure that someone else doesn't come in // and set with `ConnectConsole` while we're looking and modifying this. if (!_consoleConnected) { return; } auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& screenInfo = gci.GetActiveOutputBuffer(); auto& tb = screenInfo.GetTextBuffer(); const auto cursor = tb.GetCursor().GetPosition(); tb.ClearScrollback(cursor.y, keepCursorRow ? 1 : 0); tb.GetCursor().SetPosition({ keepCursorRow ? cursor.x : 0, 0 }); } void PtySignalInputThread::_DoShowHide(const ShowHideData& data) { LockConsole(); auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); // If the client app hasn't yet connected, stash our initial // visibility for when we do. We default to not being visible - if a // terminal wants the ConPTY windows to start "visible", then they // should send a ShowHidePseudoConsole(..., true) to tell us to // initially be visible. // // Notably, if they don't, then a ShowWindow(SW_HIDE) on the ConPTY // HWND will initially do _nothing_, because the OS will think that // the window is already hidden. if (!_consoleConnected) { _initialShowHide = data; return; } ServiceLocator::SetPseudoWindowVisibility(data.show); } // Method Description: // - Update the owner of the pseudo-window we're using for the conpty HWND. This // allows to mark the pseudoconsole windows as "owner" by the terminal HWND // that's actually hosting them. // - Refer to GH#2988 // Arguments: // - data - Packet information containing owner HWND information // Return Value: // - void PtySignalInputThread::_DoSetWindowParent(const SetParentData& data) { LockConsole(); auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); ServiceLocator::SetPseudoWindowOwner(reinterpret_cast(data.handle)); } // Method Description: // - Retrieves bytes from the file stream and exits or throws errors should the pipe state // be compromised. // Arguments: // - pBuffer - Buffer to fill with data. // - cbBuffer - Count of bytes in the given buffer. // Return Value: // - True if data was retrieved successfully. False otherwise. [[nodiscard]] bool PtySignalInputThread::_GetData(_Out_writes_bytes_(cbBuffer) void* const pBuffer, const DWORD cbBuffer) { if (!_hFile) { return false; } DWORD dwRead = 0; if (FALSE == ReadFile(_hFile.get(), pBuffer, cbBuffer, &dwRead, nullptr)) { if (const auto err = GetLastError(); err != ERROR_BROKEN_PIPE) { LOG_WIN32(err); } _hFile.reset(); return false; } if (dwRead != cbBuffer) { return false; } return true; } // Method Description: // - Starts the PTY Signal input thread. [[nodiscard]] HRESULT PtySignalInputThread::Start() noexcept { RETURN_LAST_ERROR_IF(!_hFile); HANDLE hThread = nullptr; // 0 is the right value, https://blogs.msdn.microsoft.com/oldnewthing/20040223-00/?p=40503 DWORD dwThreadId = 0; hThread = CreateThread(nullptr, 0, PtySignalInputThread::StaticThreadProc, this, 0, &dwThreadId); RETURN_LAST_ERROR_IF_NULL(hThread); _hThread.reset(hThread); _dwThreadId = dwThreadId; LOG_IF_FAILED(SetThreadDescription(hThread, L"ConPTY Signal Handler Thread")); return S_OK; } // Method Description: // - Perform a shutdown of the console. This happens when the signal pipe is // broken, which means either the parent terminal process has died, or they // called ClosePseudoConsole. // Arguments: // - // Return Value: // - void PtySignalInputThread::_Shutdown() { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.GetVtIo()->SendCloseEvent(); }