Enable the Terminal to tell ConPTY who the owner is (#12526)

## Window shenanigans, part the first:

This PR enables terminals to tell ConPTY what the owning window for the
pseudo window should be. This allows thigs like MessageBoxes created by
console applications to work. It also enables console apps to use
`GetAncestor(GetConsoleWindow(), GA_ROOT)`  to get directly at the HWND
of the Terminal (but _don't please_).

This is tested with our internal partners and seems to work for their
scenario. 

See #2988, #12799, #12515, #12570.

## PR Checklist
This is 1/3 of #2988.
This commit is contained in:
Mike Griese 2022-04-12 17:44:01 -05:00 committed by GitHub
parent 13fb1f58d0
commit 26d67d9c0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 259 additions and 15 deletions

View File

@ -145,6 +145,7 @@ REGCLS
RETURNCMD
rfind
roundf
ROOTOWNER
RSHIFT
SACL
schandle

View File

@ -60,6 +60,12 @@ namespace winrt::TerminalApp::implementation
// Method Description:
// - implements the IInitializeWithWindow interface from shobjidl_core.
// - We're going to use this HWND as the owner for the ConPTY windows, via
// ConptyConnection::ReparentWindow. We need this for applications that
// call GetConsoleWindow, and attempt to open a MessageBox for the
// console. By marking the conpty windows as owned by the Terminal HWND,
// the message box will be owned by the Terminal window as well.
// - see GH#2988
HRESULT TerminalPage::Initialize(HWND hwnd)
{
_hostingHwnd = hwnd;
@ -2409,6 +2415,11 @@ namespace winrt::TerminalApp::implementation
// create here.
// TermControl will copy the settings out of the settings passed to it.
TermControl term{ settings.DefaultSettings(), settings.UnfocusedSettings(), connection };
if (_hostingHwnd.has_value())
{
term.OwningHwnd(reinterpret_cast<uint64_t>(*_hostingHwnd));
}
return term;
}

View File

@ -310,6 +310,12 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
}
THROW_IF_FAILED(_CreatePseudoConsoleAndPipes(dimensions, flags, &_inPipe, &_outPipe, &_hPC));
if (_initialParentHwnd != 0)
{
THROW_IF_FAILED(ConptyReparentPseudoConsole(_hPC.get(), reinterpret_cast<HWND>(_initialParentHwnd)));
}
THROW_IF_FAILED(_LaunchAttachedClient());
}
// But if it was an inbound handoff... attempt to synchronize the size of it with what our connection
@ -327,6 +333,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance));
THROW_IF_FAILED(ConptyResizePseudoConsole(_hPC.get(), dimensions));
THROW_IF_FAILED(ConptyReparentPseudoConsole(_hPC.get(), reinterpret_cast<HWND>(_initialParentHwnd)));
}
_startTime = std::chrono::high_resolution_clock::now();
@ -482,6 +489,22 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
}
}
void ConptyConnection::ReparentWindow(const uint64_t newParent)
{
// If we haven't started connecting at all, stash this HWND to use once we have started.
if (!_isStateAtOrBeyond(ConnectionState::Connecting))
{
_initialParentHwnd = newParent;
}
// Otherwise, just inform the conpty of the new owner window handle.
// This shouldn't be hittable until GH#5000 / GH#1256, when it's
// possible to reparent terminals to different windows.
else if (_isConnected())
{
THROW_IF_FAILED(ConptyReparentPseudoConsole(_hPC.get(), reinterpret_cast<HWND>(newParent)));
}
}
void ConptyConnection::Close() noexcept
try
{

View File

@ -35,6 +35,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
void Resize(uint32_t rows, uint32_t columns);
void Close() noexcept;
void ClearBuffer();
void ReparentWindow(const uint64_t newParent);
winrt::guid Guid() const noexcept;
winrt::hstring Commandline() const;
@ -65,6 +66,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
uint32_t _initialRows{};
uint32_t _initialCols{};
uint64_t _initialParentHwnd{ 0 };
hstring _commandline{};
hstring _startingDirectory{};
hstring _startingTitle{};

View File

@ -13,6 +13,7 @@ namespace Microsoft.Terminal.TerminalConnection
Guid Guid { get; };
String Commandline { get; };
void ClearBuffer();
void ReparentWindow(UInt64 newParent);
static event NewConnectionHandler NewConnection;
static void StartInboundListener();

View File

@ -262,6 +262,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation
const auto height = vp.Height();
_connection.Resize(height, width);
if (_OwningHwnd != 0)
{
if (auto conpty{ _connection.try_as<TerminalConnection::ConptyConnection>() })
{
conpty.ReparentWindow(_OwningHwnd);
}
}
// Override the default width and height to match the size of the swapChainPanel
_settings->InitialCols(width);
_settings->InitialRows(height);

View File

@ -168,6 +168,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void AdjustOpacity(const double opacity, const bool relative);
// TODO:GH#1256 - When a tab can be torn out or otherwise reparented to
// another window, this value will need a custom setter, so that we can
// also update the connection.
WINRT_PROPERTY(uint64_t, OwningHwnd, 0);
RUNTIME_SETTING(double, Opacity, _settings->Opacity());
RUNTIME_SETTING(bool, UseAcrylic, _settings->UseAcrylic());

View File

@ -24,5 +24,8 @@ namespace Microsoft.Terminal.Control
Microsoft.Terminal.TerminalConnection.ConnectionState ConnectionState { get; };
Microsoft.Terminal.Core.Scheme ColorScheme { get; set; };
UInt64 OwningHwnd;
};
}

View File

@ -2794,4 +2794,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation
}
}
void TermControl::OwningHwnd(uint64_t owner)
{
_core.OwningHwnd(owner);
}
uint64_t TermControl::OwningHwnd()
{
return _core.OwningHwnd();
}
}

View File

@ -59,6 +59,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
bool BracketedPasteEnabled() const noexcept;
double BackgroundOpacity() const;
uint64_t OwningHwnd();
void OwningHwnd(uint64_t owner);
#pragma endregion
void ScrollViewport(int viewTop);

View File

@ -41,6 +41,11 @@ IslandWindow::~IslandWindow()
_source.Close();
}
HWND IslandWindow::GetInteropHandle() const
{
return _interopWindowHandle;
}
// Method Description:
// - Create the actual window that we'll use for the application.
// Arguments:

View File

@ -23,6 +23,7 @@ public:
virtual void MakeWindow() noexcept;
void Close();
virtual void OnSize(const UINT width, const UINT height);
HWND GetInteropHandle() const;
[[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override;
void OnResize(const UINT width, const UINT height) override;

View File

@ -56,6 +56,10 @@ DWORD WINAPI PtySignalInputThread::StaticThreadProc(_In_ LPVOID lpParameter)
// (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:
// - <none>
// Return Value:
@ -67,6 +71,12 @@ void PtySignalInputThread::ConnectConsole() noexcept
{
_DoResizeWindow(*_earlyResize);
}
// If we were given a owner HWND, then manually start the pseudo window now.
if (_earlyReparent)
{
_DoSetWindowParent(*_earlyReparent);
}
}
// Method Description:
@ -119,6 +129,28 @@ void PtySignalInputThread::ConnectConsole() noexcept
break;
}
case PtySignal::SetParent:
{
SetParentData reparentMessage = { 0 };
_GetData(&reparentMessage, sizeof(reparentMessage));
LockConsole();
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
// If the client app hasn't yet connected, stash the new owner.
// We'll later (PtySignalInputThread::ConnectConsole) use the value
// to set up the owner of the conpty window.
if (!_consoleConnected)
{
_earlyReparent = reparentMessage;
}
else
{
_DoSetWindowParent(reparentMessage);
}
break;
}
default:
{
THROW_HR(E_UNEXPECTED);
@ -147,6 +179,20 @@ void PtySignalInputThread::_DoClearBuffer()
_pConApi->ClearBuffer();
}
// 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:
// - <none>
void PtySignalInputThread::_DoSetWindowParent(const SetParentData& data)
{
_pConApi->ReparentWindow(data.handle);
}
// Method Description:
// - Retrieves bytes from the file stream and exits or throws errors should the pipe state
// be compromised.

View File

@ -41,6 +41,7 @@ namespace Microsoft::Console
enum class PtySignal : unsigned short
{
ClearBuffer = 2,
SetParent = 3,
ResizeWindow = 8
};
@ -49,10 +50,15 @@ namespace Microsoft::Console
unsigned short sx;
unsigned short sy;
};
struct SetParentData
{
uint64_t handle;
};
[[nodiscard]] HRESULT _InputThread();
bool _GetData(_Out_writes_bytes_(cbBuffer) void* const pBuffer, const DWORD cbBuffer);
void _DoResizeWindow(const ResizeWindowData& data);
void _DoSetWindowParent(const SetParentData& data);
void _DoClearBuffer();
void _Shutdown();
@ -62,5 +68,8 @@ namespace Microsoft::Console
bool _consoleConnected;
std::optional<ResizeWindowData> _earlyResize;
std::unique_ptr<Microsoft::Console::VirtualTerminal::ConGetSet> _pConApi;
public:
std::optional<SetParentData> _earlyReparent;
};
}

View File

@ -296,6 +296,11 @@ bool VtIo::IsUsingVt() const
if (_pPtySignalInputThread)
{
// IMPORTANT! Start the pseudo window on this thread. This thread has a
// message pump. If you DON'T, then a DPI change in the owning hwnd will
// cause us to get a dpi change as well, which we'll never deque and
// handle, effectively HANGING THE OWNER HWND.
//
// Let the signal thread know that the console is connected
_pPtySignalInputThread->ConnectConsole();
}

View File

@ -892,3 +892,17 @@ void ConhostInternalGetSet::UpdateSoftFont(const gsl::span<const uint16_t> bitPa
pRender->UpdateSoftFont(bitPattern, cellSize, centeringHint);
}
}
void ConhostInternalGetSet::ReparentWindow(const uint64_t handle)
{
// This will initialize s_interactivityFactory for us. It will also
// conveniently return 0 when we're on OneCore.
//
// If the window hasn't been created yet, by some other call to
// LocatePseudoWindow, then this will also initialize the owner of the
// window.
if (const auto psuedoHwnd{ ServiceLocator::LocatePseudoWindow(reinterpret_cast<HWND>(handle)) })
{
LOG_LAST_ERROR_IF_NULL(::SetParent(psuedoHwnd, reinterpret_cast<HWND>(handle)));
}
}

View File

@ -116,6 +116,8 @@ public:
const SIZE cellSize,
const size_t centeringHint) override;
void ReparentWindow(const uint64_t handle);
private:
void _modifyLines(const size_t count, const bool insert);

View File

@ -25,6 +25,8 @@ HRESULT WINAPI ConptyResizePseudoConsole(HPCON hPC, COORD size);
HRESULT WINAPI ConptyClearPseudoConsole(HPCON hPC);
HRESULT WINAPI ConptyReparentPseudoConsole(HPCON hPC, HWND newParent);
VOID WINAPI ConptyClosePseudoConsole(HPCON hPC);
HRESULT WINAPI ConptyPackPseudoConsole(HANDLE hServerProcess, HANDLE hRef, HANDLE hSignal, HPCON* phPC);

View File

@ -289,14 +289,15 @@ using namespace Microsoft::Console::Interactivity;
// that GetConsoleWindow returns a real value.
// Arguments:
// - hwnd: Receives the value of the newly created window's HWND.
// - owner: the HWND that should be the initial owner of the pseudo window.
// Return Value:
// - STATUS_SUCCESS on success, otherwise an appropriate error.
[[nodiscard]] NTSTATUS InteractivityFactory::CreatePseudoWindow(HWND& hwnd)
[[nodiscard]] NTSTATUS InteractivityFactory::CreatePseudoWindow(HWND& hwnd, const HWND owner)
{
hwnd = nullptr;
ApiLevel level;
NTSTATUS status = ApiDetector::DetectNtUserWindow(&level);
;
if (NT_SUCCESS(status))
{
try
@ -306,19 +307,45 @@ using namespace Microsoft::Console::Interactivity;
switch (level)
{
case ApiLevel::Win32:
{
pseudoClass.lpszClassName = PSEUDO_WINDOW_CLASS;
pseudoClass.lpfnWndProc = DefWindowProc;
RegisterClass(&pseudoClass);
// Attempt to create window
hwnd = CreateWindowExW(
0, PSEUDO_WINDOW_CLASS, nullptr, WS_OVERLAPPEDWINDOW, 0, 0, 0, 0, HWND_DESKTOP, nullptr, nullptr, nullptr);
// When merging with #12515, we're going to need to adjust these styles.
//
// Note that because we're not specifying WS_CHILD, this window
// will become an _owned_ window, not a _child_ window. This is
// important - child windows report their position as relative
// to their parent window, while owned windows are still
// relative to the desktop. (there are other subtleties as well
// as far as the difference between parent/child and owner/owned
// windows). Evan K said we should do it this way, and he
// definitely knows.
const auto windowStyle = WS_OVERLAPPEDWINDOW;
// Attempt to create window.
hwnd = CreateWindowExW(0,
PSEUDO_WINDOW_CLASS,
nullptr,
windowStyle,
0,
0,
0,
0,
owner,
nullptr,
nullptr,
nullptr);
if (hwnd == nullptr)
{
DWORD const gle = GetLastError();
status = NTSTATUS_FROM_WIN32(gle);
}
break;
break;
}
#ifdef BUILD_ONECORE_INTERACTIVITY
case ApiLevel::OneCore:
hwnd = 0;

View File

@ -26,6 +26,6 @@ namespace Microsoft::Console::Interactivity
[[nodiscard]] NTSTATUS CreateAccessibilityNotifier(_Inout_ std::unique_ptr<IAccessibilityNotifier>& notifier);
[[nodiscard]] NTSTATUS CreateSystemConfigurationProvider(_Inout_ std::unique_ptr<ISystemConfigurationProvider>& provider);
[[nodiscard]] NTSTATUS CreatePseudoWindow(HWND& hwnd);
[[nodiscard]] NTSTATUS CreatePseudoWindow(HWND& hwnd, const HWND owner);
};
}

View File

@ -288,10 +288,11 @@ Globals& ServiceLocator::LocateGlobals()
// Method Description:
// - Retrieves the pseudo console window, or attempts to instantiate one.
// Arguments:
// - <none>
// - owner: (defaults to 0 `HWND_DESKTOP`) the HWND that should be the initial
// owner of the pseudo window.
// Return Value:
// - a reference to the pseudoconsole window.
HWND ServiceLocator::LocatePseudoWindow()
HWND ServiceLocator::LocatePseudoWindow(const HWND owner)
{
NTSTATUS status = STATUS_SUCCESS;
if (!s_pseudoWindowInitialized)
@ -304,7 +305,7 @@ HWND ServiceLocator::LocatePseudoWindow()
if (NT_SUCCESS(status))
{
HWND hwnd;
status = s_interactivityFactory->CreatePseudoWindow(hwnd);
status = s_interactivityFactory->CreatePseudoWindow(hwnd, owner);
s_pseudoWindow.reset(hwnd);
}
s_pseudoWindowInitialized = true;
@ -315,8 +316,6 @@ HWND ServiceLocator::LocatePseudoWindow()
#pragma endregion
#pragma endregion
#pragma region Private Methods
[[nodiscard]] NTSTATUS ServiceLocator::LoadInteractivityFactory()

View File

@ -40,7 +40,7 @@ namespace Microsoft::Console::Interactivity
[[nodiscard]] virtual NTSTATUS CreateAccessibilityNotifier(_Inout_ std::unique_ptr<IAccessibilityNotifier>& notifier) = 0;
[[nodiscard]] virtual NTSTATUS CreateSystemConfigurationProvider(_Inout_ std::unique_ptr<ISystemConfigurationProvider>& provider) = 0;
[[nodiscard]] virtual NTSTATUS CreatePseudoWindow(HWND& hwnd) = 0;
[[nodiscard]] virtual NTSTATUS CreatePseudoWindow(HWND& hwnd, const HWND owner) = 0;
};
inline IInteractivityFactory::~IInteractivityFactory() {}

View File

@ -84,7 +84,7 @@ namespace Microsoft::Console::Interactivity
static Globals& LocateGlobals();
static HWND LocatePseudoWindow();
static HWND LocatePseudoWindow(const HWND owner = 0 /*HWND_DESKTOP*/);
protected:
ServiceLocator(ServiceLocator const&) = delete;

View File

@ -111,5 +111,7 @@ namespace Microsoft::Console::VirtualTerminal
virtual void UpdateSoftFont(const gsl::span<const uint16_t> bitPattern,
const SIZE cellSize,
const size_t centeringHint) = 0;
virtual void ReparentWindow(const uint64_t handle) = 0;
};
}

View File

@ -422,6 +422,11 @@ public:
VERIFY_ARE_EQUAL(_expectedCellSize.cy, cellSize.cy);
}
void ReparentWindow(const uint64_t /*handle*/)
{
Log::Comment(L"ReparentWindow MOCK called...");
}
void PrepData()
{
PrepData(CursorDirection::UP); // if called like this, the cursor direction doesn't matter.

View File

@ -6,7 +6,7 @@
<RootNamespace>Scratch</RootNamespace>
<ProjectName>Scratch</ProjectName>
<TargetName>Scratch</TargetName>
<ConfigurationType>Application</ConfigurationType>
<ConfigurationType>Application</ConfigurationType>
</PropertyGroup>
<Import Project="..\..\common.build.pre.props" />
<ItemGroup>
@ -21,6 +21,14 @@
<SubSystem>Console</SubSystem>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<!-- subsume fmt, one of our dependencies, into contypes. -->
<ProjectReference Include="..\..\dep\fmt\fmt.vcxproj">
<Project>{6bae5851-50d5-4934-8d5e-30361a8a40f3}</Project>
</ProjectReference>
</ItemGroup>
<!-- Careful reordering these. Some default props (contained in these files) are order sensitive. -->
<Import Project="..\..\common.build.post.props" />
<Import Project="..\..\common.build.tests.props" />

View File

@ -254,6 +254,39 @@ HRESULT _ClearPseudoConsole(_In_ const PseudoConsole* const pPty)
return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError());
}
// Function Description:
// - Sends a message to the pseudoconsole informing it that it should use the
// given window handle as the owner for the conpty's pseudo window. This
// allows the response given to GetConsoleWindow() to be a HWND that's owned
// by the actual hosting terminal's HWND.
// Arguments:
// - pPty: A pointer to a PseudoConsole struct.
// - newParent: The new owning window
// Return Value:
// - S_OK if the call succeeded, else an appropriate HRESULT for failing to
// write the resize message to the pty.
#pragma warning(suppress : 26461)
// an HWND is technically a void*, but that confuses static analysis here.
HRESULT _ReparentPseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const HWND newParent)
{
if (pPty == nullptr)
{
return E_INVALIDARG;
}
// sneaky way to pack a short and a uint64_t in a relatively literal way.
#pragma pack(push, 1)
struct _signal
{
const unsigned short id;
const uint64_t hwnd;
} data{ PTY_SIGNAL_REPARENT_WINDOW, (uint64_t)(newParent) };
#pragma pack(pop)
const BOOL fSuccess = WriteFile(pPty->hSignal, &data, sizeof(data), 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,
@ -425,6 +458,23 @@ extern "C" HRESULT WINAPI ConptyClearPseudoConsole(_In_ HPCON hPC)
return hr;
}
// Function Description:
// - Sends a message to the pseudoconsole informing it that it should use the
// given window handle as the owner for the conpty's pseudo window. This
// allows the response given to GetConsoleWindow() to be a HWND that's owned
// by the actual hosting terminal's HWND.
// - Used to support GH#2988
extern "C" HRESULT WINAPI ConptyReparentPseudoConsole(_In_ HPCON hPC, HWND newParent)
{
const PseudoConsole* const pPty = (PseudoConsole*)hPC;
HRESULT hr = pPty == nullptr ? E_INVALIDARG : S_OK;
if (SUCCEEDED(hr))
{
hr = _ReparentPseudoConsole(pPty, newParent);
}
return hr;
}
// Function Description:
// Closes the conpty and all associated state.
// Client applications attached to the conpty will also behave as though the

View File

@ -18,6 +18,7 @@ typedef struct _PseudoConsole
// 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_REPARENT_WINDOW (3u)
#define PTY_SIGNAL_RESIZE_WINDOW (8u)
// CreatePseudoConsole Flags
@ -37,6 +38,7 @@ HRESULT _CreatePseudoConsole(const HANDLE hToken,
HRESULT _ResizePseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const COORD size);
HRESULT _ClearPseudoConsole(_In_ const PseudoConsole* const pPty);
HRESULT _ReparentPseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const HWND newParent);
void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty);
VOID _ClosePseudoConsole(_In_ PseudoConsole* pPty);