Skip to content

Commit 1ca0c76

Browse files
authored
Fix a WPF<>TSF crash by avoiding TF_TMAE_CONSOLE (#19584)
As explained in detail in the diff. Closes #19562
1 parent ee6060b commit 1ca0c76

File tree

10 files changed

+67
-6
lines changed

10 files changed

+67
-6
lines changed

.github/actions/spelling/expect/expect.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1091,6 +1091,8 @@ NEXTLINE
10911091
nfe
10921092
NLSMODE
10931093
NOACTIVATE
1094+
NOACTIVATEKEYBOARDLAYOUT
1095+
NOACTIVATETIP
10941096
NOAPPLYNOW
10951097
NOCLIP
10961098
NOCOMM
@@ -1504,7 +1506,6 @@ scrolllock
15041506
scrolloffset
15051507
SCROLLSCALE
15061508
SCROLLSCREENBUFFER
1507-
scursor
15081509
sddl
15091510
SDKDDK
15101511
segfault
@@ -1703,9 +1704,11 @@ TEXTMETRIC
17031704
TEXTMETRICW
17041705
textmode
17051706
texttests
1707+
TFCAT
17061708
THUMBPOSITION
17071709
THUMBTRACK
17081710
tilunittests
1711+
TIPCAP
17091712
titlebars
17101713
TITLEISLINKNAME
17111714
TLDP
@@ -1757,6 +1760,8 @@ UIACCESS
17571760
uiacore
17581761
uiautomationcore
17591762
uielem
1763+
UIELEMENTENABLED
1764+
UIELEMENTENABLEDONLY
17601765
UINTs
17611766
uld
17621767
uldash

src/cascadia/TerminalControl/HwndTerminal.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,11 @@ void HwndTerminal::SendOutput(std::wstring_view data)
462462
_terminal->Write(data);
463463
}
464464

465+
void _stdcall AvoidBuggyTSFConsoleFlags()
466+
{
467+
Microsoft::Console::TSF::Handle::AvoidBuggyTSFConsoleFlags();
468+
}
469+
465470
HRESULT _stdcall CreateTerminal(HWND parentHwnd, _Out_ void** hwnd, _Out_ void** terminal)
466471
{
467472
auto publicTerminal = std::make_unique<HwndTerminal>(parentHwnd);

src/cascadia/TerminalControl/HwndTerminal.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ typedef struct _TerminalTheme
4141
} TerminalTheme, *LPTerminalTheme;
4242

4343
extern "C" {
44+
__declspec(dllexport) void _stdcall AvoidBuggyTSFConsoleFlags();
4445
__declspec(dllexport) HRESULT _stdcall CreateTerminal(HWND parentHwnd, _Out_ void** hwnd, _Out_ void** terminal);
4546
__declspec(dllexport) void _stdcall TerminalSendOutput(void* terminal, LPCWSTR data);
4647
__declspec(dllexport) void _stdcall TerminalRegisterScrollCallback(void* terminal, void __stdcall callback(int, int, int));

src/cascadia/TerminalControl/dll/Microsoft.Terminal.Control.def

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ EXPORTS
44
DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE
55

66
; Flat C ABI
7+
AvoidBuggyTSFConsoleFlags
78
CreateTerminal
89
DestroyTerminal
910
TerminalCalculateResize

src/cascadia/WpfTerminalControl/NativeMethods.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ public enum SetWindowPosFlags : uint
171171
SWP_SHOWWINDOW = 0x0040,
172172
}
173173

174+
[DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, ExactSpelling = true)]
175+
public static extern void AvoidBuggyTSFConsoleFlags();
176+
174177
[DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = false)]
175178
public static extern void CreateTerminal(IntPtr parent, out IntPtr hwnd, out IntPtr terminal);
176179

src/cascadia/WpfTerminalControl/TerminalContainer.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ namespace Microsoft.Terminal.Wpf
1111
using System.Windows.Automation.Peers;
1212
using System.Windows.Interop;
1313
using System.Windows.Media;
14-
using System.Windows.Threading;
1514

1615
/// <summary>
1716
/// The container class that hosts the native hwnd terminal.
@@ -32,6 +31,11 @@ public class TerminalContainer : HwndHost
3231
/// </summary>
3332
public TerminalContainer()
3433
{
34+
// WPF & TSF can't deal with us setting TF_TMAE_CONSOLE on the UI thread.
35+
// It simply crashes on Windows 10 if you use the Emoji picker.
36+
// (On later versions of Windows it just doesn't work.)
37+
NativeMethods.AvoidBuggyTSFConsoleFlags();
38+
3539
this.MessageHook += this.TerminalContainer_MessageHook;
3640
this.GotFocus += this.TerminalContainer_GotFocus;
3741
this.Focusable = true;

src/tsf/Handle.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ Handle Handle::Create()
1616
return handle;
1717
}
1818

19+
void Handle::AvoidBuggyTSFConsoleFlags()
20+
{
21+
Implementation::AvoidBuggyTSFConsoleFlags();
22+
}
23+
1924
void Handle::SetDefaultScopeAlphanumericHalfWidth(bool enable)
2025
{
2126
Implementation::SetDefaultScopeAlphanumericHalfWidth(enable);

src/tsf/Handle.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ namespace Microsoft::Console::TSF
3333
struct Handle
3434
{
3535
static Handle Create();
36+
static void AvoidBuggyTSFConsoleFlags();
3637
static void SetDefaultScopeAlphanumericHalfWidth(bool enable);
3738

3839
Handle() = default;

src/tsf/Implementation.cpp

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,45 @@ static void TfPropertyvalClose(TF_PROPERTYVAL* val)
2727
}
2828
using unique_tf_propertyval = wil::unique_struct<TF_PROPERTYVAL, decltype(&TfPropertyvalClose), &TfPropertyvalClose>;
2929

30+
// The flags passed to ActivateEx don't replace the flags during previous calls.
31+
// Instead, they're additive. So, if we pass flags that some concurrently running
32+
// TSF clients don't expect we may blow them up accidentally.
33+
//
34+
// Such is the case with WPF (and TSF, which is actually at fault there).
35+
// If you pass TF_TMAE_CONSOLE it'll instantly crash on Windows 10 on first text input.
36+
// On Windows 11 it'll at least not crash but still make emoji input completely non-functional.
37+
//
38+
// --------
39+
//
40+
// In any case, we pass the same flags as conhost v1:
41+
// - TF_TMAE_UIELEMENTENABLEDONLY: TSF activates only text services that are
42+
// categorized in GUID_TFCAT_TIPCAP_UIELEMENTENABLED.
43+
// - TF_TMAE_NOACTIVATEKEYBOARDLAYOUT: TSF does not sync the current keyboard layout
44+
// while this method is called. The keyboard layout will be adjusted when the
45+
// calling thread gets focus. This flag must be used with TF_TMAE_NOACTIVATETIP.
46+
// - TF_TMAE_CONSOLE: A text service is activated for console usage.
47+
// Some IMEs are known to use this as a hint. Particularly a Korean IME can benefit
48+
// from this, because Korean relies on "recomposing" previously finished compositions.
49+
// That can't work in a terminal, since we submit composed text to the shell immediately.
50+
//
51+
// I'm not sure what TF_TMAE_UIELEMENTENABLEDONLY does. I tried to figure it out but failed.
52+
//
53+
// For TF_TMAE_NOACTIVATEKEYBOARDLAYOUT, I'm 99% sure it doesn't do anything, including in
54+
// conhost v1. This is because IMM will be initialized on WM_ACTIVATE, which calls ActivateEx(0).
55+
// Any subsequent ActivateEx() calls will update the flags, _except_ for this one and
56+
// TF_TMAE_NOACTIVATETIP which are explicitly filtered out.
57+
//
58+
// TF_TMAE_NOACTIVATETIP however is important. Without it, TIPs are immediately initialized.
59+
static std::atomic<DWORD> s_activationFlags{ TF_TMAE_NOACTIVATETIP | TF_TMAE_UIELEMENTENABLEDONLY | TF_TMAE_NOACTIVATEKEYBOARDLAYOUT | TF_TMAE_CONSOLE };
60+
void Implementation::AvoidBuggyTSFConsoleFlags() noexcept
61+
{
62+
s_activationFlags.fetch_and(~static_cast<DWORD>(TF_TMAE_CONSOLE), std::memory_order_relaxed);
63+
}
64+
65+
static std::atomic<bool> s_wantsAnsiInputScope{ false };
3066
void Implementation::SetDefaultScopeAlphanumericHalfWidth(bool enable) noexcept
3167
{
32-
_wantsAnsiInputScope.store(enable, std::memory_order_relaxed);
68+
s_wantsAnsiInputScope.store(enable, std::memory_order_relaxed);
3369
}
3470

3571
void Implementation::Initialize()
@@ -40,7 +76,7 @@ void Implementation::Initialize()
4076
// There's no point in calling TF_GetThreadMgr. ITfThreadMgr is a per-thread singleton.
4177
_threadMgrEx = wil::CoCreateInstance<ITfThreadMgrEx>(CLSID_TF_ThreadMgr, CLSCTX_INPROC_SERVER);
4278

43-
THROW_IF_FAILED(_threadMgrEx->ActivateEx(&_clientId, TF_TMAE_CONSOLE));
79+
THROW_IF_FAILED(_threadMgrEx->ActivateEx(&_clientId, s_activationFlags.load(std::memory_order_relaxed)));
4480
THROW_IF_FAILED(_threadMgrEx->CreateDocumentMgr(_documentMgr.addressof()));
4581

4682
TfEditCookie ecTextStore;
@@ -319,7 +355,7 @@ STDMETHODIMP Implementation::GetWnd(HWND* phwnd) noexcept
319355

320356
STDMETHODIMP Implementation::GetAttribute(REFGUID rguidAttribute, VARIANT* pvarValue) noexcept
321357
{
322-
if (_wantsAnsiInputScope.load(std::memory_order_relaxed) && IsEqualGUID(rguidAttribute, GUID_PROP_INPUTSCOPE))
358+
if (s_wantsAnsiInputScope.load(std::memory_order_relaxed) && IsEqualGUID(rguidAttribute, GUID_PROP_INPUTSCOPE))
323359
{
324360
_ansiInputScope.AddRef();
325361
pvarValue->vt = VT_UNKNOWN;

src/tsf/Implementation.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ namespace Microsoft::Console::TSF
1616

1717
struct Implementation : ITfContextOwner, ITfContextOwnerCompositionSink, ITfTextEditSink
1818
{
19+
static void AvoidBuggyTSFConsoleFlags() noexcept;
1920
static void SetDefaultScopeAlphanumericHalfWidth(bool enable) noexcept;
2021

2122
virtual ~Implementation() = default;
@@ -129,6 +130,5 @@ namespace Microsoft::Console::TSF
129130
int _compositions = 0;
130131

131132
AnsiInputScope _ansiInputScope{ this };
132-
inline static std::atomic<bool> _wantsAnsiInputScope{ false };
133133
};
134134
}

0 commit comments

Comments
 (0)