Skip to content

Commit

Permalink
Shim the AzureConn through a conhost to stop VT bleeding (#4652)
Browse files Browse the repository at this point in the history
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
DHowett committed Feb 21, 2020
1 parent 693cdc1 commit 7d6738c
Show file tree
Hide file tree
Showing 10 changed files with 361 additions and 2 deletions.
17 changes: 17 additions & 0 deletions OpenConsole.sln
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{D3EF
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "winconpty.Tests.Feature", "src\winconpty\ft_pty\winconpty.FeatureTests.vcxproj", "{024052DE-83FB-4653-AEA4-90790D29D5BD}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TerminalAzBridge", "src\cascadia\TerminalAzBridge\TerminalAzBridge.vcxproj", "{067F0A06-FCB7-472C-96E9-B03B54E8E18D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
AuditMode|Any CPU = AuditMode|Any CPU
Expand Down Expand Up @@ -1435,6 +1437,20 @@ Global
{024052DE-83FB-4653-AEA4-90790D29D5BD}.Release|x64.Build.0 = Release|x64
{024052DE-83FB-4653-AEA4-90790D29D5BD}.Release|x86.ActiveCfg = Release|Win32
{024052DE-83FB-4653-AEA4-90790D29D5BD}.Release|x86.Build.0 = Release|Win32
{067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|Any CPU.ActiveCfg = Debug|Win32
{067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|ARM64.ActiveCfg = Debug|ARM64
{067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|ARM64.Build.0 = Debug|ARM64
{067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|x64.ActiveCfg = Debug|x64
{067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|x64.Build.0 = Debug|x64
{067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|x86.ActiveCfg = Debug|Win32
{067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Debug|x86.Build.0 = Debug|Win32
{067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|Any CPU.ActiveCfg = Release|Win32
{067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|ARM64.ActiveCfg = Release|ARM64
{067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|ARM64.Build.0 = Release|ARM64
{067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|x64.ActiveCfg = Release|x64
{067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|x64.Build.0 = Release|x64
{067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|x86.ActiveCfg = Release|Win32
{067F0A06-FCB7-472C-96E9-B03B54E8E18D}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1510,6 +1526,7 @@ Global
{6B5A44ED-918D-4747-BFB1-2472A1FCA173} = {04170EEF-983A-4195-BFEF-2321E5E38A1E}
{D3EF7B96-CD5E-47C9-B9A9-136259563033} = {04170EEF-983A-4195-BFEF-2321E5E38A1E}
{024052DE-83FB-4653-AEA4-90790D29D5BD} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB}
{067F0A06-FCB7-472C-96E9-B03B54E8E18D} = {59840756-302F-44DF-AA47-441A9D673202}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3140B1B7-C8EE-43D1-A772-D82A7061A271}
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/CascadiaPackage/CascadiaPackage.wapproj
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
<ItemGroup>
<ProjectReference Include="..\WindowsTerminal\WindowsTerminal.vcxproj" />
<ProjectReference Include="..\..\host\exe\Host.EXE.vcxproj" />
<ProjectReference Include="..\TerminalAzBridge\TerminalAzBridge.vcxproj" />
</ItemGroup>
<Target Name="OpenConsoleStompSourceProjectForWapProject" BeforeTargets="_ConvertItems">
<ItemGroup>
Expand Down
6 changes: 4 additions & 2 deletions src/cascadia/TerminalApp/TerminalPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -602,8 +602,10 @@ namespace winrt::TerminalApp::implementation
profile->GetConnectionType() == AzureConnectionType &&
TerminalConnection::AzureConnection::IsAzureConnectionAvailable())
{
connection = TerminalConnection::AzureConnection(settings.InitialRows(),
settings.InitialCols());
// TODO GH#4661: Replace this with directly using the AzCon when our VT is better
std::filesystem::path azBridgePath{ wil::GetModuleFileNameW<std::wstring>(nullptr) };
azBridgePath.replace_filename(L"TerminalAzBridge.exe");
connection = TerminalConnection::ConptyConnection(azBridgePath.wstring(), L".", L"Azure", settings.InitialRows(), settings.InitialCols(), winrt::guid());
}

else if (profile->HasConnectionType() &&
Expand Down
87 changes: 87 additions & 0 deletions src/cascadia/TerminalAzBridge/ConsoleInputReader.cpp
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;
}
33 changes: 33 additions & 0 deletions src/cascadia/TerminalAzBridge/ConsoleInputReader.h
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;
};
71 changes: 71 additions & 0 deletions src/cascadia/TerminalAzBridge/TerminalAzBridge.vcxproj
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>
104 changes: 104 additions & 0 deletions src/cascadia/TerminalAzBridge/main.cpp
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;
}
4 changes: 4 additions & 0 deletions src/cascadia/TerminalAzBridge/packages.config
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>
4 changes: 4 additions & 0 deletions src/cascadia/TerminalAzBridge/pch.cpp
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"
Loading

0 comments on commit 7d6738c

Please sign in to comment.