diff --git a/OpenConsole.sln b/OpenConsole.sln index 530f602b1..b70cea6c8 100644 --- a/OpenConsole.sln +++ b/OpenConsole.sln @@ -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 @@ -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 @@ -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} diff --git a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj index 8375c57e8..f415febc0 100644 --- a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj +++ b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj @@ -55,6 +55,7 @@ + diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index ed6b64e0c..78a8caec9 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -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(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() && diff --git a/src/cascadia/TerminalAzBridge/ConsoleInputReader.cpp b/src/cascadia/TerminalAzBridge/ConsoleInputReader.cpp new file mode 100644 index 000000000..12e7f51cb --- /dev/null +++ b/src/cascadia/TerminalAzBridge/ConsoleInputReader.cpp @@ -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 callback) +{ + _windowSizeChangedCallback = std::move(callback); +} + +std::optional ConsoleInputReader::Read() +{ + DWORD readCount{ 0 }; + + _convertedString.clear(); + while (_convertedString.empty()) + { + _buffer.resize(BufferSize); + BOOL succeeded = + ReadConsoleInputW(_handle, _buffer.data(), gsl::narrow_cast(_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; +} diff --git a/src/cascadia/TerminalAzBridge/ConsoleInputReader.h b/src/cascadia/TerminalAzBridge/ConsoleInputReader.h new file mode 100644 index 000000000..894804f45 --- /dev/null +++ b/src/cascadia/TerminalAzBridge/ConsoleInputReader.h @@ -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 callback); + std::optional Read(); + +private: + static constexpr size_t BufferSize{ 128 }; + + HANDLE _handle; + std::wstring _convertedString; + std::vector _buffer; + std::optional _highSurrogate; + std::function _windowSizeChangedCallback; +}; diff --git a/src/cascadia/TerminalAzBridge/TerminalAzBridge.vcxproj b/src/cascadia/TerminalAzBridge/TerminalAzBridge.vcxproj new file mode 100644 index 000000000..3e78cfd9b --- /dev/null +++ b/src/cascadia/TerminalAzBridge/TerminalAzBridge.vcxproj @@ -0,0 +1,71 @@ + + + + + {067F0A06-FCB7-472C-96E9-B03B54E8E18D} + Win32Proj + TerminalAzBridge + TerminalAzBridge + TerminalAzBridge + Application + false + Windows Store + true + + + + + + + + true + + + Console + + + + + true + true + + + + + + + + + + Create + + + + + + + + + + + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED} + + + + + + + + + + + WindowsLocalDebugger + + + + + + diff --git a/src/cascadia/TerminalAzBridge/main.cpp b/src/cascadia/TerminalAzBridge/main.cpp new file mode 100644 index 000000000..df51677d7 --- /dev/null +++ b/src/cascadia/TerminalAzBridge/main.cpp @@ -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 state; + std::mutex stateMutex; + + connection.StateChanged([&](auto&& /*s*/, auto&& /*e*/) { + std::unique_lock lg{ stateMutex }; + state = connection.State(); + stateChangeVar.notify_all(); + }); + + connection.Start(); + + std::unique_lock 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(size.Y), gsl::narrow_cast(size.X) }; + + const auto state = RunConnectionToCompletion(azureConn, conOut, conIn); + + return state == ConnectionState::Closed ? 0 : 1; +} diff --git a/src/cascadia/TerminalAzBridge/packages.config b/src/cascadia/TerminalAzBridge/packages.config new file mode 100644 index 000000000..e345a6ccd --- /dev/null +++ b/src/cascadia/TerminalAzBridge/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/src/cascadia/TerminalAzBridge/pch.cpp b/src/cascadia/TerminalAzBridge/pch.cpp new file mode 100644 index 000000000..398a99f66 --- /dev/null +++ b/src/cascadia/TerminalAzBridge/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" diff --git a/src/cascadia/TerminalAzBridge/pch.h b/src/cascadia/TerminalAzBridge/pch.h new file mode 100644 index 000000000..e32c4f906 --- /dev/null +++ b/src/cascadia/TerminalAzBridge/pch.h @@ -0,0 +1,36 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- pch.h + +Abstract: +- Contains external headers to include in the precompile phase of console build process. +- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). +--*/ + +#pragma once + +// Ignore checked iterators warning from VC compiler. +#define _SCL_SECURE_NO_WARNINGS + +// Block minwindef.h min/max macros to prevent conflict +#define NOMINMAX + +#define WIN32_LEAN_AND_MEAN +#include + +#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) + +#include + +#include "../inc/LibraryIncludes.h" + +#include + +#include +#include + +#include +#include