Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/actions/spelling/expect/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ allocing
alpc
ALTERNATENAME
ALTF
ALTGR
ALTNUMPAD
ALWAYSTIP
ansicpg
Expand Down Expand Up @@ -867,6 +868,7 @@ kinda
KIYEOK
KKP
KLF
KLLF
KLMNO
KOK
KPRIORITY
Expand Down Expand Up @@ -1984,8 +1986,8 @@ WRITECONSOLEINPUT
WRITECONSOLEOUTPUT
WRITECONSOLEOUTPUTSTRING
wrkstr
wrl
WRL
wrl
wrp
WRunoff
WSLENV
Expand Down
2 changes: 2 additions & 0 deletions src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
<ClCompile Include="inputTest.cpp" />
<ClCompile Include="kittyKeyboardProtocol.cpp" />
<ClCompile Include="MouseInputTest.cpp" />
<ClCompile Include="TestHook.cpp" />
<ClCompile Include="..\precomp.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\precomp.h" />
<ClInclude Include="TestHook.h" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\buffer\out\lib\bufferout.vcxproj">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@
<ClCompile Include="kittyKeyboardProtocol.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="TestHook.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\precomp.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="TestHook.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />
Expand Down
118 changes: 118 additions & 0 deletions src/terminal/adapter/ut_adapter/TestHook.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#include "precomp.h"
#include "TestHook.h"

using namespace TestHook;

thread_local HKL g_keyboardLayout;

extern "C" HKL TestHook_TerminalInput_KeyboardLayout()
{
return g_keyboardLayout;
}

static bool isPreloadedLayout(const wchar_t* klid) noexcept
Comment thread Fixed
{
wil::unique_hkey preloadKey;
if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Keyboard Layout\\Preload", 0, KEY_READ, preloadKey.addressof()) != ERROR_SUCCESS)
{
return false;
}

wil::unique_hkey substitutesKey;
RegOpenKeyExW(HKEY_CURRENT_USER, L"Keyboard Layout\\Substitutes", 0, KEY_READ, substitutesKey.addressof());

wchar_t idx[16];
wchar_t layoutId[KL_NAMELENGTH];

for (DWORD i = 0;; i++)
{
DWORD idxLen = ARRAYSIZE(idx);
DWORD layoutIdSize = sizeof(layoutId);
if (RegEnumValueW(preloadKey.get(), i, idx, &idxLen, nullptr, nullptr, reinterpret_cast<BYTE*>(layoutId), &layoutIdSize) != ERROR_SUCCESS)
{
break;
}

// Preload contains base language IDs (e.g. "0000040c").
// The actual layout ID (e.g. "0001040c") may only appear in the Substitutes key.
if (substitutesKey)
{
wchar_t substitute[KL_NAMELENGTH];
DWORD substituteSize = sizeof(substitute);
if (RegGetValueW(substitutesKey.get(), nullptr, layoutId, RRF_RT_REG_SZ, nullptr, substitute, &substituteSize) == ERROR_SUCCESS)
{
memcpy(layoutId, substitute, sizeof(layoutId));
}
}

if (wcscmp(layoutId, klid) == 0)
Comment thread Fixed
{
return true;
}
}

return false;
}

void LayoutGuard::_destroy() const noexcept
{
if (g_keyboardLayout == _layout)
{
g_keyboardLayout = nullptr;
}
if (_needsUnload)
{
UnloadKeyboardLayout(_layout);
}
}

LayoutGuard::LayoutGuard(HKL layout, bool needsUnload) noexcept :
_layout{ layout },
_needsUnload{ needsUnload }
{
}

LayoutGuard::~LayoutGuard()
{
_destroy();
}

LayoutGuard::LayoutGuard(LayoutGuard&& other) noexcept :
_layout{ std::exchange(other._layout, nullptr) },
_needsUnload{ std::exchange(other._needsUnload, false) }
{
}

LayoutGuard& LayoutGuard::operator=(LayoutGuard&& other) noexcept
{
if (this != &other)
{
_destroy();
_layout = std::exchange(other._layout, nullptr);
_needsUnload = std::exchange(other._needsUnload, false);
}
return *this;
}

LayoutGuard::operator HKL() const noexcept
{
return _layout;
}

LayoutGuard TestHook::SetTerminalInputKeyboardLayout(const wchar_t* klid)
Comment thread Fixed
{
THROW_HR_IF_MSG(E_UNEXPECTED, g_keyboardLayout != nullptr, "Nested layout test overrides are not supported");

const auto layout = LoadKeyboardLayoutW(klid, KLF_NOTELLSHELL);
Comment thread Fixed
Comment thread Fixed
THROW_LAST_ERROR_IF_NULL(layout);

g_keyboardLayout = layout;

// Unload the layout if it's not one of the user's layouts.
// GetKeyboardLayoutList is unreliable for this purpose, as the keyboard layout API mutates global OS state.
// If a process crashes or exits early without calling UnloadKeyboardLayout all future processes will get it
// returned in their GetKeyboardLayoutList calls. Shell could fix it but alas. So we peek into the registry.
const auto needsUnload = !isPreloadedLayout(klid);
Comment thread Fixed

return { layout, needsUnload };
}
25 changes: 25 additions & 0 deletions src/terminal/adapter/ut_adapter/TestHook.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#pragma once

namespace TestHook
{
struct LayoutGuard
{
LayoutGuard(HKL layout, bool needsUnload) noexcept;
~LayoutGuard();

LayoutGuard(const LayoutGuard&) = delete;
LayoutGuard& operator=(const LayoutGuard&) = delete;
LayoutGuard(LayoutGuard&& other) noexcept;
LayoutGuard& operator=(LayoutGuard&& other) noexcept;

operator HKL() const noexcept;

private:
void _destroy() const noexcept;

HKL _layout = nullptr;
bool _needsUnload = false;
};

LayoutGuard SetTerminalInputKeyboardLayout(const wchar_t* klid);
}
88 changes: 69 additions & 19 deletions src/terminal/adapter/ut_adapter/inputTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include "precomp.h"

#include "TestHook.h"
#include "../../../interactivity/inc/VtApiRedirection.hpp"
#include "../../input/terminalInput.hpp"
#include "../types/inc/IInputEvent.hpp"
Expand Down Expand Up @@ -308,16 +309,48 @@ void InputTest::TerminalInputModifierKeyTests()
const auto slashVkey = LOBYTE(OneCoreSafeVkKeyScanW(L'/'));
const auto nullVkey = LOBYTE(OneCoreSafeVkKeyScanW(0));

uint8_t keyboardState[256] = {};
wchar_t unicodeBuf[4] = {};
const uint8_t rightAlt = WI_IsFlagSet(uiKeystate, RIGHT_ALT_PRESSED) ? 0x80 : 0;
const uint8_t leftAlt = WI_IsFlagSet(uiKeystate, LEFT_ALT_PRESSED) ? 0x80 : 0;
const uint8_t rightCtrl = WI_IsFlagSet(uiKeystate, RIGHT_CTRL_PRESSED) ? 0x80 : 0;
const uint8_t leftCtrl = WI_IsFlagSet(uiKeystate, LEFT_CTRL_PRESSED) ? 0x80 : 0;
const uint8_t shift = WI_IsFlagSet(uiKeystate, SHIFT_PRESSED) ? 0x80 : 0;
const uint8_t capsLock = WI_IsFlagSet(uiKeystate, CAPSLOCK_ON) ? 0x01 : 0;
keyboardState[VK_SHIFT] = shift;
keyboardState[VK_CONTROL] = leftCtrl | rightCtrl;
keyboardState[VK_MENU] = leftAlt | rightAlt;
keyboardState[VK_CAPITAL] = capsLock;
keyboardState[VK_LSHIFT] = shift;
keyboardState[VK_LCONTROL] = leftCtrl;
keyboardState[VK_RCONTROL] = rightCtrl;
keyboardState[VK_LMENU] = leftAlt;
keyboardState[VK_RMENU] = rightAlt;

const auto anyCtrlPressed = WI_IsAnyFlagSet(uiKeystate, CTRL_PRESSED);
const auto bothCtrlPressed = WI_AreAllFlagsSet(uiKeystate, CTRL_PRESSED);
const auto anyAltPressed = WI_IsAnyFlagSet(uiKeystate, ALT_PRESSED);
const auto bothAltPressed = WI_AreAllFlagsSet(uiKeystate, ALT_PRESSED);
const auto shiftPressed = WI_IsFlagSet(uiKeystate, SHIFT_PRESSED);

Log::Comment(L"Sending every possible VKEY at the input stream for interception during key DOWN.");
for (BYTE vkey = 0; vkey < BYTE_MAX; vkey++)
{
Log::Comment(NoThrowString().Format(L"Testing Key 0x%x", vkey));

auto fExpectedKeyHandled = true;
auto fModifySequence = false;
wchar_t ch = LOWORD(OneCoreSafeMapVirtualKeyW(vkey, MAPVK_VK_TO_CHAR));

if (ControlPressed(uiKeystate))
til::at(keyboardState, vkey) = 0x80; // Momentarily pretend as if the key is set
const auto unicodeLen = ToUnicodeEx(vkey, 0, &keyboardState[0], &unicodeBuf[0], ARRAYSIZE(unicodeBuf), 0b101, nullptr);
til::at(keyboardState, vkey) = 0;

wchar_t ch = unicodeLen == 1 ? unicodeBuf[0] : 0;
const auto altGrPressed = anyAltPressed && anyCtrlPressed && (ch > 0x20 && ch != 0x7f);
const auto ctrlPressed = bothCtrlPressed || (anyCtrlPressed && !altGrPressed);
const auto altPressed = bothAltPressed || (anyAltPressed && !altGrPressed);

if (ctrlPressed)
{
// For Ctrl-/ see DifferentModifiersTest.
if (vkey == VK_DIVIDE || vkey == slashVkey)
Expand Down Expand Up @@ -472,28 +505,28 @@ void InputTest::TerminalInputModifierKeyTests()
expected = TerminalInput::MakeOutput({ &ch, 1 });
break;
case VK_RETURN:
if (AltPressed(uiKeystate))
if (altPressed)
{
const auto str = ControlPressed(uiKeystate) ? L"\x1b\n" : L"\x1b\r";
const auto str = ctrlPressed ? L"\x1b\n" : L"\x1b\r";
expected = TerminalInput::MakeOutput(str);
}
else
{
const auto str = ControlPressed(uiKeystate) ? L"\n" : L"\r";
const auto str = ctrlPressed ? L"\n" : L"\r";
expected = TerminalInput::MakeOutput(str);
}
break;
case VK_TAB:
if (AltPressed(uiKeystate))
if (altPressed)
{
// Alt+Tab isn't possible - that's reserved by the system.
continue;
}
else if (ShiftPressed(uiKeystate))
else if (shiftPressed)
{
expected = TerminalInput::MakeOutput(L"\x1b[Z");
}
else if (ControlPressed(uiKeystate))
else
{
expected = TerminalInput::MakeOutput(L"\t");
}
Expand All @@ -506,13 +539,19 @@ void InputTest::TerminalInputModifierKeyTests()
case VK_OEM_102:
// OEM keys require special case handling when combined with a Ctrl
// modifier, but otherwise work the same way as regular keys.
if (ControlPressed(uiKeystate))
if (ctrlPressed)
{
continue;
}
[[fallthrough]];
default:
if (ControlPressed(uiKeystate) && (vkey >= '1' && vkey <= '9'))
// Map VK_ESCAPE, etc., to their corresponding character value, if needed.
if (ch == 0)
{
ch = LOWORD(OneCoreSafeMapVirtualKeyW(vkey, MAPVK_VK_TO_CHAR));
}

if (ctrlPressed && (vkey >= '1' && vkey <= '9'))
{
// The C-# keys get translated into very specific control
// characters that don't play nicely with this test. These keys
Expand All @@ -531,7 +570,7 @@ void InputTest::TerminalInputModifierKeyTests()
// Alt+Key generates [0x1b, Ctrl+key] into the stream
// Pressing the control key causes all bits but the 5 least
// significant ones to be zeroed out (when using ASCII).
if (AltPressed(uiKeystate) && ControlPressed(uiKeystate) && ch > 0x40 && ch <= 0x5A)
if (altPressed && ctrlPressed && ch > L'@' && ch <= L'~')
{
const wchar_t buffer[2]{ L'\x1b', gsl::narrow_cast<wchar_t>(ch & 0b11111) };
expected = TerminalInput::MakeOutput({ &buffer[0], 2 });
Expand All @@ -540,17 +579,25 @@ void InputTest::TerminalInputModifierKeyTests()
}

// Alt+Key generates [0x1b, key] into the stream
if (AltPressed(uiKeystate) && ch != 0)
if (altPressed && ch != 0)
{
const wchar_t buffer[2]{ L'\x1b', ch };
expected = TerminalInput::MakeOutput({ &buffer[0], 2 });
if (ControlPressed(uiKeystate))
if (ctrlPressed)
{
ch = 0;
}
break;
}

// Ctrl+Key masks the key value.
if (ctrlPressed && ch > L'@' && ch <= L'~')
{
const auto b = gsl::narrow_cast<wchar_t>(ch & 0b11111);
expected = TerminalInput::MakeOutput({ &b, 1 });
break;
}

if (ch != 0)
{
expected = TerminalInput::MakeOutput({ &ch, 1 });
Expand All @@ -563,11 +610,14 @@ void InputTest::TerminalInputModifierKeyTests()

if (fModifySequence)
{
auto fShift = !!(uiKeystate & SHIFT_PRESSED);
auto fAlt = (uiKeystate & LEFT_ALT_PRESSED) || (uiKeystate & RIGHT_ALT_PRESSED);
auto fCtrl = (uiKeystate & LEFT_CTRL_PRESSED) || (uiKeystate & RIGHT_CTRL_PRESSED);
const auto mod = shiftPressed + (2 * altPressed) + (4 * ctrlPressed);
if (mod == 0)
{
continue;
}

auto& str = expected.value();
str[str.size() - 2] = L'1' + (fShift ? 1 : 0) + (fAlt ? 2 : 0) + (fCtrl ? 4 : 0);
str[str.size() - 2] = static_cast<wchar_t>(L'1' + mod);
}

TestKey(expected, input, uiKeystate, vkey, ch);
Expand All @@ -578,13 +628,14 @@ void InputTest::TerminalInputNullKeyTests()
{
using namespace std::string_view_literals;

const auto layout = TestHook::SetTerminalInputKeyboardLayout(L"00000409"); // US English
unsigned int uiKeystate = LEFT_CTRL_PRESSED;

TerminalInput input;

Log::Comment(L"Sending every possible VKEY at the input stream for interception during key DOWN.");

BYTE vkey = LOBYTE(OneCoreSafeVkKeyScanW(0));
BYTE vkey = LOBYTE(VkKeyScanExW(0, layout));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need to use OneCoreSaveVkKeyScanW; this test will fail to run (or, maybe the entire fixture will fail to load?) if we link VkKeyScanW.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it depends on LoadKeyboardLayout and ToUnicodeEx now. Won't all of these fail to work anyway? Do they have to work in the first? The TerminalInput logic is platform-independent with the exception of a single ToUnicodeEx call.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, so here's what I have come to understand.

We refer to these APIs using an extension APIset.

    ext-ms-win-ntuser-keyboard-l1-1-0
      GetKeyState
      GetKeyboardLayout
      MapVirtualKeyW
      ToUnicodeEx
      VkKeyScanW

OneCoreSafeEtc runs those through coniosrv or equivalent rather than ntuser.
In theory, we could have done some effort to compose onecore with a new extension apiset that fills the same contract. Instead we used runtime dynamic dispatch. Eh.

As long as the implementation calls OneCoreSafeEtc, we're fine.

We will need to prevent the kitty tests from running on OneCore, so that we do not experience test gate failures. :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added exception guards around SetTerminalInputKeyboardLayout. However, I have not checked if it results in linking failures in OneCore. I'm somewhat confused how this works in general given that we have never defined a OneCoreSafe wrapper for ToUnicodeEx.

Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate));

INPUT_RECORD irTest = { 0 };
Expand All @@ -600,7 +651,6 @@ void InputTest::TerminalInputNullKeyTests()
vkey = VK_SPACE;
Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate));
irTest.Event.KeyEvent.wVirtualKeyCode = vkey;
irTest.Event.KeyEvent.uChar.UnicodeChar = vkey;
VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\0"sv), input.HandleKey(irTest), L"Verify key was handled if it should have been.");

uiKeystate = LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED;
Expand Down
Loading
Loading