Add a "debug tap" that lets you see the VT behind a connection (#5127)

This commit adds a debugging feature that can be activated by holding
down Left Alt _and_ Right Alt when a new tab is being created, if you
have the global setting "debugFeatures" set to true. That global setting
will default to true in DEBUG builds.

That debugging feature takes the form of a split pane that shows the raw
VT sequences being written to and received from the connection.

When those buttons are held down, every connection that's created as
part of a new tab is wrapped and split into _two_ connections: one to
capture input (and stand in for the main connection) and one to capture
output (and be displayed off to the side)

Closes #3206
This commit is contained in:
Dustin L. Howett (MSFT) 2020-03-26 15:33:47 -07:00 committed by GitHub
parent 31efd69149
commit d47da2d617
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 239 additions and 2 deletions

View file

@ -0,0 +1,147 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "DebugTapConnection.h"
using namespace ::winrt::Microsoft::Terminal::TerminalConnection;
using namespace ::winrt::Windows::Foundation;
namespace winrt::Microsoft::TerminalApp::implementation
{
// DebugInputTapConnection is an implementation detail of DebugTapConnection.
// It wraps the _actual_ connection so it can hook WriteInput and forward it
// into the actual debug panel.
class DebugInputTapConnection : public winrt::implements<DebugInputTapConnection, ITerminalConnection>
{
public:
DebugInputTapConnection(winrt::com_ptr<DebugTapConnection> pairedTap, ITerminalConnection wrappedConnection) :
_pairedTap{ pairedTap },
_wrappedConnection{ std::move(wrappedConnection) }
{
}
~DebugInputTapConnection() = default;
void Start()
{
_wrappedConnection.Start();
}
void WriteInput(hstring const& data)
{
_pairedTap->_PrintInput(data);
_wrappedConnection.WriteInput(data);
}
void Resize(uint32_t rows, uint32_t columns) { _wrappedConnection.Resize(rows, columns); }
void Close() { _wrappedConnection.Close(); }
winrt::event_token TerminalOutput(TerminalOutputHandler const& args) { return _wrappedConnection.TerminalOutput(args); };
void TerminalOutput(winrt::event_token const& token) noexcept { _wrappedConnection.TerminalOutput(token); };
winrt::event_token StateChanged(TypedEventHandler<ITerminalConnection, IInspectable> const& handler) { return _wrappedConnection.StateChanged(handler); };
void StateChanged(winrt::event_token const& token) noexcept { _wrappedConnection.StateChanged(token); };
ConnectionState State() const noexcept { return _wrappedConnection.State(); }
private:
winrt::com_ptr<DebugTapConnection> _pairedTap;
ITerminalConnection _wrappedConnection;
};
DebugTapConnection::DebugTapConnection(ITerminalConnection wrappedConnection)
{
_outputRevoker = wrappedConnection.TerminalOutput(winrt::auto_revoke, { this, &DebugTapConnection::_OutputHandler });
_stateChangedRevoker = wrappedConnection.StateChanged(winrt::auto_revoke, [this](auto&& /*s*/, auto&& /*e*/) {
_StateChangedHandlers(*this, nullptr);
});
_wrappedConnection = wrappedConnection;
}
DebugTapConnection::~DebugTapConnection()
{
}
void DebugTapConnection::Start()
{
// presume the wrapped connection is started.
}
void DebugTapConnection::WriteInput(hstring const& data)
{
// If the user types into the tap side, forward it to the input side
if (auto strongInput{ _inputSide.get() })
{
auto inputAsTap{ winrt::get_self<DebugInputTapConnection>(strongInput) };
inputAsTap->WriteInput(data);
}
}
void DebugTapConnection::Resize(uint32_t /*rows*/, uint32_t /*columns*/)
{
// no resize events are propagated
}
void DebugTapConnection::Close()
{
_outputRevoker.revoke();
_stateChangedRevoker.revoke();
_wrappedConnection = nullptr;
}
ConnectionState DebugTapConnection::State() const noexcept
{
if (auto strongConnection{ _wrappedConnection.get() })
{
return strongConnection.State();
}
return ConnectionState::Failed;
}
static std::wstring _sanitizeString(const std::wstring_view str)
{
std::wstring newString{ str.begin(), str.end() };
for (auto& ch : newString)
{
if (ch < 0x20)
{
ch += 0x2400;
}
else if (ch == 0x20)
{
ch = 0x2423; // replace space with ␣
}
else if (ch == 0x7f)
{
ch = 0x2421; // replace del with ␡
}
}
return newString;
}
void DebugTapConnection::_OutputHandler(const hstring str)
{
_TerminalOutputHandlers(_sanitizeString(str));
}
// Called by the DebugInputTapConnection to print user input
void DebugTapConnection::_PrintInput(const hstring& str)
{
auto clean{ _sanitizeString(str) };
auto formatted{ wil::str_printf<std::wstring>(L"\x1b[91m%ls\x1b[m", clean.data()) };
_TerminalOutputHandlers(formatted);
}
// Wire us up so that we can forward input through
void DebugTapConnection::SetInputTap(const Microsoft::Terminal::TerminalConnection::ITerminalConnection& inputTap)
{
_inputSide = inputTap;
}
}
// Function Description
// - Takes one connection and returns two connections:
// 1. One that can be used in place of the original connection (wrapped)
// 2. One that will print raw VT sequences sent into and received _from_ the original connection.
std::tuple<ITerminalConnection, ITerminalConnection> OpenDebugTapConnection(ITerminalConnection baseConnection)
{
using namespace winrt::Microsoft::TerminalApp::implementation;
auto debugSide{ winrt::make_self<DebugTapConnection>(baseConnection) };
auto inputSide{ winrt::make_self<DebugInputTapConnection>(debugSide, baseConnection) };
debugSide->SetInputTap(*inputSide);
std::tuple<ITerminalConnection, ITerminalConnection> p{ *inputSide, *debugSide };
return p;
}

View file

@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include <winrt/Microsoft.Terminal.TerminalConnection.h>
#include "../../inc/cppwinrt_utils.h"
namespace winrt::Microsoft::TerminalApp::implementation
{
class DebugInputTapConnection;
class DebugTapConnection : public winrt::implements<DebugTapConnection, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection>
{
public:
DebugTapConnection(Microsoft::Terminal::TerminalConnection::ITerminalConnection wrappedConnection);
~DebugTapConnection();
void Start();
void WriteInput(hstring const& data);
void Resize(uint32_t rows, uint32_t columns);
void Close();
winrt::Microsoft::Terminal::TerminalConnection::ConnectionState State() const noexcept;
void SetInputTap(const Microsoft::Terminal::TerminalConnection::ITerminalConnection& inputTap);
WINRT_CALLBACK(TerminalOutput, winrt::Microsoft::Terminal::TerminalConnection::TerminalOutputHandler);
TYPED_EVENT(StateChanged, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection, winrt::Windows::Foundation::IInspectable);
private:
void _PrintInput(const hstring& data);
void _OutputHandler(const hstring str);
winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection::TerminalOutput_revoker _outputRevoker;
winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection::StateChanged_revoker _stateChangedRevoker;
winrt::weak_ref<Microsoft::Terminal::TerminalConnection::ITerminalConnection> _wrappedConnection;
winrt::weak_ref<Microsoft::Terminal::TerminalConnection::ITerminalConnection> _inputSide;
friend class DebugInputTapConnection;
};
}
std::tuple<winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection> OpenDebugTapConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection baseConnection);

View file

@ -41,6 +41,14 @@ static constexpr std::wstring_view LightThemeValue{ L"light" };
static constexpr std::wstring_view DarkThemeValue{ L"dark" };
static constexpr std::wstring_view SystemThemeValue{ L"system" };
static constexpr std::string_view DebugFeaturesKey{ "debugFeatures" };
#ifdef _DEBUG
static constexpr bool debugFeaturesDefault{ true };
#else
static constexpr bool debugFeaturesDefault{ false };
#endif
GlobalAppSettings::GlobalAppSettings() :
_keybindings{ winrt::make_self<winrt::TerminalApp::implementation::AppKeyBindings>() },
_keybindingsWarnings{},
@ -59,7 +67,8 @@ GlobalAppSettings::GlobalAppSettings() :
_tabWidthMode{ TabViewWidthMode::Equal },
_wordDelimiters{ DEFAULT_WORD_DELIMITERS },
_copyOnSelect{ false },
_launchMode{ LaunchMode::DefaultMode }
_launchMode{ LaunchMode::DefaultMode },
_debugFeatures{ debugFeaturesDefault }
{
}
@ -171,6 +180,11 @@ void GlobalAppSettings::SetConfirmCloseAllTabs(const bool confirmCloseAllTabs) n
_confirmCloseAllTabs = confirmCloseAllTabs;
}
bool GlobalAppSettings::DebugFeaturesEnabled() const noexcept
{
return _debugFeatures;
}
#pragma region ExperimentalSettings
bool GlobalAppSettings::GetShowTabsInTitlebar() const noexcept
{
@ -237,6 +251,7 @@ Json::Value GlobalAppSettings::ToJson() const
jsonObject[JsonKey(KeybindingsKey)] = _keybindings->ToJson();
jsonObject[JsonKey(ConfirmCloseAllKey)] = _confirmCloseAllTabs;
jsonObject[JsonKey(SnapToGridOnResizeKey)] = _SnapToGridOnResize;
jsonObject[JsonKey(DebugFeaturesKey)] = _debugFeatures;
return jsonObject;
}
@ -324,6 +339,9 @@ void GlobalAppSettings::LayerJson(const Json::Value& json)
}
JsonUtils::GetBool(json, SnapToGridOnResizeKey, _SnapToGridOnResize);
// GetBool will only override the current value if the key exists
JsonUtils::GetBool(json, DebugFeaturesKey, _debugFeatures);
}
// Method Description:

View file

@ -77,6 +77,8 @@ public:
winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode GetTabWidthMode() const noexcept;
bool DebugFeaturesEnabled() const noexcept;
Json::Value ToJson() const;
static GlobalAppSettings FromJson(const Json::Value& json);
void LayerJson(const Json::Value& json);
@ -115,6 +117,8 @@ private:
winrt::TerminalApp::LaunchMode _launchMode;
bool _debugFeatures;
static winrt::Windows::UI::Xaml::ElementTheme _ParseTheme(const std::wstring& themeString) noexcept;
static std::wstring_view _SerializeTheme(const winrt::Windows::UI::Xaml::ElementTheme theme) noexcept;

View file

@ -15,6 +15,7 @@
#include "AzureCloudShellGenerator.h" // For AzureConnectionType
#include "TelnetGenerator.h" // For TelnetConnectionType
#include "TabRowControl.h"
#include "DebugTapConnection.h"
using namespace winrt;
using namespace winrt::Windows::UI::Xaml;
@ -564,7 +565,22 @@ namespace winrt::TerminalApp::implementation
// Initialize the new tab
// Create a connection based on the values in our settings object.
const auto connection = _CreateConnectionFromSettings(profileGuid, settings);
auto connection = _CreateConnectionFromSettings(profileGuid, settings);
TerminalConnection::ITerminalConnection debugConnection{ nullptr };
if (_settings->GlobalSettings().DebugFeaturesEnabled())
{
const CoreWindow window = CoreWindow::GetForCurrentThread();
const auto rAltState = window.GetKeyState(VirtualKey::RightMenu);
const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu);
const bool bothAltsPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) &&
WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down);
if (bothAltsPressed)
{
std::tie(connection, debugConnection) = OpenDebugTapConnection(connection);
}
}
TermControl term{ settings, connection };
// Add the new tab to the list of our tabs.
@ -614,6 +630,14 @@ namespace winrt::TerminalApp::implementation
}
});
if (debugConnection) // this will only be set if global debugging is on and tap is active
{
TermControl newControl{ settings, debugConnection };
_RegisterTerminalEvents(newControl, *newTabImpl);
// Split (auto) with the debug tap.
newTabImpl->SplitPane(SplitState::Automatic, profileGuid, newControl);
}
// This kicks off TabView::SelectionChanged, in response to which
// we'll attach the terminal's Xaml control to the Xaml root.
_tabView.SelectedItem(tabViewItem);

View file

@ -107,6 +107,7 @@
<ClInclude Include="../ActionAndArgs.h">
<DependentUpon>../ActionArgs.idl</DependentUpon>
</ClInclude>
<ClInclude Include="../DebugTapConnection.h" />
<ClInclude Include="../AppKeyBindings.h">
<DependentUpon>../AppKeyBindings.idl</DependentUpon>
</ClInclude>
@ -153,6 +154,7 @@
<ClCompile Include="../WslDistroGenerator.cpp" />
<ClCompile Include="../AzureCloudShellGenerator.cpp" />
<ClCompile Include="../Pane.LayoutSizeNode.cpp" />
<ClCompile Include="../DebugTapConnection.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>