Compare commits

...

14 Commits

Author SHA1 Message Date
Dustin L. Howett
ea1f6cce69 Try: add basic actions for Start/Stop/Mark REcording 2025-12-18 18:59:51 -06:00
Dustin L. Howett
a959be4cac Code format blackbox 2025-09-16 12:53:42 -05:00
Dustin L. Howett
41f8ca3e5b Merge remote-tracking branch 'origin/main' into dev/duhowett/fhl-2024/asciicast-recorder
# Conflicts:
#	src/cascadia/TerminalApp/TerminalAppLib.vcxproj
#	src/cascadia/TerminalApp/TerminalPage.cpp
2025-09-15 14:43:40 -05:00
Dustin L. Howett
a76925e20f Let the output handler hold a ref to the blackbox, not to the recorder 2025-03-07 18:10:32 -06:00
Dustin L. Howett
0197d501d9 why does it crash on shutdown? WE REVOKED THE EVENT HANDLER 2025-03-07 17:22:17 -06:00
Dustin L. Howett
f6c51ffd3f record all connections on start to unique paths 2025-03-07 17:05:01 -06:00
Dustin L. Howett
d764596b6a bbox: pull out implementation 2025-03-07 15:59:45 -06:00
Dustin L. Howett
1b8163f42c bbox: move format preamble closer to thread start 2025-03-07 15:52:26 -06:00
Dustin L. Howett
70b7e6761f Merge remote-tracking branch 'origin/main' into dev/duhowett/fhl-2024/asciicast-recorder 2025-03-03 14:05:46 -06:00
Dustin L. Howett
92b2ca2fda Revert "Add some annoying stuff for catching resizes..."
This reverts commit e9517a6509.
2024-03-31 20:01:07 -07:00
Dustin L. Howett
e9517a6509 Add some annoying stuff for catching resizes... 2024-03-31 20:01:00 -07:00
Dustin L. Howett
bbb2bb9f05 Merge remote-tracking branch 'origin/main' into dev/duhowett/fhl-2024/asciicast-recorder 2024-03-31 19:26:13 -07:00
Dustin L. Howett
4fbad3b7c1 clean it up a bit; make Start explicit, and open the file then; add size records 2024-03-31 19:26:06 -07:00
Dustin L. Howett
4a83889bdd Nobody had better rely on this in production, that's for sure 2024-03-26 20:58:28 -05:00
10 changed files with 373 additions and 1 deletions

View 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();
}

View 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;
};

View File

@@ -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);
}
}
}

View File

@@ -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);

View File

@@ -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" />

View File

@@ -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();
}
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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; };
}
}

View File

@@ -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