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