mirror of
https://github.com/microsoft/terminal.git
synced 2026-05-17 15:36:35 +00:00
Compare commits
14 Commits
dev/migrie
...
dev/duhowe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea1f6cce69 | ||
|
|
a959be4cac | ||
|
|
41f8ca3e5b | ||
|
|
a76925e20f | ||
|
|
0197d501d9 | ||
|
|
f6c51ffd3f | ||
|
|
d764596b6a | ||
|
|
1b8163f42c | ||
|
|
70b7e6761f | ||
|
|
92b2ca2fda | ||
|
|
e9517a6509 | ||
|
|
bbb2bb9f05 | ||
|
|
4fbad3b7c1 | ||
|
|
4a83889bdd |
162
src/cascadia/TerminalApp/Blackbox.cpp
Normal file
162
src/cascadia/TerminalApp/Blackbox.cpp
Normal file
@@ -0,0 +1,162 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include <til/spsc.h>
|
||||
#include <til/unicode.h>
|
||||
#include <til/u8u16convert.h>
|
||||
|
||||
#include "Blackbox.h"
|
||||
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 26446)
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string u16json8(std::wstring_view s)
|
||||
{
|
||||
std::string o{};
|
||||
o.reserve(s.size() * 3);
|
||||
for (const auto& v : til::utf16_iterator{ s })
|
||||
{
|
||||
char cu[6];
|
||||
int i = 0;
|
||||
char32_t ch = v[0];
|
||||
|
||||
if (v.size() > 1) [[unlikely]]
|
||||
{
|
||||
ch = til::combine_surrogates(v[0], v[1]);
|
||||
}
|
||||
|
||||
if (ch < 0x20 || ch == '"' || ch == '\\') [[unlikely]]
|
||||
{
|
||||
cu[i++] = '\\';
|
||||
switch (ch)
|
||||
{
|
||||
case '\n':
|
||||
cu[i++] = 'n';
|
||||
break;
|
||||
case '\r':
|
||||
cu[i++] = 'r';
|
||||
break;
|
||||
case '\\':
|
||||
case '\"':
|
||||
cu[i++] = static_cast<char>(ch);
|
||||
break;
|
||||
default:
|
||||
{
|
||||
static constexpr char hexit[] = "0123456789abcdef";
|
||||
cu[i++] = 'u';
|
||||
cu[i++] = '0';
|
||||
cu[i++] = '0';
|
||||
cu[i++] = hexit[(ch >> 4) & 0x0f];
|
||||
cu[i++] = hexit[(ch) & 0x0f];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (ch <= 0x7F) [[likely]]
|
||||
{
|
||||
// Single-byte character (0xxxxxxx)
|
||||
cu[i++] = static_cast<char>(ch);
|
||||
}
|
||||
else if (ch <= 0x7FF)
|
||||
{
|
||||
// Two-byte character (110xxxxx 10xxxxxx)
|
||||
cu[i++] = static_cast<char>(0xC0 | ((ch >> 6) & 0x1F));
|
||||
cu[i++] = static_cast<char>(0x80 | (ch & 0x3F));
|
||||
}
|
||||
else if (ch <= 0xFFFF)
|
||||
{
|
||||
// Three-byte character (1110xxxx 10xxxxxx 10xxxxxx)
|
||||
cu[i++] = static_cast<char>(0xE0 | ((ch >> 12) & 0x0F));
|
||||
cu[i++] = static_cast<char>(0x80 | ((ch >> 6) & 0x3F));
|
||||
cu[i++] = static_cast<char>(0x80 | (ch & 0x3F));
|
||||
}
|
||||
else if (ch <= 0x10FFFF)
|
||||
{
|
||||
// Four-byte character (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx)
|
||||
cu[i++] = static_cast<char>(0xF0 | ((ch >> 18) & 0x07));
|
||||
cu[i++] = static_cast<char>(0x80 | ((ch >> 12) & 0x3F));
|
||||
cu[i++] = static_cast<char>(0x80 | ((ch >> 6) & 0x3F));
|
||||
cu[i++] = static_cast<char>(0x80 | (ch & 0x3F));
|
||||
}
|
||||
o.append(cu, &cu[0] + i);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning(pop)
|
||||
|
||||
void Blackbox::Thread(til::spsc::consumer<Record> rx)
|
||||
{
|
||||
Record queue[16];
|
||||
|
||||
wil::unique_hfile file;
|
||||
do
|
||||
{
|
||||
int i = 0;
|
||||
auto [sz, ok] = rx.pop_n(til::spsc::block_initially, queue, std::extent_v<decltype(queue)>);
|
||||
|
||||
while (sz--)
|
||||
{
|
||||
Record rec = std::move(queue[i++]);
|
||||
auto timeDelta = (rec.time - _start).count() / 1e9f;
|
||||
UINT32 length{ 0 };
|
||||
auto buf = WindowsGetStringRawBuffer(rec.string, &length);
|
||||
auto jsonLine{ fmt::format(FMT_COMPILE(R"-([{}, "{}", "{}"])-"
|
||||
"\n"),
|
||||
timeDelta,
|
||||
(char)rec.typecode,
|
||||
u16json8(std::wstring_view{ buf, length })) };
|
||||
WriteFile(_file.get(), jsonLine.data(), (DWORD)jsonLine.size(), nullptr, nullptr);
|
||||
}
|
||||
if (!ok)
|
||||
{
|
||||
break; // FINISH IT OFF DAVE
|
||||
}
|
||||
} while (true);
|
||||
_file.reset();
|
||||
}
|
||||
|
||||
ConnectionRecorder::ConnectionRecorder() :
|
||||
_blackbox{ std::make_shared<Blackbox>() }
|
||||
{
|
||||
}
|
||||
|
||||
ConnectionRecorder::~ConnectionRecorder() noexcept
|
||||
{
|
||||
_connectionEvents = {}; // disconnect all event handlers
|
||||
_connection = { nullptr }; // release connection handle
|
||||
_blackbox->Close();
|
||||
}
|
||||
|
||||
void ConnectionRecorder::Connection(const winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection& connection)
|
||||
{
|
||||
_connectionEvents.output = connection.TerminalOutput(winrt::auto_revoke, [blackbox = _blackbox](const winrt::hstring& output) {
|
||||
blackbox->Log(output);
|
||||
});
|
||||
#if 0
|
||||
_connectionEvents.stateChanged = connection.StateChanged(winrt::auto_revoke, [this](auto&&, auto&&) {
|
||||
|
||||
});
|
||||
#endif
|
||||
_connection = connection;
|
||||
}
|
||||
|
||||
void ConnectionRecorder::Path(std::wstring_view path)
|
||||
{
|
||||
_filePath = path;
|
||||
}
|
||||
|
||||
void ConnectionRecorder::Start()
|
||||
{
|
||||
if (!std::exchange(_started, true))
|
||||
{
|
||||
_blackbox->Start(_filePath);
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectionRecorder::Stop()
|
||||
{
|
||||
_blackbox->Close();
|
||||
}
|
||||
133
src/cascadia/TerminalApp/Blackbox.h
Normal file
133
src/cascadia/TerminalApp/Blackbox.h
Normal file
@@ -0,0 +1,133 @@
|
||||
#include <til/spsc.h>
|
||||
|
||||
struct Blackbox : public std::enable_shared_from_this<Blackbox>
|
||||
{
|
||||
struct Record
|
||||
{
|
||||
Record() :
|
||||
time{}, typecode{ 0 }, string{ nullptr } {}
|
||||
Record(char type, HSTRING f) :
|
||||
time{ std::chrono::high_resolution_clock::now() }, typecode{ type } { WindowsDuplicateString(f, &string); }
|
||||
Record(HSTRING f) :
|
||||
Record('o', f) {}
|
||||
Record(Record&& r) :
|
||||
time{ r.time },
|
||||
typecode{ r.typecode },
|
||||
string{ r.string }
|
||||
{
|
||||
r.string = nullptr;
|
||||
}
|
||||
Record& operator=(Record&& r)
|
||||
{
|
||||
time = r.time;
|
||||
typecode = r.typecode;
|
||||
string = r.string;
|
||||
r.string = nullptr;
|
||||
return *this;
|
||||
}
|
||||
~Record()
|
||||
{
|
||||
if (string)
|
||||
{
|
||||
WindowsDeleteString(string);
|
||||
}
|
||||
}
|
||||
std::chrono::high_resolution_clock::time_point time;
|
||||
char typecode{ 0 };
|
||||
HSTRING string{ nullptr };
|
||||
};
|
||||
|
||||
Blackbox() {}
|
||||
|
||||
~Blackbox()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
void Start(wil::zwstring_view filePath)
|
||||
{
|
||||
_start = std::chrono::high_resolution_clock::now();
|
||||
auto [tx, rx] = til::spsc::channel<Record>(1024);
|
||||
_chan = std::move(tx);
|
||||
|
||||
_file.reset(CreateFileW(filePath.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr));
|
||||
THROW_LAST_ERROR_IF(!_file);
|
||||
|
||||
auto s{ fmt::format(R"-({{"version": 2, "width": 120, "height": 30}})-"
|
||||
"\n") };
|
||||
WriteFile(_file.get(), s.data(), (DWORD)s.size(), nullptr, nullptr);
|
||||
|
||||
_thread = std::thread([this, strong = shared_from_this(), rx = std::move(rx)]() mutable {
|
||||
Thread(std::move(rx));
|
||||
});
|
||||
}
|
||||
|
||||
void Log(HSTRING h)
|
||||
{
|
||||
if (!_closed)
|
||||
{
|
||||
_chan.emplace(h);
|
||||
}
|
||||
}
|
||||
|
||||
void Log(const winrt::hstring& h) { Log((HSTRING)winrt::get_abi(h)); }
|
||||
|
||||
void LogResize(til::CoordType columns, til::CoordType rows)
|
||||
{
|
||||
if (!_closed)
|
||||
{
|
||||
//? TODO(DH) determine whether we should pass the size along as a string or just make Record a tagged union w/ typecode
|
||||
winrt::hstring newSizeRecord{ fmt::format(FMT_COMPILE(L"{0}x{1}"), columns, rows) };
|
||||
_chan.emplace('r', (HSTRING)winrt::get_abi(newSizeRecord));
|
||||
}
|
||||
}
|
||||
|
||||
void Close()
|
||||
{
|
||||
if (!std::exchange(_closed, true))
|
||||
{
|
||||
{
|
||||
auto _ = std::move(_chan);
|
||||
// the tx side of the channel closes at the end of this scope
|
||||
}
|
||||
|
||||
// we may be getting destructed on the Thread thread.
|
||||
if (_thread.get_id() != std::this_thread::get_id() && _thread.joinable())
|
||||
{
|
||||
_thread.join(); // flush
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Thread(til::spsc::consumer<Record> rx);
|
||||
|
||||
private:
|
||||
std::thread _thread;
|
||||
std::chrono::high_resolution_clock::time_point _start;
|
||||
til::spsc::producer<Record> _chan{ nullptr };
|
||||
bool _closed{ false };
|
||||
wil::unique_hfile _file;
|
||||
};
|
||||
|
||||
struct ConnectionRecorder : public winrt::implements<ConnectionRecorder, winrt::Windows::Foundation::IInspectable>
|
||||
{
|
||||
ConnectionRecorder();
|
||||
~ConnectionRecorder() noexcept;
|
||||
void Connection(const winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection&);
|
||||
void Path(std::wstring_view path);
|
||||
void Start();
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
bool _started{ false };
|
||||
std::shared_ptr<Blackbox> _blackbox;
|
||||
std::wstring _filePath;
|
||||
|
||||
winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _connection{ nullptr };
|
||||
|
||||
struct
|
||||
{
|
||||
winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection::StateChanged_revoker stateChanged;
|
||||
winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection::TerminalOutput_revoker output;
|
||||
} _connectionEvents;
|
||||
};
|
||||
@@ -48,6 +48,21 @@ namespace winrt::TerminalApp::implementation
|
||||
}
|
||||
}
|
||||
|
||||
void ContentManager::AddRecorderForCore(uint64_t id, const winrt::Windows::Foundation::IInspectable& recorder)
|
||||
{
|
||||
_contentRecorders.insert_or_assign(id, recorder);
|
||||
}
|
||||
|
||||
winrt::Windows::Foundation::IInspectable ContentManager::RecorderForCore(uint64_t id)
|
||||
{
|
||||
const auto it = _contentRecorders.find(id);
|
||||
if (it != _contentRecorders.end())
|
||||
{
|
||||
return it->second;
|
||||
}
|
||||
return { nullptr };
|
||||
}
|
||||
|
||||
void ContentManager::_closedHandler(const winrt::Windows::Foundation::IInspectable& sender,
|
||||
const winrt::Windows::Foundation::IInspectable&)
|
||||
{
|
||||
@@ -55,6 +70,7 @@ namespace winrt::TerminalApp::implementation
|
||||
{
|
||||
const auto& contentId{ content.Id() };
|
||||
_content.erase(contentId);
|
||||
_contentRecorders.erase(contentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +40,12 @@ namespace winrt::TerminalApp::implementation
|
||||
|
||||
void Detach(const Microsoft::Terminal::Control::TermControl& control);
|
||||
|
||||
void AddRecorderForCore(uint64_t id, const winrt::Windows::Foundation::IInspectable& recorder);
|
||||
winrt::Windows::Foundation::IInspectable RecorderForCore(uint64_t id);
|
||||
|
||||
private:
|
||||
std::unordered_map<uint64_t, Microsoft::Terminal::Control::ControlInteractivity> _content;
|
||||
std::unordered_map<uint64_t, winrt::Windows::Foundation::IInspectable> _contentRecorders;
|
||||
|
||||
void _closedHandler(const winrt::Windows::Foundation::IInspectable& sender,
|
||||
const winrt::Windows::Foundation::IInspectable& e);
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
<ItemGroup>
|
||||
<ClInclude Include="App.base.h" />
|
||||
<ClInclude Include="AppCommandlineArgs.h" />
|
||||
<ClInclude Include="Blackbox.h" />
|
||||
<ClInclude Include="Commandline.h" />
|
||||
<ClInclude Include="CommandPaletteItems.h" />
|
||||
<ClInclude Include="Jumplist.h" />
|
||||
@@ -185,6 +186,7 @@
|
||||
</ItemGroup>
|
||||
<!-- ========================= Cpp Files ======================== -->
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Blackbox.cpp" />
|
||||
<ClCompile Include="init.cpp" />
|
||||
<ClCompile Include="AppCommandlineArgs.cpp" />
|
||||
<ClCompile Include="Commandline.cpp" />
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include <Utils.h>
|
||||
#include <TerminalCore/ControlKeyStates.hpp>
|
||||
|
||||
#include <fmt/chrono.h>
|
||||
|
||||
#include "App.h"
|
||||
#include "DebugTapConnection.h"
|
||||
#include "MarkdownPaneContent.h"
|
||||
@@ -28,6 +30,8 @@
|
||||
#include "RequestMoveContentArgs.g.cpp"
|
||||
#include "TerminalPage.g.cpp"
|
||||
|
||||
#include "Blackbox.h"
|
||||
|
||||
using namespace winrt;
|
||||
using namespace winrt::Microsoft::Management::Deployment;
|
||||
using namespace winrt::Microsoft::Terminal::Control;
|
||||
@@ -3442,6 +3446,21 @@ namespace winrt::TerminalApp::implementation
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
winrt::Windows::Foundation::IInspectable TerminalPage::_StartRecording(const winrt::Microsoft::Terminal::Control::TermControl& control, const winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection& connection)
|
||||
{
|
||||
auto rec{ winrt::make_self<ConnectionRecorder>() };
|
||||
std::time_t t = std::time(nullptr);
|
||||
auto filename = fmt::format(FMT_COMPILE(L"{}\\Desktop\\Recordings\\{}-{:%Y%m%dT%H%M%S%z}.cast"),
|
||||
wil::GetEnvironmentVariableW<std::wstring>(L"USERPROFILE"),
|
||||
L"ProfileName",
|
||||
fmt::localtime(t));
|
||||
(void)control;
|
||||
rec->Path(filename);
|
||||
rec->Connection(connection);
|
||||
rec->Start();
|
||||
return *rec;
|
||||
}
|
||||
|
||||
TermControl TerminalPage::_SetupControl(const TermControl& term)
|
||||
{
|
||||
// GH#12515: ConPTY assumes it's hidden at the start. If we're not, let it know now.
|
||||
@@ -3559,6 +3578,12 @@ namespace winrt::TerminalApp::implementation
|
||||
control.RestoreFromPath(path);
|
||||
}
|
||||
|
||||
if (true /* record on startup */)
|
||||
{
|
||||
auto recorder = _StartRecording(control, connection);
|
||||
_manager.AddRecorderForCore(control.ContentId(), recorder);
|
||||
}
|
||||
|
||||
auto paneContent{ winrt::make<TerminalPaneContent>(profile, _terminalSettingsCache, control) };
|
||||
|
||||
auto resultPane = std::make_shared<Pane>(paneContent);
|
||||
@@ -3692,6 +3717,12 @@ namespace winrt::TerminalApp::implementation
|
||||
if (const auto& connection{ _duplicateConnectionForRestart(paneContent) })
|
||||
{
|
||||
paneContent.GetTermControl().Connection(connection);
|
||||
if (auto recorder{ _manager.RecorderForCore(paneContent.GetTermControl().ContentId()) }; recorder)
|
||||
{
|
||||
auto rec = winrt::get_self<ConnectionRecorder>(recorder);
|
||||
// CHANGE HORSES MID-RACE
|
||||
rec->Connection(connection);
|
||||
}
|
||||
connection.Start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,6 +471,9 @@ namespace winrt::TerminalApp::implementation
|
||||
winrt::Microsoft::Terminal::Control::TermControl _SetupControl(const winrt::Microsoft::Terminal::Control::TermControl& term);
|
||||
winrt::Microsoft::Terminal::Control::TermControl _AttachControlToContent(const uint64_t& contentGuid);
|
||||
|
||||
winrt::Windows::Foundation::IInspectable _StartRecording(const winrt::Microsoft::Terminal::Control::TermControl& control,
|
||||
const winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection& connection);
|
||||
|
||||
TerminalApp::IPaneContent _makeSettingsContent();
|
||||
std::shared_ptr<Pane> _MakeTerminalPane(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs = nullptr,
|
||||
const winrt::TerminalApp::Tab& sourceTab = nullptr,
|
||||
|
||||
@@ -13,6 +13,9 @@ namespace TerminalApp
|
||||
|
||||
Microsoft.Terminal.Control.ControlInteractivity TryLookupCore(UInt64 id);
|
||||
void Detach(Microsoft.Terminal.Control.TermControl control);
|
||||
|
||||
void AddRecorderForCore(UInt64 id, Object recorder);
|
||||
Object RecorderForCore(UInt64 id);
|
||||
}
|
||||
|
||||
[default_interface] runtimeclass RenameWindowRequestedArgs
|
||||
|
||||
@@ -482,5 +482,20 @@ namespace Microsoft.Terminal.Settings.Model
|
||||
SelectOutputDirection Direction { get; };
|
||||
}
|
||||
|
||||
runtimeclass StartRecordingArgs : [default] IActionArgs
|
||||
{
|
||||
String Filename { get; };
|
||||
String Directory { get; };
|
||||
}
|
||||
|
||||
runtimeclass StopRecordingArgs : [default] IActionArgs
|
||||
{
|
||||
Boolean Save { get; };
|
||||
}
|
||||
|
||||
runtimeclass MarkRecordingArgs : [default] IActionArgs
|
||||
{
|
||||
String Marker { get; };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -158,7 +158,10 @@
|
||||
ON_ALL_ACTIONS_WITH_ARGS(Suggestions) \
|
||||
ON_ALL_ACTIONS_WITH_ARGS(SelectCommand) \
|
||||
ON_ALL_ACTIONS_WITH_ARGS(SelectOutput) \
|
||||
ON_ALL_ACTIONS_WITH_ARGS(ColorSelection)
|
||||
ON_ALL_ACTIONS_WITH_ARGS(ColorSelection) \
|
||||
ON_ALL_ACTIONS_WITH_ARGS(StartRecording)\
|
||||
ON_ALL_ACTIONS_WITH_ARGS(StopRecording)\
|
||||
ON_ALL_ACTIONS_WITH_ARGS(MarkRecording)
|
||||
|
||||
// These two macros here are for actions that we only use as internal currency.
|
||||
// They don't need to be parsed by the settings model, or saved as actions to
|
||||
|
||||
Reference in New Issue
Block a user