-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Shim the AzureConn through a conhost to stop VT bleeding (#4652)
This commit introduces a small console-subsystem application whose sole job is to consume TerminalConnection.dll and hook it up to something other than Terminal. It is 99% of the way to a generic solution. I've introduced a stopgap in TerminalPage that makes sure we launch TerminalAzBridge using ConptyConnection instead of AzureConnection. As a bonus, this commit includes a class whose sole job it is to make reading VT input off a console handle not terrible. It returns you a string and dispatches window size change callbacks. Fixes #2267. Fixes #4589. Related to #2266 (since pwsh needs better VT).
- Loading branch information
Showing
10 changed files
with
361 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
#include "pch.h" | ||
#include "ConsoleInputReader.h" | ||
#include "unicode.hpp" | ||
|
||
ConsoleInputReader::ConsoleInputReader(HANDLE handle) : | ||
_handle(handle) | ||
{ | ||
_buffer.resize(BufferSize); | ||
_convertedString.reserve(BufferSize); | ||
} | ||
|
||
void ConsoleInputReader::SetWindowSizeChangedCallback(std::function<void()> callback) | ||
{ | ||
_windowSizeChangedCallback = std::move(callback); | ||
} | ||
|
||
std::optional<std::wstring_view> ConsoleInputReader::Read() | ||
{ | ||
DWORD readCount{ 0 }; | ||
|
||
_convertedString.clear(); | ||
while (_convertedString.empty()) | ||
{ | ||
_buffer.resize(BufferSize); | ||
BOOL succeeded = | ||
ReadConsoleInputW(_handle, _buffer.data(), gsl::narrow_cast<DWORD>(_buffer.size()), &readCount); | ||
if (!succeeded) | ||
{ | ||
return std::nullopt; | ||
} | ||
|
||
_buffer.resize(readCount); | ||
for (auto it = _buffer.begin(); it != _buffer.end(); ++it) | ||
{ | ||
if (it->EventType == WINDOW_BUFFER_SIZE_EVENT && _windowSizeChangedCallback) | ||
{ | ||
_windowSizeChangedCallback(); | ||
} | ||
else if (it->EventType == KEY_EVENT) | ||
{ | ||
const auto& keyEvent = it->Event.KeyEvent; | ||
if (keyEvent.bKeyDown || (!keyEvent.bKeyDown && keyEvent.wVirtualKeyCode == VK_MENU)) | ||
{ | ||
// Got a high surrogate at the end of the buffer | ||
if (IS_HIGH_SURROGATE(keyEvent.uChar.UnicodeChar)) | ||
{ | ||
_highSurrogate.emplace(keyEvent.uChar.UnicodeChar); | ||
continue; // we've consumed it -- only dispatch it if we get a low | ||
} | ||
|
||
if (IS_LOW_SURROGATE(keyEvent.uChar.UnicodeChar)) | ||
{ | ||
// No matter what we do, we want to destructively consume the high surrogate | ||
if (const auto oldHighSurrogate{ std::exchange(_highSurrogate, std::nullopt) }) | ||
{ | ||
_convertedString.push_back(*_highSurrogate); | ||
} | ||
else | ||
{ | ||
// If we get a low without a high surrogate, we've done everything we can. | ||
// This is an illegal state. | ||
_convertedString.push_back(UNICODE_REPLACEMENT); | ||
continue; // onto the next event | ||
} | ||
} | ||
|
||
// (\0 with a scancode is probably a modifier key, not a VT input key) | ||
if (keyEvent.uChar.UnicodeChar != L'\0' || keyEvent.wVirtualScanCode == 0) | ||
{ | ||
if (_highSurrogate) // non-destructive: we don't want to set it to nullopt needlessly for every character | ||
{ | ||
// If we get a high surrogate *here*, we didn't find a low surrogate. | ||
// This state is also illegal. | ||
_convertedString.push_back(UNICODE_REPLACEMENT); | ||
_highSurrogate.reset(); | ||
} | ||
_convertedString.push_back(keyEvent.uChar.UnicodeChar); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return _convertedString; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/*++ | ||
Copyright (c) Microsoft Corporation. | ||
Licensed under the MIT license. | ||
Module Name: | ||
ConsoleInputReader.h | ||
Abstract: | ||
This file contains a class whose sole purpose is to | ||
abstract away a bunch of details you usually need to | ||
know to read VT from a console input handle. | ||
--*/ | ||
|
||
class ConsoleInputReader | ||
{ | ||
public: | ||
ConsoleInputReader(HANDLE handle); | ||
void SetWindowSizeChangedCallback(std::function<void()> callback); | ||
std::optional<std::wstring_view> Read(); | ||
|
||
private: | ||
static constexpr size_t BufferSize{ 128 }; | ||
|
||
HANDLE _handle; | ||
std::wstring _convertedString; | ||
std::vector<INPUT_RECORD> _buffer; | ||
std::optional<wchar_t> _highSurrogate; | ||
std::function<void()> _windowSizeChangedCallback; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | ||
|
||
<PropertyGroup Label="Globals"> | ||
<ProjectGuid>{067F0A06-FCB7-472C-96E9-B03B54E8E18D}</ProjectGuid> | ||
<Keyword>Win32Proj</Keyword> | ||
<RootNamespace>TerminalAzBridge</RootNamespace> | ||
<ProjectName>TerminalAzBridge</ProjectName> | ||
<TargetName>TerminalAzBridge</TargetName> | ||
<ConfigurationType>Application</ConfigurationType> | ||
<OpenConsoleUniversalApp>false</OpenConsoleUniversalApp> | ||
<ApplicationType>Windows Store</ApplicationType> | ||
<NoOutputRedirection>true</NoOutputRedirection> | ||
</PropertyGroup> | ||
|
||
<Import Project="..\..\..\common.openconsole.props" Condition="'$(OpenConsoleDir)'==''" /> | ||
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.pre.props" /> | ||
|
||
<ItemDefinitionGroup> | ||
<ClCompile> | ||
<SDLCheck>true</SDLCheck> | ||
</ClCompile> | ||
<Link> | ||
<SubSystem>Console</SubSystem> | ||
</Link> | ||
</ItemDefinitionGroup> | ||
|
||
<PropertyGroup> | ||
<GenerateManifest>true</GenerateManifest> | ||
<EmbedManifest>true</EmbedManifest> | ||
</PropertyGroup> | ||
|
||
<!-- Source Files --> | ||
<ItemGroup> | ||
<ClInclude Include="pch.h" /> | ||
<ClInclude Include="ConsoleInputReader.h" /> | ||
</ItemGroup> | ||
<ItemGroup> | ||
<ClCompile Include="pch.cpp"> | ||
<PrecompiledHeader>Create</PrecompiledHeader> | ||
</ClCompile> | ||
<ClCompile Include="main.cpp" /> | ||
<ClCompile Include="ConsoleInputReader.cpp" /> | ||
</ItemGroup> | ||
<ItemGroup> | ||
<None Include="packages.config" /> | ||
</ItemGroup> | ||
<!-- Dependencies --> | ||
<ItemGroup> | ||
<ProjectReference Include="$(OpenConsoleDir)src\cascadia\TerminalConnection\TerminalConnection.vcxproj"> | ||
<Project>{CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}</Project> | ||
</ProjectReference> | ||
<ProjectReference Include="$(OpenConsoleDir)src\types\lib\types.vcxproj" /> | ||
</ItemGroup> | ||
|
||
<!-- | ||
This ItemGroup and the Globals PropertyGroup below it are required in order | ||
to enable F5 debugging for the unpackaged application | ||
--> | ||
<ItemGroup> | ||
<PropertyPageSchema Include="$(VCTargetsPath)$(LangID)\debugger_general.xml" /> | ||
<PropertyPageSchema Include="$(VCTargetsPath)$(LangID)\debugger_local_windows.xml" /> | ||
</ItemGroup> | ||
<PropertyGroup Label="Globals"> | ||
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor> | ||
</PropertyGroup> | ||
|
||
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.post.props" /> | ||
|
||
<Import Project="$(OpenConsoleDir)\build\rules\GenerateSxsManifestsFromWinmds.targets" /> | ||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
#include "pch.h" | ||
#include "winrt/Microsoft.Terminal.TerminalConnection.h" | ||
#include "ConsoleInputReader.h" | ||
|
||
using namespace winrt; | ||
using namespace winrt::Windows::Foundation; | ||
using namespace winrt::Microsoft::Terminal::TerminalConnection; | ||
|
||
static COORD GetConsoleScreenSize(HANDLE outputHandle) | ||
{ | ||
CONSOLE_SCREEN_BUFFER_INFOEX csbiex{}; | ||
csbiex.cbSize = sizeof(csbiex); | ||
GetConsoleScreenBufferInfoEx(outputHandle, &csbiex); | ||
return { | ||
(csbiex.srWindow.Right - csbiex.srWindow.Left) + 1, | ||
(csbiex.srWindow.Bottom - csbiex.srWindow.Top) + 1 | ||
}; | ||
} | ||
|
||
static ConnectionState RunConnectionToCompletion(const ITerminalConnection& connection, HANDLE outputHandle, HANDLE inputHandle) | ||
{ | ||
connection.TerminalOutput([outputHandle](const winrt::hstring& output) { | ||
WriteConsoleW(outputHandle, output.data(), output.size(), nullptr, nullptr); | ||
}); | ||
|
||
// Detach a thread to spin the console read indefinitely. | ||
// This application exits when the connection is closed, so | ||
// the connection's lifetime will outlast this thread. | ||
std::thread([connection, outputHandle, inputHandle] { | ||
ConsoleInputReader reader{ inputHandle }; | ||
reader.SetWindowSizeChangedCallback([&]() { | ||
const auto size = GetConsoleScreenSize(outputHandle); | ||
|
||
connection.Resize(size.Y, size.X); | ||
}); | ||
|
||
while (true) | ||
{ | ||
auto input = reader.Read(); | ||
if (input) | ||
{ | ||
connection.WriteInput(*input); | ||
} | ||
} | ||
}).detach(); | ||
|
||
std::condition_variable stateChangeVar; | ||
std::optional<ConnectionState> state; | ||
std::mutex stateMutex; | ||
|
||
connection.StateChanged([&](auto&& /*s*/, auto&& /*e*/) { | ||
std::unique_lock<std::mutex> lg{ stateMutex }; | ||
state = connection.State(); | ||
stateChangeVar.notify_all(); | ||
}); | ||
|
||
connection.Start(); | ||
|
||
std::unique_lock<std::mutex> lg{ stateMutex }; | ||
stateChangeVar.wait(lg, [&]() { | ||
if (!state.has_value()) | ||
{ | ||
return false; | ||
} | ||
return state.value() == ConnectionState::Closed || state.value() == ConnectionState::Failed; | ||
}); | ||
|
||
return state.value(); | ||
} | ||
|
||
int wmain(int /*argc*/, wchar_t** /*argv*/) | ||
{ | ||
winrt::init_apartment(winrt::apartment_type::single_threaded); | ||
|
||
DWORD inputMode{}, outputMode{}; | ||
HANDLE conIn{ GetStdHandle(STD_INPUT_HANDLE) }, conOut{ GetStdHandle(STD_OUTPUT_HANDLE) }; | ||
UINT codepage{ GetConsoleCP() }, outputCodepage{ GetConsoleOutputCP() }; | ||
|
||
RETURN_IF_WIN32_BOOL_FALSE(GetConsoleMode(conIn, &inputMode)); | ||
RETURN_IF_WIN32_BOOL_FALSE(GetConsoleMode(conOut, &outputMode)); | ||
|
||
RETURN_IF_WIN32_BOOL_FALSE(SetConsoleMode(conIn, ENABLE_WINDOW_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT)); | ||
RETURN_IF_WIN32_BOOL_FALSE(SetConsoleMode(conOut, ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_WRAP_AT_EOL_OUTPUT | DISABLE_NEWLINE_AUTO_RETURN)); | ||
RETURN_IF_WIN32_BOOL_FALSE(SetConsoleCP(CP_UTF8)); | ||
RETURN_IF_WIN32_BOOL_FALSE(SetConsoleOutputCP(CP_UTF8)); | ||
|
||
auto restoreConsoleModes = wil::scope_exit([&]() { | ||
SetConsoleMode(conIn, inputMode); | ||
SetConsoleMode(conOut, outputMode); | ||
SetConsoleCP(codepage); | ||
SetConsoleOutputCP(outputCodepage); | ||
}); | ||
|
||
const auto size = GetConsoleScreenSize(conOut); | ||
|
||
AzureConnection azureConn{ gsl::narrow_cast<uint32_t>(size.Y), gsl::narrow_cast<uint32_t>(size.X) }; | ||
|
||
const auto state = RunConnectionToCompletion(azureConn, conOut, conIn); | ||
|
||
return state == ConnectionState::Closed ? 0 : 1; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<packages> | ||
<package id="Microsoft.Windows.CppWinRT" version="2.0.190730.2" targetFramework="native" /> | ||
</packages> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
#include "pch.h" |
Oops, something went wrong.