diff --git a/NOTICE.md b/NOTICE.md index 623e3fb70..120eb7683 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -47,3 +47,33 @@ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` + +## telnetpp + +**Source**: https://github.com/KazDragon/telnetpp + +### License + +``` +The MIT License (MIT) + +Copyright (c) 2015-2017 Matthew Chaplain a.k.a KazDragon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/src/cascadia/CascadiaPackage/ProfileIcons/{550ce7b8-d500-50ad-8a1a-c400c3262db3}.scale-100.png b/src/cascadia/CascadiaPackage/ProfileIcons/{550ce7b8-d500-50ad-8a1a-c400c3262db3}.scale-100.png new file mode 100644 index 000000000..6d57b166f Binary files /dev/null and b/src/cascadia/CascadiaPackage/ProfileIcons/{550ce7b8-d500-50ad-8a1a-c400c3262db3}.scale-100.png differ diff --git a/src/cascadia/CascadiaPackage/ProfileIcons/{550ce7b8-d500-50ad-8a1a-c400c3262db3}.scale-200.png b/src/cascadia/CascadiaPackage/ProfileIcons/{550ce7b8-d500-50ad-8a1a-c400c3262db3}.scale-200.png new file mode 100644 index 000000000..ebba22951 Binary files /dev/null and b/src/cascadia/CascadiaPackage/ProfileIcons/{550ce7b8-d500-50ad-8a1a-c400c3262db3}.scale-200.png differ diff --git a/src/cascadia/TerminalApp/App.cpp b/src/cascadia/TerminalApp/App.cpp index 51cb82c54..2a949461b 100644 --- a/src/cascadia/TerminalApp/App.cpp +++ b/src/cascadia/TerminalApp/App.cpp @@ -49,8 +49,8 @@ namespace winrt::TerminalApp::implementation if (content == nullptr) { auto logic = Logic(); + logic.RunAsUwp(); // Must set UWP status first, settings might change based on it. logic.LoadSettings(); - logic.RunAsUwp(); logic.Create(); auto page = logic.GetRoot().as(); diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 522f91106..b9376a246 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -129,9 +129,24 @@ namespace winrt::TerminalApp::implementation _root = winrt::make_self(); } + // Method Decscription: + // - Called around the codebase to discover if this is a UWP where we need to turn off specific settings. + // Arguments: + // - - reports internal state + // Return Value: + // - True if UWP, false otherwise. + bool AppLogic::IsUwp() const noexcept + { + return _isUwp; + } + // Method Description: // - Called by UWP context invoker to let us know that we may have to change some of our behaviors // for being a UWP + // Arguments: + // - (sets to UWP = true, one way change) + // Return Value: + // - void AppLogic::RunAsUwp() { _isUwp = true; @@ -438,7 +453,7 @@ namespace winrt::TerminalApp::implementation try { - auto newSettings = CascadiaSettings::LoadAll(); + auto newSettings = _isUwp ? CascadiaSettings::LoadUniversal() : CascadiaSettings::LoadAll(); _settings = std::move(newSettings); const auto& warnings = _settings->GetWarnings(); hr = warnings.size() == 0 ? S_OK : S_FALSE; diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index 66e5027d6..01affca29 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -28,6 +28,7 @@ namespace winrt::TerminalApp::implementation ~AppLogic() = default; void Create(); + bool IsUwp() const noexcept; void RunAsUwp(); void LoadSettings(); [[nodiscard]] std::shared_ptr<::TerminalApp::CascadiaSettings> GetSettings() const noexcept; diff --git a/src/cascadia/TerminalApp/AppLogic.idl b/src/cascadia/TerminalApp/AppLogic.idl index 8be0b6ead..e99211bcf 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -22,6 +22,7 @@ namespace TerminalApp // registered?" when it definitely is. void Create(); + Boolean IsUwp(); void RunAsUwp(); void LoadSettings(); diff --git a/src/cascadia/TerminalApp/CascadiaSettings.h b/src/cascadia/TerminalApp/CascadiaSettings.h index d782786c6..b34a9862b 100644 --- a/src/cascadia/TerminalApp/CascadiaSettings.h +++ b/src/cascadia/TerminalApp/CascadiaSettings.h @@ -48,6 +48,7 @@ public: static std::unique_ptr LoadDefaults(); static std::unique_ptr LoadAll(); + static std::unique_ptr LoadUniversal(); static const CascadiaSettings& GetCurrentAppSettings(); diff --git a/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp index b784db398..4daee90ec 100644 --- a/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp @@ -12,6 +12,7 @@ // defaults.h is a file containing the default json settings in a std::string_view #include "defaults.h" +#include "defaults-universal.h" // userDefault.h is like the above, but with a default template for the user's profiles.json. #include "userDefaults.h" // Both defaults.h and userDefaults.h are generated at build time into the @@ -121,6 +122,29 @@ std::unique_ptr CascadiaSettings::LoadAll() return resultPtr; } +// Function Description: +// - Loads a batch of settings curated for the Universal variant of the terminal app +// Arguments: +// - +// Return Value: +// - a unique_ptr to a CascadiaSettings with the connection types and settings for Universal terminal +std::unique_ptr CascadiaSettings::LoadUniversal() +{ + // We're going to do this ourselves because we want to exclude almost everything + // from the special Universal-for-developers configuration + + // Create settings and get the universal defaults loaded up. + auto resultPtr = std::make_unique(); + resultPtr->_ParseJsonString(DefaultUniversalJson, true); + resultPtr->LayerJson(resultPtr->_defaultSettings); + + // Now validate. + // If this throws, the app will catch it and use the default settings + resultPtr->_ValidateSettings(); + + return resultPtr; +} + // Function Description: // - Creates a new CascadiaSettings object initialized with settings from the // hardcoded defaults.json. diff --git a/src/cascadia/TerminalApp/LegacyProfileGeneratorNamespaces.h b/src/cascadia/TerminalApp/LegacyProfileGeneratorNamespaces.h index 30d0f636a..cf15c2b69 100644 --- a/src/cascadia/TerminalApp/LegacyProfileGeneratorNamespaces.h +++ b/src/cascadia/TerminalApp/LegacyProfileGeneratorNamespaces.h @@ -18,4 +18,5 @@ Author(s): static constexpr std::wstring_view WslGeneratorNamespace{ L"Windows.Terminal.Wsl" }; static constexpr std::wstring_view AzureGeneratorNamespace{ L"Windows.Terminal.Azure" }; +static constexpr std::wstring_view TelnetGeneratorNamespace{ L"Windows.Terminal.Telnet" }; static constexpr std::wstring_view PowershellCoreGeneratorNamespace{ L"Windows.Terminal.PowershellCore" }; diff --git a/src/cascadia/TerminalApp/TelnetGenerator.h b/src/cascadia/TerminalApp/TelnetGenerator.h new file mode 100644 index 000000000..85282b7c4 --- /dev/null +++ b/src/cascadia/TerminalApp/TelnetGenerator.h @@ -0,0 +1,19 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- TelnetGenerator + +Abstract: +- Information needed to detect a Telnet connection type. + +Author(s): +- Michael Niksa - 2019-12-05 + +--*/ + +#pragma once + +// {311153fb-d3f0-4ac6-b920-038de7cf5289} +static constexpr GUID TelnetConnectionType = { 0x311153fb, 0xd3f0, 0x4ac6, { 0xb9, 0x20, 0x03, 0x8d, 0xe7, 0xcf, 0x52, 0x89 } }; diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 8e6158a5b..471101f54 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -13,6 +13,7 @@ #include #include "AzureCloudShellGenerator.h" // For AzureConnectionType +#include "TelnetGenerator.h" // For TelnetConnectionType #include "TabRowControl.h" using namespace winrt; @@ -340,35 +341,40 @@ namespace winrt::TerminalApp::implementation // add static items { - // Create the settings button. - auto settingsItem = WUX::Controls::MenuFlyoutItem{}; - settingsItem.Text(RS_(L"SettingsMenuItem")); + const auto isUwp = ::winrt::Windows::UI::Xaml::Application::Current().as<::winrt::TerminalApp::App>().Logic().IsUwp(); - WUX::Controls::SymbolIcon ico{}; - ico.Symbol(WUX::Controls::Symbol::Setting); - settingsItem.Icon(ico); - - settingsItem.Click({ this, &TerminalPage::_SettingsButtonOnClick }); - newTabFlyout.Items().Append(settingsItem); - - auto settingsKeyChord = keyBindings.GetKeyBindingForAction(ShortcutAction::OpenSettings); - if (settingsKeyChord) + if (!isUwp) { - _SetAcceleratorForMenuItem(settingsItem, settingsKeyChord); + // Create the settings button. + auto settingsItem = WUX::Controls::MenuFlyoutItem{}; + settingsItem.Text(RS_(L"SettingsMenuItem")); + + WUX::Controls::SymbolIcon ico{}; + ico.Symbol(WUX::Controls::Symbol::Setting); + settingsItem.Icon(ico); + + settingsItem.Click({ this, &TerminalPage::_SettingsButtonOnClick }); + newTabFlyout.Items().Append(settingsItem); + + auto settingsKeyChord = keyBindings.GetKeyBindingForAction(ShortcutAction::OpenSettings); + if (settingsKeyChord) + { + _SetAcceleratorForMenuItem(settingsItem, settingsKeyChord); + } + + // Create the feedback button. + auto feedbackFlyout = WUX::Controls::MenuFlyoutItem{}; + feedbackFlyout.Text(RS_(L"FeedbackMenuItem")); + + WUX::Controls::FontIcon feedbackIcon{}; + feedbackIcon.Glyph(L"\xE939"); + feedbackIcon.FontFamily(Media::FontFamily{ L"Segoe MDL2 Assets" }); + feedbackFlyout.Icon(feedbackIcon); + + feedbackFlyout.Click({ this, &TerminalPage::_FeedbackButtonOnClick }); + newTabFlyout.Items().Append(feedbackFlyout); } - // Create the feedback button. - auto feedbackFlyout = WUX::Controls::MenuFlyoutItem{}; - feedbackFlyout.Text(RS_(L"FeedbackMenuItem")); - - WUX::Controls::FontIcon feedbackIcon{}; - feedbackIcon.Glyph(L"\xE939"); - feedbackIcon.FontFamily(Media::FontFamily{ L"Segoe MDL2 Assets" }); - feedbackFlyout.Icon(feedbackIcon); - - feedbackFlyout.Click({ this, &TerminalPage::_FeedbackButtonOnClick }); - newTabFlyout.Items().Append(feedbackFlyout); - // Create the about button. auto aboutFlyout = WUX::Controls::MenuFlyoutItem{}; aboutFlyout.Text(RS_(L"AboutMenuItem")); @@ -527,6 +533,12 @@ namespace winrt::TerminalApp::implementation settings.InitialCols()); } + else if (profile->HasConnectionType() && + profile->GetConnectionType() == TelnetConnectionType) + { + connection = TerminalConnection::TelnetConnection(settings.Commandline()); + } + else { auto conhostConn = TerminalConnection::ConptyConnection(settings.Commandline(), diff --git a/src/cascadia/TerminalApp/defaults-universal.json b/src/cascadia/TerminalApp/defaults-universal.json new file mode 100644 index 000000000..bbf087648 --- /dev/null +++ b/src/cascadia/TerminalApp/defaults-universal.json @@ -0,0 +1,105 @@ +// THIS IS AN AUTO-GENERATED FILE! Changes to this file will be ignored. +{ + "alwaysShowTabs": true, + "defaultProfile": "{550ce7b8-d500-50ad-8a1a-c400c3262db3}", + "initialCols": 120, + "initialRows": 30, + "requestedTheme": "system", + "showTabsInTitlebar": false, + "showTerminalTitleInTitlebar": true, + "wordDelimiters": " /\\()\"'-.,:;<>~!@#$%^&*|+=[]{}~?\u2502", + + "profiles": + [ + { + "guid": "{550ce7b8-d500-50ad-8a1a-c400c3262db3}", + "name": "Telnet Loopback", + "commandline": "ms-telnet-loop://127.0.0.1:23", + "connectionType" : "{311153fb-d3f0-4ac6-b920-038de7cf5289}", + "hidden": false, + "startingDirectory": "%USERPROFILE%", + "closeOnExit": "graceful", + "colorScheme": "Vintage", + "cursorColor": "#FFFFFF", + "cursorShape": "bar", + "icon": "ms-appx:///ProfileIcons/{550ce7b8-d500-50ad-8a1a-c400c3262db3}.png", + "padding": "8, 8, 8, 8", + "snapOnInput": true, + "useAcrylic": false + } + ], + "schemes": + [ + { + "name": "Vintage", + "foreground": "#C0C0C0", + "background": "#000000", + "black": "#000000", + "red": "#800000", + "green": "#008000", + "yellow": "#808000", + "blue": "#000080", + "purple": "#800080", + "cyan": "#008080", + "white": "#C0C0C0", + "brightBlack": "#808080", + "brightRed": "#FF0000", + "brightGreen": "#00FF00", + "brightYellow": "#FFFF00", + "brightBlue": "#0000FF", + "brightPurple": "#FF00FF", + "brightCyan": "#00FFFF", + "brightWhite": "#FFFFFF" + } + ], + "keybindings": + [ + { "command": "closePane", "keys": [ "ctrl+shift+w" ] }, + { "command": "closeWindow", "keys": [ "alt+f4" ] }, + { "command": "copy", "keys": [ "ctrl+shift+c" ] }, + { "command": "decreaseFontSize", "keys": [ "ctrl+-" ] }, + { "command": "duplicateTab", "keys": [ "ctrl+shift+d" ] }, + { "command": "increaseFontSize", "keys": [ "ctrl+=" ] }, + { "command": { "action": "moveFocus", "direction": "down" }, "keys": [ "alt+down" ] }, + { "command": { "action": "moveFocus", "direction": "left" }, "keys": [ "alt+left" ] }, + { "command": { "action": "moveFocus", "direction": "right" }, "keys": [ "alt+right" ] }, + { "command": { "action": "moveFocus", "direction": "up" }, "keys": [ "alt+up" ] }, + { "command": "newTab", "keys": [ "ctrl+shift+t" ] }, + { "command": { "action": "newTab", "index": 0 }, "keys": ["ctrl+shift+1"] }, + { "command": { "action": "newTab", "index": 1 }, "keys": ["ctrl+shift+2"] }, + { "command": { "action": "newTab", "index": 2 }, "keys": ["ctrl+shift+3"] }, + { "command": { "action": "newTab", "index": 3 }, "keys": ["ctrl+shift+4"] }, + { "command": { "action": "newTab", "index": 4 }, "keys": ["ctrl+shift+5"] }, + { "command": { "action": "newTab", "index": 5 }, "keys": ["ctrl+shift+6"] }, + { "command": { "action": "newTab", "index": 6 }, "keys": ["ctrl+shift+7"] }, + { "command": { "action": "newTab", "index": 7 }, "keys": ["ctrl+shift+8"] }, + { "command": { "action": "newTab", "index": 8 }, "keys": ["ctrl+shift+9"] }, + { "command": "nextTab", "keys": [ "ctrl+tab" ] }, + { "command": "openNewTabDropdown", "keys": [ "ctrl+shift+space" ] }, + { "command": "openSettings", "keys": [ "ctrl+," ] }, + { "command": "paste", "keys": [ "ctrl+shift+v" ] }, + { "command": "prevTab", "keys": [ "ctrl+shift+tab" ] }, + { "command": "resetFontSize", "keys": ["ctrl+0"]}, + { "command": { "action": "resizePane", "direction": "down" }, "keys": [ "alt+shift+down" ] }, + { "command": { "action": "resizePane", "direction": "left" }, "keys": [ "alt+shift+left" ] }, + { "command": { "action": "resizePane", "direction": "right" }, "keys": [ "alt+shift+right" ] }, + { "command": { "action": "resizePane", "direction": "up" }, "keys": [ "alt+shift+up" ] }, + { "command": "scrollDown", "keys": [ "ctrl+shift+down" ] }, + { "command": "scrollDownPage", "keys": [ "ctrl+shift+pgdn" ] }, + { "command": "scrollUp", "keys": [ "ctrl+shift+up" ] }, + { "command": "scrollUpPage", "keys": [ "ctrl+shift+pgup" ] }, + { "command": { "action": "splitPane", "split": "horizontal"}, "keys": [ "alt+shift+-" ] }, + { "command": { "action": "splitPane", "split": "vertical"}, "keys": [ "alt+shift+plus" ] }, + { "command": { "action": "switchToTab", "index": 0 }, "keys": ["ctrl+alt+1"] }, + { "command": { "action": "switchToTab", "index": 1 }, "keys": ["ctrl+alt+2"] }, + { "command": { "action": "switchToTab", "index": 2 }, "keys": ["ctrl+alt+3"] }, + { "command": { "action": "switchToTab", "index": 3 }, "keys": ["ctrl+alt+4"] }, + { "command": { "action": "switchToTab", "index": 4 }, "keys": ["ctrl+alt+5"] }, + { "command": { "action": "switchToTab", "index": 5 }, "keys": ["ctrl+alt+6"] }, + { "command": { "action": "switchToTab", "index": 6 }, "keys": ["ctrl+alt+7"] }, + { "command": { "action": "switchToTab", "index": 7 }, "keys": ["ctrl+alt+8"] }, + { "command": { "action": "switchToTab", "index": 8 }, "keys": ["ctrl+alt+9"] }, + { "command": "toggleFullscreen", "keys": [ "alt+enter" ] }, + { "command": "toggleFullscreen", "keys": [ "f11" ] } + ] +} diff --git a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj index 634ed95c0..acb6de792 100644 --- a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj @@ -10,14 +10,12 @@ StaticLibrary Console true - true - false - - @@ -86,27 +81,27 @@ + - + ../ShortcutActionDispatch.idl - + ../ActionArgs.idl - + ../ActionArgs.idl - + ../AppKeyBindings.idl - + ../App.xaml - + ../AppLogic.idl - @@ -141,41 +136,39 @@ Create - + ../AppKeyBindings.idl - + ../ShortcutActionDispatch.idl - + ../ActionArgs.idl - + ../ActionArgs.idl - + ../App.xaml - + ../App.xaml - + ../AppLogic.idl - NotUsing - - + ../App.xaml @@ -199,13 +192,11 @@ Code - - - {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE} false - - <_BinRoot Condition="'$(Platform)' != 'Win32'">$(OpenConsoleDir)$(Platform)\$(Configuration)\ <_BinRoot Condition="'$(Platform)' == 'Win32'">$(OpenConsoleDir)$(Configuration)\ - Warning - - $(_BinRoot)TerminalSettings\Microsoft.Terminal.Settings.winmd true false false - $(_BinRoot)TerminalConnection\Microsoft.Terminal.TerminalConnection.winmd true false false - $(_BinRoot)TerminalControl\Microsoft.Terminal.TerminalControl.winmd true false false - - @@ -279,11 +260,8 @@ WindowsApp.lib;shell32.lib;%(AdditionalDependencies) - - - Microsoft.Toolkit.Win32.UI.XamlHost.dll @@ -318,7 +295,6 @@ - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. @@ -326,8 +302,6 @@ - - - + + + + + - + - - + \ No newline at end of file diff --git a/src/cascadia/TerminalConnection/AzureConnection.cpp b/src/cascadia/TerminalConnection/AzureConnection.cpp index 99b4c686c..067fb7cb5 100644 --- a/src/cascadia/TerminalConnection/AzureConnection.cpp +++ b/src/cascadia/TerminalConnection/AzureConnection.cpp @@ -2,6 +2,11 @@ // Licensed under the MIT license. #include "pch.h" + +// We have to define GSL here, not PCH +// because TelnetConnection has a conflicting GSL implementation. +#include + #include "AzureConnection.h" #include "AzureClientID.h" #include diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp index 06319381b..4f379a500 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.cpp +++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp @@ -2,6 +2,11 @@ // Licensed under the MIT license. #include "pch.h" + +// We have to define GSL here, not PCH +// because TelnetConnection has a conflicting GSL implementation. +#include + #include "ConptyConnection.h" #include diff --git a/src/cascadia/TerminalConnection/Resources/en-US/Resources.resw b/src/cascadia/TerminalConnection/Resources/en-US/Resources.resw index 43a0242a4..0be15c690 100644 --- a/src/cascadia/TerminalConnection/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalConnection/Resources/en-US/Resources.resw @@ -214,4 +214,7 @@ The first argument is the hexadecimal error code. The second is the process name. If this resource spans multiple lines, it will not be displayed properly. Yeah. + + Could not connect to telnet server. + \ No newline at end of file diff --git a/src/cascadia/TerminalConnection/TelnetConnection.cpp b/src/cascadia/TerminalConnection/TelnetConnection.cpp new file mode 100644 index 000000000..4ad25d268 --- /dev/null +++ b/src/cascadia/TerminalConnection/TelnetConnection.cpp @@ -0,0 +1,315 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "TelnetConnection.h" +#include + +#include "TelnetConnection.g.cpp" + +#include "../../types/inc/Utils.hpp" + +using namespace ::Microsoft::Console; + +constexpr std::wstring_view telnetScheme = L"telnet"; +constexpr std::wstring_view msTelnetLoopbackScheme = L"ms-telnet-loop"; + +namespace winrt::Microsoft::Terminal::TerminalConnection::implementation +{ + TelnetConnection::TelnetConnection(const hstring& uri) : + _reader{ nullptr }, + _writer{ nullptr }, + _uri{ uri } + { + _session.install(_nawsServer); + _nawsServer.activate([](auto&&) {}); + } + + // Method description: + // - ascribes to the ITerminalConnection interface + // - creates the output thread + void TelnetConnection::Start() + try + { + // Create our own output handling thread + // Each connection needs to make sure to drain the output from its backing host. + _hOutputThread.reset(CreateThread( + nullptr, + 0, + [](LPVOID lpParameter) { + auto pInstance = reinterpret_cast(lpParameter); + return pInstance->_outputThread(); + }, + this, + 0, + nullptr)); + + THROW_LAST_ERROR_IF_NULL(_hOutputThread); + + _transitionToState(ConnectionState::Connecting); + + // Set initial winodw title. + _TerminalOutputHandlers(L"\x1b]0;Telnet\x7"); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + _transitionToState(ConnectionState::Failed); + } + + // Method description: + // - ascribes to the ITerminalConnection interface + // - handles the different possible inputs in the different states + // Arguments: + // the user's input + void TelnetConnection::WriteInput(hstring const& data) + { + if (!_isStateOneOf(ConnectionState::Connected, ConnectionState::Connecting)) + { + return; + } + + auto str = winrt::to_string(data); + if (str.size() == 1 && str.at(0) == L'\r') + { + str = "\r\n"; + } + + telnetpp::bytes bytes(reinterpret_cast(str.data()), str.size()); + _session.send(bytes, [=](telnetpp::bytes data) { + _socketSend(data); + }); + } + + // Method description: + // - ascribes to the ITerminalConnection interface + // - resizes the terminal + // Arguments: + // - the new rows/cols values + void TelnetConnection::Resize(uint32_t rows, uint32_t columns) + { + if (_prevResize.has_value() && _prevResize.value().first == rows && _prevResize.value().second == columns) + { + return; + } + + _prevResize.emplace(std::pair{ rows, columns }); + + _nawsServer.set_window_size(gsl::narrow(columns), + gsl::narrow(rows), + [=](telnetpp::subnegotiation sub) { + _session.send(sub, + [=](telnetpp::bytes data) { + _socketBufferedSend(data); + }); + _socketFlushBuffer(); + }); + } + + // Method description: + // - ascribes to the ITerminalConnection interface + // - closes the socket connection and the output thread + void TelnetConnection::Close() + try + { + if (_transitionToState(ConnectionState::Closing)) + { + _socket.Close(); + if (_hOutputThread) + { + // Tear down our output thread + WaitForSingleObject(_hOutputThread.get(), INFINITE); + _hOutputThread.reset(); + } + + _transitionToState(ConnectionState::Closed); + } + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + _transitionToState(ConnectionState::Failed); + } + + // Method description: + // - this is the output thread, where we initiate the connection to the remote host + // and establish a socket connection + // Return value: + // - return status + DWORD TelnetConnection::_outputThread() + try + { + while (true) + { + if (_isStateOneOf(ConnectionState::Failed)) + { + _TerminalOutputHandlers(RS_(L"TelnetInternetOrServerIssue") + L"\r\n"); + return E_FAIL; + } + else if (_isStateAtOrBeyond(ConnectionState::Closing)) + { + return S_FALSE; + } + else if (_isStateOneOf(ConnectionState::Connecting)) + { + try + { + const auto uri = Windows::Foundation::Uri(_uri); + const auto host = Windows::Networking::HostName(uri.Host()); + + bool autoLogin = false; + // If we specified the special ms loopback scheme, then set autologin and proceed below. + if (msTelnetLoopbackScheme == uri.SchemeName()) + { + autoLogin = true; + } + // Otherwise, make sure we said telnet://, anything else is not supported here. + else if (telnetScheme != uri.SchemeName()) + { + THROW_HR(E_INVALIDARG); + } + + _socket.ConnectAsync(host, winrt::to_hstring(uri.Port())).get(); + _writer = Windows::Storage::Streams::DataWriter(_socket.OutputStream()); + _reader = Windows::Storage::Streams::DataReader(_socket.InputStream()); + _reader.InputStreamOptions(Windows::Storage::Streams::InputStreamOptions::Partial); // returns when 1 or more bytes ready. + _transitionToState(ConnectionState::Connected); + + if (autoLogin) + { + // Send newline to bypass User Name prompt. + const auto newline = winrt::to_hstring("\r\n"); + WriteInput(newline); + + // Wait for login. + Sleep(1000); + + // Send "cls" enter to clear the thing and just look like a prompt. + const auto clearScreen = winrt::to_hstring("cls\r\n"); + WriteInput(clearScreen); + } + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + _transitionToState(ConnectionState::Failed); + } + } + else if (_isStateOneOf(ConnectionState::Connected)) + { + // Read from socket + const auto amountReceived = _socketReceive(_receiveBuffer); + + _session.receive( + telnetpp::bytes{ _receiveBuffer.data(), amountReceived }, + [=](telnetpp::bytes data, + std::function const& send) { + _applicationReceive(data, send); + }, + [=](telnetpp::bytes data) { + _socketSend(data); + }); + } + } + } + catch (...) + { + // If the exception was thrown while we were already supposed to be closing, fine. We're closed. + // This is because the socket got mad things were being torn down. + if (_isStateAtOrBeyond(ConnectionState::Closing)) + { + _transitionToState(ConnectionState::Closed); + return S_OK; + } + else + { + LOG_CAUGHT_EXCEPTION(); + _transitionToState(ConnectionState::Failed); + return E_FAIL; + } + } + + // Routine Description: + // - Call to buffer up bytes to send to the remote device. + // - You must flush before they'll go out. + // Arguments: + // - data - View of bytes to be sent + // Return Value: + // - + void TelnetConnection::_socketBufferedSend(telnetpp::bytes data) + { + const uint8_t* first = data.data(); + const uint8_t* last = data.data() + data.size(); + const winrt::array_view arrayView(first, last); + _writer.WriteBytes(arrayView); + } + + // Routine Description: + // - Flushes any buffered bytes to the underlying socket + // Arguments: + // - + // Return Value: + // - + fire_and_forget TelnetConnection::_socketFlushBuffer() + { + co_await _writer.StoreAsync(); + } + + // Routine Description: + // - Used to send bytes into the socket to the remote device + // Arguments: + // - data - View of bytes to be sent + // Return Value: + // - + void TelnetConnection::_socketSend(telnetpp::bytes data) + { + _socketBufferedSend(data); + _socketFlushBuffer(); + } + + // Routine Description: + // - Reads bytes from the socket into the given array. + // Arguments: + // - buffer - The array of bytes to use for storage + // Return Value: + // - The number of bytes actually read (less than or equal to input array size) + size_t TelnetConnection::_socketReceive(gsl::span buffer) + { + const auto bytesLoaded = _reader.LoadAsync(gsl::narrow(buffer.size())).get(); + + // winrt::array_view, despite having a pointer and size constructor + // hides it as protected. + // So we have to get first/last (even though cppcorechecks will be + // mad at us for it) to use a public array_view constructor. + // The WinRT team isn't fixing this because std::span is coming + // soon and that will do it. + // So just do this for now and suppress the warnings. + const auto first = buffer.data(); + const auto last = first + bytesLoaded; + const winrt::array_view arrayView(first, last); + _reader.ReadBytes(arrayView); + return bytesLoaded; + } + + // Routine Description: + // - Called by telnetpp framework when application data is received on the channel + // In contrast, telnet metadata payload is consumed by telnetpp and not forwarded to us. + // Arguments: + // - data - The relevant application-level payload received + // - send - A function where we can send a reply to given data immediately + // in reaction to the received message. + // Return Value: + // - + void TelnetConnection::_applicationReceive(telnetpp::bytes data, + std::function const& /*send*/) + { + // Convert telnetpp bytes to standard string_view + const auto stringView = std::string_view{ reinterpret_cast(data.data()), gsl::narrow(data.size()) }; + + // Convert to hstring + const auto hstr = winrt::to_hstring(stringView); + + // Pass the output to our registered event handlers + _TerminalOutputHandlers(hstr); + } +} diff --git a/src/cascadia/TerminalConnection/TelnetConnection.h b/src/cascadia/TerminalConnection/TelnetConnection.h new file mode 100644 index 000000000..ca632c436 --- /dev/null +++ b/src/cascadia/TerminalConnection/TelnetConnection.h @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "TelnetConnection.g.h" + +#include +#include + +#pragma warning(push) +#pragma warning(disable : 4100) +#pragma warning(disable : 4251) +#include +#include +#include +#pragma warning(pop) + +#include "winrt/Windows.Networking.Sockets.h" +#include "winrt/Windows.Storage.Streams.h" + +#include "../cascadia/inc/cppwinrt_utils.h" +#include "ConnectionStateHolder.h" + +namespace winrt::Microsoft::Terminal::TerminalConnection::implementation +{ + struct TelnetConnection : TelnetConnectionT, ConnectionStateHolder + { + TelnetConnection(const hstring& uri); + + void Start(); + void WriteInput(hstring const& data); + void Resize(uint32_t rows, uint32_t columns); + void Close(); + + WINRT_CALLBACK(TerminalOutput, TerminalOutputHandler); + + private: + hstring _uri; + + void _applicationReceive(telnetpp::bytes data, + std::function const& send); + + void _socketBufferedSend(telnetpp::bytes data); + fire_and_forget _socketFlushBuffer(); + void _socketSend(telnetpp::bytes data); + size_t _socketReceive(gsl::span buffer); + + telnetpp::session _session; + // NAWS = Negotiation About Window Size + telnetpp::options::naws::server _nawsServer; + Windows::Networking::Sockets::StreamSocket _socket; + Windows::Storage::Streams::DataWriter _writer; + Windows::Storage::Streams::DataReader _reader; + + std::optional> _prevResize; + + static constexpr size_t _receiveBufferSize = 1024; + std::array _receiveBuffer; + + wil::unique_handle _hOutputThread; + + DWORD _outputThread(); + }; +} + +namespace winrt::Microsoft::Terminal::TerminalConnection::factory_implementation +{ + struct TelnetConnection : TelnetConnectionT + { + }; +} diff --git a/src/cascadia/TerminalConnection/TelnetConnection.idl b/src/cascadia/TerminalConnection/TelnetConnection.idl new file mode 100644 index 000000000..ac250b39c --- /dev/null +++ b/src/cascadia/TerminalConnection/TelnetConnection.idl @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "ITerminalConnection.idl"; + +namespace Microsoft.Terminal.TerminalConnection +{ + [default_interface] + runtimeclass TelnetConnection : ITerminalConnection + { + TelnetConnection(String uri); + }; + +} diff --git a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj index ebd9c4189..a2d3b3087 100644 --- a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj +++ b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj @@ -7,7 +7,6 @@ DynamicLibrary Console true - true - - - + + AzureConnection.idl + ConptyConnection.idl @@ -29,10 +28,15 @@ EchoConnection.idl + + TelnetConnection.idl + - - + + + AzureConnection.idl + Create @@ -43,18 +47,21 @@ ConptyConnection.idl + + TelnetConnection.idl + + -