diff --git a/src/tools/scratch/README.md b/src/tools/scratch/README.md
new file mode 100644
index 00000000000..fed2a766d98
--- /dev/null
+++ b/src/tools/scratch/README.md
@@ -0,0 +1,5 @@
+## Narrator Buddy
+
+This is a sample of how we might implement Narrator Buddy. This was an internal tool for taking what narrator would read aloud, and logging it to the console.
+
+Currently this builds as a scratch project in the Terminal solution. It's provided as a reference implementation for how a real narrator buddy might be implemented in the future.
diff --git a/src/tools/scratch/Scratch.vcxproj b/src/tools/scratch/Scratch.vcxproj
index 07c917e1240..165d3424d4d 100644
--- a/src/tools/scratch/Scratch.vcxproj
+++ b/src/tools/scratch/Scratch.vcxproj
@@ -9,6 +9,7 @@
Application
+
@@ -32,4 +33,5 @@
+
diff --git a/src/tools/scratch/main.cpp b/src/tools/scratch/main.cpp
index c6f9cc901a2..55ea2d25ab9 100644
--- a/src/tools/scratch/main.cpp
+++ b/src/tools/scratch/main.cpp
@@ -2,9 +2,141 @@
// Licensed under the MIT license.
#include
+#include
+#include
+#include
+#include
+#include
+#include
-// This wmain exists for help in writing scratch programs while debugging.
-int __cdecl wmain(int /*argc*/, WCHAR* /*argv[]*/)
+#include
+#include
+#include
+
+#include
+#include
+
+namespace
+{
+ wil::unique_event g_stopEvent{ wil::EventOptions::None };
+
+} // namespace anonymous
+
+void WINAPI ProcessEtwEvent(_In_ PEVENT_RECORD rawEvent)
+{
+ // This is the task id from srh.man (with the same name).
+ constexpr auto InitiateSpeaking = 5;
+
+ auto data = rawEvent->UserData;
+ auto dataLen = rawEvent->UserDataLength;
+ auto processId = rawEvent->EventHeader.ProcessId;
+ auto task = rawEvent->EventHeader.EventDescriptor.Task;
+
+ if (task == InitiateSpeaking)
+ {
+ // The payload first has an int32 representing the channel we're writing to. This is basically always writing to the
+ // default channel, so just skip over those bytes... the rest is the string payload we want to speak,
+ // as a null terminated string.
+ if (dataLen <= 4)
+ {
+ return;
+ }
+
+ const auto stringPayloadSize = (dataLen - 4) / sizeof(wchar_t);
+ // We don't need the null terminator, because wstring intends to handle that on its own.
+ const auto stringLen = stringPayloadSize - 1;
+ const auto payload = std::wstring(reinterpret_cast(static_cast(data) + 4), stringLen);
+
+ wprintf(L"[Narrator pid=%d]: %s\n", processId, payload.c_str());
+ fflush(stdout);
+
+ if (payload == L"Exiting Narrator")
+ {
+ g_stopEvent.SetEvent();
+ }
+ }
+}
+
+int __cdecl wmain(int argc, wchar_t* argv[])
{
+ const bool runForever = (argc == 2) &&
+ std::wstring{ argv[1] } == L"-forever";
+
+ GUID sessionGuid;
+ FAIL_FAST_IF_FAILED(::CoCreateGuid(&sessionGuid));
+
+ std::array traceSessionName{};
+ FAIL_FAST_IF_FAILED(StringCchPrintf(traceSessionName.data(), static_cast(traceSessionName.size()), L"NarratorTraceSession_%d", ::GetCurrentProcessId()));
+
+ unsigned int traceSessionNameBytes = static_cast((wcslen(traceSessionName.data()) + 1) * sizeof(wchar_t));
+
+ // Now, to get tracing. Most settings below are defaults from MSDN, except where noted.
+ // First, set up a session (StartTrace) - which requires a PROPERTIES struct. This has to have
+ // the session name after it in the same block of memory...
+ const unsigned int propertiesByteSize = sizeof(EVENT_TRACE_PROPERTIES) + traceSessionNameBytes;
+ std::vector eventTracePropertiesBuffer(propertiesByteSize);
+ auto properties = reinterpret_cast(eventTracePropertiesBuffer.data());
+
+ // Set up properties struct for a real-time session...
+ properties->Wnode.BufferSize = propertiesByteSize;
+ properties->Wnode.Guid = sessionGuid;
+ properties->Wnode.ClientContext = 1;
+ properties->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
+ properties->LogFileMode = EVENT_TRACE_REAL_TIME_MODE | EVENT_TRACE_USE_PAGED_MEMORY;
+ properties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
+ properties->FlushTimer = 1;
+ // Finally, copy the session name...
+ memcpy(properties + 1, traceSessionName.data(), traceSessionNameBytes);
+
+ std::thread traceThread;
+ auto joinTraceThread = wil::scope_exit([&]() {
+ if (traceThread.joinable())
+ {
+ traceThread.join();
+ }
+ });
+
+ TRACEHANDLE session{};
+ const auto rc = ::StartTrace(&session, traceSessionName.data(), properties);
+ FAIL_FAST_IF(rc != ERROR_SUCCESS);
+ auto stopTrace = wil::scope_exit([&]() {
+ EVENT_TRACE_PROPERTIES properties{};
+ properties.Wnode.BufferSize = sizeof(properties);
+ properties.Wnode.Guid = sessionGuid;
+ properties.Wnode.Flags = WNODE_FLAG_TRACED_GUID;
+
+ ::ControlTrace(session, nullptr, &properties, EVENT_TRACE_CONTROL_STOP);
+ });
+
+ constexpr GUID narratorProviderGuid = { 0x835b79e2, 0xe76a, 0x44c4, 0x98, 0x85, 0x26, 0xad, 0x12, 0x2d, 0x3b, 0x4d };
+ FAIL_FAST_IF(ERROR_SUCCESS != ::EnableTrace(TRUE /* enable */, 0 /* enableFlag */, TRACE_LEVEL_VERBOSE, &narratorProviderGuid, session));
+ auto disableTrace = wil::scope_exit([&]() {
+ ::EnableTrace(FALSE /* enable */, 0 /* enableFlag */, TRACE_LEVEL_VERBOSE, &narratorProviderGuid, session);
+ });
+
+ // Finally, start listening (OpenTrace/ProcessTrace/CloseTrace)...
+ EVENT_TRACE_LOGFILE trace{};
+ trace.LoggerName = traceSessionName.data();
+ trace.ProcessTraceMode = PROCESS_TRACE_MODE_EVENT_RECORD | PROCESS_TRACE_MODE_REAL_TIME;
+ trace.EventRecordCallback = ProcessEtwEvent;
+
+ using unique_tracehandle = wil::unique_any;
+ unique_tracehandle traceHandle{ ::OpenTrace(&trace) };
+
+ // Since the actual call to ProcessTrace blocks while it's working,
+ // we spin up a separate thread to do that.
+ traceThread = std::thread([traceHandle(std::move(traceHandle))]() mutable {
+ ::ProcessTrace(traceHandle.addressof(), 1 /* handleCount */, nullptr /* startTime */, nullptr /* endTime */);
+ });
+
+ if (runForever)
+ {
+ ::Sleep(INFINITE);
+ }
+ else
+ {
+ g_stopEvent.wait(INFINITE);
+ }
+
return 0;
}