Skip to content

Commit 77215d9

Browse files
authored
Fix ShowWindow(GetConsoleWindow()) (#13118)
A bad merge, that actually revealed a horrible bug. There was a secret conflict between the code in #12526 and #12515. 69b77ca was a bad merge that hid just how bad the issue was. Fixing the one line `nullptr`->`this` in `InteractivityFactory` resulted in a window that would flash uncontrollably, as it minimized and restored itself in a loop. Great. This can seemingly be fixed by making sure that the conpty window is initially created with the owner already set, rather than relying on a `SetParent` call in post. This does pose some complications for the #1256 future we're approaching. However, this is a blocking bug _now_, and we can figure out the tearout/`SetParent` thing in post. * fixes #13066. * Tested with the script in that issue. * Window doesn't flash uncontrollably. * `gci | ogv` still works right * I work here. * Opening a new tab doesn't spontaneously cause the window to minimize * Restoring from minimized doesn't yeet focus to an invisible window * Opening a new tab doesn't yeet focus to an invisible window * There _is_ a viable way to call `GetAncestor` s.t. it returns the Terminal's hwnd in Terminal, and the console's in Conhost The `SW_SHOWNOACTIVATE` change is also quite load bearing. With just `SW_NORMAL`, the pseudo window (which is invisible!) gets activated whenever the terminal window is restored from minimized. That's BAD. There's actually more to this as well. Calling `SetParent` on a window that is `WS_VISIBLE` will cause the OS to hide the window, make it a _child_ window, then call `SW_SHOW` on the window to re-show it. `SW_SHOW`, however, will cause the OS to also set that window as the _foreground_ window, which would result in the pty's hwnd stealing the foreground away from the owning terminal window. That's bad. `SetWindowLongPtr` seems to do the job of changing who the window owner is, without all the other side effects of reparenting the window. Without `SetParent`, however, the pty HWND is no longer a descendant of the Terminal HWND, so that means `GA_ROOT` can no longer be used to find the owner's hwnd. For even more insanity, without `WS_POPUP`, none of the values of `GetAncestor` will actually get the terminal HWND. So, now we also need `WS_POPUP` on the pty hwnd. To get at the Terminal hwnd, you'll need ```c++ GetAncestor(GetConsoleWindow(), GA_ROOTOWNER) ```
1 parent 0154da5 commit 77215d9

File tree

12 files changed

+110
-33
lines changed

12 files changed

+110
-33
lines changed

src/cascadia/TerminalApp/TerminalPage.cpp

+4-1
Original file line numberDiff line numberDiff line change
@@ -2431,7 +2431,10 @@ namespace winrt::TerminalApp::implementation
24312431
TermControl term{ settings.DefaultSettings(), settings.UnfocusedSettings(), connection };
24322432

24332433
// GH#12515: ConPTY assumes it's hidden at the start. If we're not, let it know now.
2434-
term.WindowVisibilityChanged(_visible);
2434+
if (_visible)
2435+
{
2436+
term.WindowVisibilityChanged(_visible);
2437+
}
24352438

24362439
if (_hostingHwnd.has_value())
24372440
{

src/cascadia/TerminalConnection/ConptyConnection.cpp

+6-2
Original file line numberDiff line numberDiff line change
@@ -311,13 +311,17 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
311311

312312
THROW_IF_FAILED(_CreatePseudoConsoleAndPipes(dimensions, flags, &_inPipe, &_outPipe, &_hPC));
313313

314-
// GH#12515: The conpty assumes it's hidden at the start. If we're visible, let it know now.
315-
THROW_IF_FAILED(ConptyShowHidePseudoConsole(_hPC.get(), _initialVisibility));
316314
if (_initialParentHwnd != 0)
317315
{
318316
THROW_IF_FAILED(ConptyReparentPseudoConsole(_hPC.get(), reinterpret_cast<HWND>(_initialParentHwnd)));
319317
}
320318

319+
// GH#12515: The conpty assumes it's hidden at the start. If we're visible, let it know now.
320+
if (_initialVisibility)
321+
{
322+
THROW_IF_FAILED(ConptyShowHidePseudoConsole(_hPC.get(), _initialVisibility));
323+
}
324+
321325
THROW_IF_FAILED(_LaunchAttachedClient());
322326
}
323327
// But if it was an inbound handoff... attempt to synchronize the size of it with what our connection

src/cascadia/TerminalConnection/ConptyConnection.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
7373
hstring _commandline{};
7474
hstring _startingDirectory{};
7575
hstring _startingTitle{};
76-
bool _initialVisibility{ false };
76+
bool _initialVisibility{ true };
7777
Windows::Foundation::Collections::ValueSet _environment{ nullptr };
7878
guid _guid{}; // A unique session identifier for connected client
7979
hstring _clientName{}; // The name of the process hosted by this ConPTY connection (as of launch).

src/cascadia/TerminalControl/ControlCore.cpp

+11-5
Original file line numberDiff line numberDiff line change
@@ -1196,8 +1196,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
11961196

11971197
void ControlCore::_terminalShowWindowChanged(bool showOrHide)
11981198
{
1199-
auto showWindow = winrt::make_self<implementation::ShowWindowArgs>(showOrHide);
1200-
_ShowWindowChangedHandlers(*this, *showWindow);
1199+
if (_initializedTerminal)
1200+
{
1201+
auto showWindow = winrt::make_self<implementation::ShowWindowArgs>(showOrHide);
1202+
_ShowWindowChangedHandlers(*this, *showWindow);
1203+
}
12011204
}
12021205

12031206
bool ControlCore::HasSelection() const
@@ -1703,10 +1706,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation
17031706
// - <none>
17041707
void ControlCore::WindowVisibilityChanged(const bool showOrHide)
17051708
{
1706-
// show is true, hide is false
1707-
if (auto conpty{ _connection.try_as<TerminalConnection::ConptyConnection>() })
1709+
if (_initializedTerminal)
17081710
{
1709-
conpty.ShowHide(showOrHide);
1711+
// show is true, hide is false
1712+
if (auto conpty{ _connection.try_as<TerminalConnection::ConptyConnection>() })
1713+
{
1714+
conpty.ShowHide(showOrHide);
1715+
}
17101716
}
17111717
}
17121718

src/host/PtySignalInputThread.cpp

+29-7
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,20 @@ void PtySignalInputThread::ConnectConsole() noexcept
7575
_DoShowHide(_initialShowHide->show);
7676
}
7777

78-
// If we were given a owner HWND, then manually start the pseudo window now.
79-
if (_earlyReparent)
80-
{
81-
_DoSetWindowParent(*_earlyReparent);
82-
}
78+
// We should have successfully used the _earlyReparent message in CreatePseudoWindow.
79+
}
80+
81+
// Method Description:
82+
// - Create our pseudo window. We're doing this here, instead of in
83+
// ConnectConsole, because the window is created in
84+
// ConsoleInputThreadProcWin32, before ConnectConsole is first called. Doing
85+
// this here ensures that the window is first created with the initial owner
86+
// set up (if so specified).
87+
// - Refer to GH#13066 for details.
88+
void PtySignalInputThread::CreatePseudoWindow()
89+
{
90+
HWND owner = _earlyReparent.has_value() ? reinterpret_cast<HWND>((*_earlyReparent).handle) : HWND_DESKTOP;
91+
ServiceLocator::LocatePseudoWindow(owner);
8392
}
8493

8594
// Method Description:
@@ -227,15 +236,28 @@ void PtySignalInputThread::_DoShowHide(const bool show)
227236
// - <none>
228237
void PtySignalInputThread::_DoSetWindowParent(const SetParentData& data)
229238
{
239+
const auto owner{ reinterpret_cast<HWND>(data.handle) };
230240
// This will initialize s_interactivityFactory for us. It will also
231241
// conveniently return 0 when we're on OneCore.
232242
//
233243
// If the window hasn't been created yet, by some other call to
234244
// LocatePseudoWindow, then this will also initialize the owner of the
235245
// window.
236-
if (const auto pseudoHwnd{ ServiceLocator::LocatePseudoWindow(reinterpret_cast<HWND>(data.handle)) })
246+
if (const auto pseudoHwnd{ ServiceLocator::LocatePseudoWindow(owner) })
237247
{
238-
LOG_LAST_ERROR_IF_NULL(::SetParent(pseudoHwnd, reinterpret_cast<HWND>(data.handle)));
248+
// DO NOT USE SetParent HERE!
249+
//
250+
// Calling SetParent on a window that is WS_VISIBLE will cause the OS to
251+
// hide the window, make it a _child_ window, then call SW_SHOW on the
252+
// window to re-show it. SW_SHOW, however, will cause the OS to also set
253+
// that window as the _foreground_ window, which would result in the
254+
// pty's hwnd stealing the foreground away from the owning terminal
255+
// window. That's bad.
256+
//
257+
// SetWindowLongPtr seems to do the job of changing who the window owner
258+
// is, without all the other side effects of reparenting the window.
259+
// See #13066
260+
::SetWindowLongPtr(pseudoHwnd, GWLP_HWNDPARENT, reinterpret_cast<LONG_PTR>(owner));
239261
}
240262
}
241263

src/host/PtySignalInputThread.hpp

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ namespace Microsoft::Console
3333
PtySignalInputThread& operator=(const PtySignalInputThread&) = delete;
3434

3535
void ConnectConsole() noexcept;
36+
void CreatePseudoWindow();
3637

3738
private:
3839
enum class PtySignal : unsigned short

src/host/VtIo.cpp

+21-5
Original file line numberDiff line numberDiff line change
@@ -300,18 +300,34 @@ bool VtIo::IsUsingVt() const
300300

301301
if (_pPtySignalInputThread)
302302
{
303-
// IMPORTANT! Start the pseudo window on this thread. This thread has a
304-
// message pump. If you DON'T, then a DPI change in the owning hwnd will
305-
// cause us to get a dpi change as well, which we'll never deque and
306-
// handle, effectively HANGING THE OWNER HWND.
303+
// Let the signal thread know that the console is connected.
307304
//
308-
// Let the signal thread know that the console is connected
305+
// By this point, the pseudo window should have already been created, by
306+
// ConsoleInputThreadProcWin32. That thread has a message pump, which is
307+
// needed to ensure that DPI change messages to the owning terminal
308+
// window don't end up hanging because the pty didn't also process it.
309309
_pPtySignalInputThread->ConnectConsole();
310310
}
311311

312312
return S_OK;
313313
}
314314

315+
// Method Description:
316+
// - Create our pseudo window. This is exclusively called by
317+
// ConsoleInputThreadProcWin32 on the console input thread.
318+
// * It needs to be called on that thread, before any other calls to
319+
// LocatePseudoWindow, to make sure that the input thread is the HWND's
320+
// message thread.
321+
// * It needs to be plumbed through the signal thread, because the signal
322+
// thread knows if someone should be marked as the window's owner. It's
323+
// VERY IMPORTANT that any initial owners are set up when the window is
324+
// first created.
325+
// - Refer to GH#13066 for details.
326+
void VtIo::CreatePseudoWindow()
327+
{
328+
_pPtySignalInputThread->CreatePseudoWindow();
329+
}
330+
315331
// Method Description:
316332
// - Create and start the signal thread. The signal thread can be created
317333
// independent of the i/o threads, and doesn't require a client first

src/host/VtIo.hpp

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ namespace Microsoft::Console::VirtualTerminal
5252

5353
[[nodiscard]] HRESULT ManuallyClearScrollback() const noexcept;
5454

55+
void CreatePseudoWindow();
56+
5557
private:
5658
// After CreateIoHandlers is called, these will be invalid.
5759
wil::unique_hfile _hInput;

src/host/outputStream.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ void ConhostInternalGetSet::ShowWindow(bool showOrHide)
285285
{
286286
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
287287
const auto hwnd = gci.IsInVtIoMode() ? ServiceLocator::LocatePseudoWindow() : ServiceLocator::LocateConsoleWindow()->GetWindowHandle();
288-
::ShowWindow(hwnd, showOrHide ? SW_NORMAL : SW_MINIMIZE);
288+
::ShowWindow(hwnd, showOrHide ? SW_SHOWNOACTIVATE : SW_MINIMIZE);
289289
}
290290

291291
// Routine Description:

src/interactivity/base/InteractivityFactory.cpp

+9-3
Original file line numberDiff line numberDiff line change
@@ -320,8 +320,14 @@ using namespace Microsoft::Console::Interactivity;
320320
// as far as the difference between parent/child and owner/owned
321321
// windows). Evan K said we should do it this way, and he
322322
// definitely knows.
323-
const auto windowStyle = WS_OVERLAPPEDWINDOW;
324-
const auto exStyles = WS_EX_TOOLWINDOW | WS_EX_TRANSPARENT | WS_EX_LAYERED;
323+
//
324+
// GH#13066: Load-bearing: Make sure to set WS_POPUP. If you
325+
// don't, then GetAncestor(GetConsoleWindow(), GA_ROOTOWNER)
326+
// will return the console handle again, not the owning
327+
// terminal's handle. It's not entirely clear why, but WS_POPUP
328+
// is absolutely vital for this to work correctly.
329+
const auto windowStyle = WS_OVERLAPPEDWINDOW | WS_POPUP;
330+
const auto exStyles = WS_EX_TOOLWINDOW | WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_NOACTIVATE;
325331

326332
// Attempt to create window.
327333
hwnd = CreateWindowExW(exStyles,
@@ -335,7 +341,7 @@ using namespace Microsoft::Console::Interactivity;
335341
owner,
336342
nullptr,
337343
nullptr,
338-
nullptr);
344+
this);
339345

340346
if (hwnd == nullptr)
341347
{

src/interactivity/win32/windowio.cpp

+12-3
Original file line numberDiff line numberDiff line change
@@ -1036,9 +1036,18 @@ DWORD WINAPI ConsoleInputThreadProcWin32(LPVOID /*lpParameter*/)
10361036
// If we are headless (because we're a pseudo console), we
10371037
// will still need a window handle in the win32 environment
10381038
// in case anyone sends messages at that HWND (vim.exe is an example.)
1039-
// We have to CreateWindow on the same thread that will pump the messages
1040-
// which is this thread.
1041-
ServiceLocator::LocatePseudoWindow();
1039+
//
1040+
// IMPORTANT! We have to CreateWindow on the same thread that will pump
1041+
// the messages, which is this thread. If you DON'T, then a DPI change
1042+
// in the owning hwnd will cause us to get a dpi change as well, which
1043+
// we'll never deque and handle, effectively HANGING THE OWNER HWND.
1044+
// ServiceLocator::LocatePseudoWindow();
1045+
//
1046+
// Instead of just calling LocatePseudoWindow, make sure to go through
1047+
// VtIo's CreatePseudoWindow, which will make sure that the window is
1048+
// successfully created with the owner configured when the window is
1049+
// first created. See GH#13066 for details.
1050+
ServiceLocator::LocateGlobals().getConsoleInformation().GetVtIo()->CreatePseudoWindow();
10421051
}
10431052

10441053
UnlockConsole();

src/terminal/adapter/InteractDispatch.cpp

+13-5
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,19 @@ bool InteractDispatch::FocusChanged(const bool focused) const
199199
{
200200
// They want focus, we found a pseudo hwnd.
201201

202-
// Note: ::GetParent(pseudoHwnd) will return 0. GetAncestor works though.
203-
// GA_PARENT and GA_ROOT seemingly return the same thing for
204-
// Terminal. We're going with GA_ROOT since it seems
205-
// semantically more correct here.
206-
if (const auto ownerHwnd{ ::GetAncestor(pseudoHwnd, GA_ROOT) })
202+
// BODGY
203+
//
204+
// This needs to be GA_ROOTOWNER here. Not GA_ROOT, GA_PARENT,
205+
// or GetParent. The ConPTY hwnd is an owned, top-level, popup,
206+
// non-parented window. It does not have a parent set. It does
207+
// have an owner set. It is not a WS_CHILD window. This
208+
// combination of things allows us to find the owning window
209+
// with GA_ROOTOWNER. GA_ROOT will get us ourselves, and
210+
// GA_PARENT will return the desktop HWND.
211+
//
212+
// See GH#13066
213+
214+
if (const auto ownerHwnd{ ::GetAncestor(pseudoHwnd, GA_ROOTOWNER) })
207215
{
208216
// We have an owner from a previous call to ReparentWindow
209217

0 commit comments

Comments
 (0)