WSL 容器 C++ SDK 中的 ProcessOutputHandler:事件模式下的 stdout/stderr 输出回调解析
2026/9/10 10:30:44 网站建设 项目流程

WSL 容器 C++ SDK 中的 ProcessOutputHandler:事件模式下的 stdout/stderr 输出回调解析

【免费下载链接】WSLWindows Subsystem for Linux项目地址: https://gitcode.com/GitHub_Trending/ws/WSL

本文以微软开源仓库 WSL 的 Windows 容器 SDK(WSLC,Microsoft.WSL.ContainersWinRT/C++ API)中的ProcessOutputHandler委托为切入点,讲解如何在 C++ 中以事件回调方式接收容器内进程的 stdout/stderr 原始字节输出。你将掌握ProcessOutputMode::Event模式的配置方法、OutputReceived/ErrorReceived事件的订阅与退订机制,以及底层 C 回调到winrt::array_view<const uint8_t>的转发链路,可直接应用于容器内命令执行的实时日志捕获场景。

ProcessOutputHandler 是什么

ProcessOutputHandlerMicrosoft.WSL.ContainersC++ API 中用于承载进程输出事件的委托类型,位于 delegates-and-events 目录,与ProcessCrashHandlerProcessExitHandlerSessionTerminationHandler并列,是容器进程生命周期事件体系的一部分。

在 C++ 中,它是一个接收单一参数(winrt::array_view<const uint8_t>)的委托。该参数承载的是进程 stdout/stderr 的原始输出字节(raw output bytes),不是字符串、不是按行分割的文本,也不是已经编码/解码后的hstring。这意味着:

  • 你可以直接按字节语义处理二进制输出;
  • 也可以像示例那样把字节序列化为std::string后再做文本处理(前提是输出本身是文本编码);
  • 数据的分块粒度由 SDK 底层 C 回调每次递交的数据长度决定,不保证按行对齐,因此按行解析时需要自行做缓冲拼接。

事件模式下的事件源:OutputReceived 与 ErrorReceived

ProcessOutputHandler类型的委托被用在 Process 类 的两个事件上:

  • OutputReceived:进程标准输出(stdout)有新数据时触发;
  • ErrorReceived:进程标准错误(stderr)有新数据时触发。

这两个事件只有在ProcessOutputMode::Event模式下才可用。如果进程的ProcessSettings::OutputMode()不是Event,订阅操作会抛出hresult_illegal_method_call。这一约束在 Process.cpp 中有明确实现:

winrt::event_token Process::OutputReceived(winrt::Microsoft::WSL::Containers::ProcessOutputHandler const& handler) { if (m_outputMode != ProcessOutputMode::Event) { throw winrt::hresult_illegal_method_call(L"OutputReceived requires OutputMode::Event"); } return m_outputReceivedEvent.add(handler); }

ErrorReceived的检查逻辑与之一致,只是改投递到m_errorReceivedEvent

实现细节:在 Process.h 中,m_outputReceivedEventm_errorReceivedEvent都是winrt::event<winrt::Microsoft::WSL::Containers::ProcessOutputHandler>类型的成员。订阅(add)返回winrt::event_token,可用来在之后退订(remove),实现完整的订阅生命周期管理。

从 C 回调到 WinRT 事件的转发链路

WSLC 的 C 层通过回调结构体向 WinRT 包装层递交输出。包装层的静态回调OutputCallback会根据WslcProcessIOHandle判断数据来自 stdout 还是 stderr,然后构造winrt::array_view<const uint8_t>并触发对应事件(见 Process.cpp):

void CALLBACK Process::OutputCallback(WslcProcessIOHandle ioHandle, _In_reads_bytes_(dataBytes) const BYTE* data, _In_ uint32_t dataBytes, _In_opt_ PVOID context) noexcept { auto process = static_cast<Process*>(context); auto& outputEvent = (ioHandle == WSLC_PROCESS_IO_HANDLE_STDOUT) ? process->m_outputReceivedEvent : process->m_errorReceivedEvent; winrt::array_view<const uint8_t> buffer{data, dataBytes}; outputEvent(buffer); }

也就是说,原文档中"wrapper forwards awinrt::array_view<const uint8_t>produced from the C callback buffer"这句话对应了完整的调用链:

  1. C 层回调以const BYTE* data + uint32_t dataBytes递交一段原始输出;
  2. 包装层将其封装为winrt::array_view<const uint8_t>,只引用 C 回调缓冲区、不复制数据;
  3. 依据 ioHandle 分流到OutputReceived(stdout)或ErrorReceived(stderr)。

从源码结构看,OutputCallback通过context指回Process实例,WslcProcessCallbacks结构体在 Event 模式下由包装层装配,并在Process::Start()启动进程后生效。此外,Process.h 中的注释提示了一个重要的生命周期约定:释放进程句柄会断开回调,因此m_processWslcProcess句柄)被刻意放在类成员末尾,确保其最先被释放,避免事件对象在仍可能被信号化时被销毁。

快速上手:订阅进程输出

原文档给出的最小用法如下(从winrt::array_view<const uint8_t>构造std::string并打印):

process.OutputReceived([](auto const& data) { std::string text(data.begin(), data.end()); printf("stdout: %s\n", text.c_str()); });

winrt::array_view<const uint8_t>提供begin()/end()迭代器,因此可以一行完成字节序列到std::string的转换。当输出文本不以\0结尾时,%s打印不会越界,因为std::string内部保证以空字符结尾。

一个更完整的、同时订阅 stdout 与 stderr 的示例:

ProcessSettings eventSettings; eventSettings.OutputMode(ProcessOutputMode::Event); // ... set CommandLine ... auto eventProc = container.CreateProcess(eventSettings); eventProc.OutputReceived([](auto const& data) { printf("stdout bytes: %zu\n", data.size()); }); eventProc.ErrorReceived([](auto const& data) { printf("stderr bytes: %zu\n", data.size()); }); eventProc.Exited([](int32_t exitCode) { printf("done: %d\n", exitCode); }); eventProc.Start();

订阅时机

务必先订阅事件再调用Start()。在 end-to-end example 中,容器 init 进程的输出订阅就发生在container.Start()之前:

auto initProcess = container.InitProcess(); auto exitedEvent = handle{ CreateEvent(nullptr, TRUE, FALSE, nullptr) }; int32_t initExitCode = -1; initProcess.OutputReceived([](auto const& data) { std::string text(data.begin(), data.end()); printf("%s", text.c_str()); }); initProcess.Exited(& { initExitCode = exitCode; SetEvent(exitedEvent.get()); }); container.Start();

如果订阅发生在进程已启动且已产生输出之后,前面的输出就会丢失。

配置前提:ProcessOutputMode::Event

ProcessOutputHandler事件能否生效,取决于 ProcessSettings 的OutputMode()设置。ProcessOutputMode枚举的底层值与行为如下(见 processoutputmode.md):

名称行为
0Discard默认值。不产生 stdout/stderr 事件,也没有输出流
1Stream可通过Process::GetOutputStream(...)读取流式输出
2Eventstdout/stderr 通过回调递交,即OutputReceived/ErrorReceived事件

配置示例:

procSettings.OutputMode(ProcessOutputMode::Event);

同时需要注意ProcessSettings的约束(见 processsettings.md):

  • CommandLine(nullptr)EnvironmentVariables(nullptr)会被拒绝;
  • Process::Start()要求CommandLine()非空
  • 三种输出模式由 C 层分别以"安装 C 回调(Event)"、"期望流式访问(Stream)"、"丢弃(Discard)"方式实现。

与其他输出模式的取舍

  • 若你只需要进程退出码和少量输出,Event模式最简单直接,适合日志聚合、进度输出、逐行打印等场景;
  • 若你需要像普通子进程那样操作 stdin/stdout/stderr 句柄做双向管道交互,应使用Stream模式并配合Process::GetOutputStream(ProcessOutputHandle)StandardOutput = 1StandardError = 2,见 processoutputhandle.md);
  • 若完全不需要输出,保持默认的Discard模式可省去回调与流管理的开销。

组合使用:Event 模式下的完整进程生命周期

Event模式下,OutputReceived/ErrorReceivedExited事件配合,可以构成完整的"捕获输出 → 等待退出 → 读取退出码"闭环。Process类的事件与方法清单见 process.md:

  • Start()Signal(Signal)GetOutputStream(ProcessOutputHandle)GetInputStream()Pid()State()ExitCode()Close()
  • 事件:OutputReceivedErrorReceivedExited

其中Exited在事件模式下由退出回调(exit callback)触发;在流/丢弃模式下则是等待进程退出事件后触发。也就是说,只要使用Event模式,输出回调与退出回调都来自 C 层回调机制,行为一致。

一个同时利用输出与退出事件、并实现同步等待的完整片段:

auto proc = container.CreateProcess(procSettings); proc.Exited([](int32_t exitCode) { printf("process exited: %d\n", exitCode); }); proc.Start();

如果需要主线程阻塞等待,可以像 end-to-end 示例那样用CreateEvent+WaitForSingleObject在退出回调中唤醒等待线程。

常见问题与注意事项

  • 事件模式必须先配置后订阅ProcessSettings::OutputMode(ProcessOutputMode::Event)要在创建Process之前设置;订阅事件时若当前不是 Event 模式,会抛出hresult_illegal_method_call
  • 字节分块不对齐行边界OutputCallback每次递交的数据长度由底层决定,data可能只包含一行的片段,也可能包含多行。按行解析输出时,需要在回调侧维护跨回调的缓冲。
  • 回调在 C 回调线程上执行:从源码结构看,OutputCallbackCALLBACK约定(stdcall)的静态回调,直接在当前递交输出的线程上运行。若要在 UI 或主线程更新状态,需要自行做线程调度/同步;回调内部抛出异常会被CATCH_LOG()捕获记录。
  • 退订与生命周期OutputReceived(event_token)ErrorReceived(event_token)支持通过订阅时返回的 token 退订(见 Process.cpp)。进程句柄释放会断开回调,Process析构时也会通过final_release调用Close()清理。
  • 初始化线程模型:使用Microsoft.WSL.Containers之前需要调用winrt::init_apartment(),并在结束时session.Terminate()、删除容器,完整流程可参考 end-to-end-example.md。

总结

ProcessOutputHandler是 WSL 容器 C++ SDK 在ProcessOutputMode::Event模式下接收进程 stdout/stderr 的官方委托类型。其核心机制是:C 回调缓冲区 →winrt::array_view<const uint8_t>OutputReceived(stdout)/ErrorReceived(stderr)事件。订阅前务必把ProcessSettings::OutputMode()设置为Event,并在Start()之前完成订阅;配合Exited事件即可实现"边运行、边捕获、结束时取退出码"的完整容器内进程输出管理方案。

【免费下载链接】WSLWindows Subsystem for Linux项目地址: https://gitcode.com/GitHub_Trending/ws/WSL

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询