diff --git a/src/windows/WslcSDK/CMakeLists.txt b/src/windows/WslcSDK/CMakeLists.txt index 94e779985..98f445800 100644 --- a/src/windows/WslcSDK/CMakeLists.txt +++ b/src/windows/WslcSDK/CMakeLists.txt @@ -1,10 +1,12 @@ set(SOURCES + IOCallback.cpp ProgressCallback.cpp TerminationCallback.cpp wslcsdk.cpp WslcsdkPrivate.cpp ) set(HEADERS + IOCallback.h ProgressCallback.h TerminationCallback.h wslcsdk.h diff --git a/src/windows/WslcSDK/IOCallback.cpp b/src/windows/WslcSDK/IOCallback.cpp new file mode 100644 index 000000000..ffd805bca --- /dev/null +++ b/src/windows/WslcSDK/IOCallback.cpp @@ -0,0 +1,82 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + IOCallback.cpp + +Abstract: + + Holds IO callback objects. + +--*/ +#include "precomp.h" +#include "WslcsdkPrivate.h" + +IOCallback::IOCallback(IWSLAProcess* process, const WslcContainerProcessIOCallbackOptions& options) +{ + using namespace wsl::windows::common::relay; + + auto addIOCallback = [&](WslcProcessIOHandle ioHandle, WslcStdIOCallback callback, PVOID context) { + std::function& Buffer)> function; + if (callback) + { + function = [callback, context](const gsl::span& buffer) { + callback(reinterpret_cast(buffer.data()), static_cast(buffer.size()), context); + }; + } + else + { + function = [](const gsl::span&) {}; + } + + m_io.AddHandle(std::make_unique(GetIOHandle(process, ioHandle), std::move(function))); + }; + + addIOCallback(WSLC_PROCESS_IO_HANDLE_STDOUT, options.stdOutCallback, options.stdOutCallbackContext); + addIOCallback(WSLC_PROCESS_IO_HANDLE_STDERR, options.stdErrCallback, options.stdErrCallbackContext); + + m_io.AddHandle(std::make_unique(m_cancelEvent.get()), MultiHandleWait::CancelOnCompleted); + + m_thread = std::thread([this]() { + try + { + m_io.Run({}); + } + CATCH_LOG(); + }); +} + +IOCallback::~IOCallback() +{ + Cancel(); + if (m_thread.joinable()) + { + m_thread.join(); + } +} + +void IOCallback::Cancel() +{ + m_cancelEvent.SetEvent(); +} + +bool IOCallback::HasIOCallback(const WslcContainerProcessOptionsInternal* options) +{ + return options && HasIOCallback(options->ioCallbacks); +} + +bool IOCallback::HasIOCallback(const WslcContainerProcessIOCallbackOptions& options) +{ + return options.stdOutCallback || options.stdErrCallback; +} + +wil::unique_handle IOCallback::GetIOHandle(IWSLAProcess* process, WslcProcessIOHandle ioHandle) +{ + ULONG ulongHandle = 0; + + THROW_IF_FAILED(process->GetStdHandle(static_cast(static_cast>(ioHandle)), &ulongHandle)); + + return wil::unique_handle{ULongToHandle(ulongHandle)}; +} diff --git a/src/windows/WslcSDK/IOCallback.h b/src/windows/WslcSDK/IOCallback.h new file mode 100644 index 000000000..51a854a0f --- /dev/null +++ b/src/windows/WslcSDK/IOCallback.h @@ -0,0 +1,38 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + IOCallback.h + +Abstract: + + Holds IO callback objects. + +--*/ +#pragma once +#include "wslaservice.h" +#include "relay.hpp" +#include + +struct WslcContainerProcessIOCallbackOptions; +struct WslcContainerProcessOptionsInternal; + +struct IOCallback +{ + IOCallback(IWSLAProcess* process, const WslcContainerProcessIOCallbackOptions& options); + ~IOCallback(); + + void Cancel(); + + static bool HasIOCallback(const WslcContainerProcessOptionsInternal* options); + static bool HasIOCallback(const WslcContainerProcessIOCallbackOptions& options); + + static wil::unique_handle GetIOHandle(IWSLAProcess* process, WslcProcessIOHandle ioHandle); + +private: + std::thread m_thread; + wsl::windows::common::relay::MultiHandleWait m_io; + wil::unique_event m_cancelEvent{wil::EventOptions::ManualReset}; +}; diff --git a/src/windows/WslcSDK/WslcsdkPrivate.h b/src/windows/WslcSDK/WslcsdkPrivate.h index cab81c615..c0be865d5 100644 --- a/src/windows/WslcSDK/WslcsdkPrivate.h +++ b/src/windows/WslcSDK/WslcsdkPrivate.h @@ -15,6 +15,7 @@ Module Name: #include #include "wslcsdk.h" #include "wslaservice.h" +#include "IOCallback.h" #include #include // COM helpers // #include // handle wrappers @@ -45,6 +46,14 @@ static_assert(std::is_trivial_v, "WSLC_SESSION_OPTIO WslcSessionOptionsInternal* GetInternalType(WslcSessionSettings* settings); +struct WslcContainerProcessIOCallbackOptions +{ + WslcStdIOCallback stdOutCallback; + PVOID stdOutCallbackContext; + WslcStdIOCallback stdErrCallback; + PVOID stdErrCallbackContext; +}; + // PROCESS DEFINITIONS typedef struct WslcContainerProcessOptionsInternal { @@ -53,11 +62,12 @@ typedef struct WslcContainerProcessOptionsInternal PCSTR const* environment; uint32_t environmentCount; PCSTR currentDirectory; + WslcContainerProcessIOCallbackOptions ioCallbacks; } WslcContainerProcessOptionsInternal; static_assert( sizeof(WslcContainerProcessOptionsInternal) == WSLC_CONTAINER_PROCESS_OPTIONS_SIZE, - "WSLC_CONTAINER_PROCESS_OPTIONS_INTERNAL must be 48 bytes"); + "WSLC_CONTAINER_PROCESS_OPTIONS_INTERNAL must be 72 bytes"); static_assert( __alignof(WslcContainerProcessOptionsInternal) == WSLC_CONTAINER_PROCESS_OPTIONS_ALIGNMENT, "WSLC_CONTAINER_PROCESS_OPTIONS_INTERNAL must be 8-byte aligned"); @@ -106,6 +116,8 @@ WslcSessionImpl* GetInternalType(WslcSession handle); struct WslcContainerImpl { wil::com_ptr container; + WslcContainerProcessIOCallbackOptions ioCallbackOptions{}; + std::atomic> ioCallbacks; }; WslcContainerImpl* GetInternalType(WslcContainer handle); @@ -113,6 +125,7 @@ WslcContainerImpl* GetInternalType(WslcContainer handle); struct WslcProcessImpl { wil::com_ptr process; + std::shared_ptr ioCallbacks; }; WslcProcessImpl* GetInternalType(WslcProcess handle); diff --git a/src/windows/WslcSDK/wslcsdk.cpp b/src/windows/WslcSDK/wslcsdk.cpp index 45c097e8c..9be8ee6b5 100644 --- a/src/windows/WslcSDK/wslcsdk.cpp +++ b/src/windows/WslcSDK/wslcsdk.cpp @@ -535,6 +535,12 @@ try if (SUCCEEDED(errorInfoWrapper.CaptureResult(internalSession->session->CreateContainer(&containerOptions, &result->container)))) { wsl::windows::common::security::ConfigureForCOMImpersonation(result->container.get()); + + if (IOCallback::HasIOCallback(internalContainerSettings->initProcessOptions)) + { + result->ioCallbackOptions = internalContainerSettings->initProcessOptions->ioCallbacks; + } + *container = reinterpret_cast(result.release()); } @@ -549,7 +555,23 @@ try auto internalType = CheckAndGetInternalType(container); RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->container); - return errorInfoWrapper.CaptureResult(internalType->container->Start(ConvertFlags(flags), nullptr)); + bool hasIOCallback = IOCallback::HasIOCallback(internalType->ioCallbackOptions); + // If callbacks were provided, ATTACH must be used. + // TODO: Consider if we should just override flags when callbacks were provided instead. + RETURN_HR_IF(E_INVALIDARG, WI_IsFlagClear(flags, WSLC_CONTAINER_START_FLAG_ATTACH) && hasIOCallback); + + if (SUCCEEDED(errorInfoWrapper.CaptureResult(internalType->container->Start(ConvertFlags(flags), nullptr)))) + { + if (hasIOCallback) + { + wil::com_ptr process; + RETURN_IF_FAILED(internalType->container->GetInitProcess(&process)); + wsl::windows::common::security::ConfigureForCOMImpersonation(process.get()); + internalType->ioCallbacks = std::make_shared(process.get(), internalType->ioCallbackOptions); + } + } + + return errorInfoWrapper; } CATCH_RETURN(); @@ -680,6 +702,12 @@ try if (SUCCEEDED(errorInfoWrapper.CaptureResult(internalContainer->container->Exec(&runtimeOptions, nullptr, &result->process)))) { wsl::windows::common::security::ConfigureForCOMImpersonation(result->process.get()); + + if (IOCallback::HasIOCallback(internalProcessSettings)) + { + result->ioCallbacks = std::make_shared(result->process.get(), internalProcessSettings->ioCallbacks); + } + *newProcess = reinterpret_cast(result.release()); } @@ -724,6 +752,9 @@ try RETURN_IF_FAILED(internalType->container->GetInitProcess(&result->process)); wsl::windows::common::security::ConfigureForCOMImpersonation(result->process.get()); + + result->ioCallbacks = internalType->ioCallbacks.load(); + *initProcess = reinterpret_cast(result.release()); return S_OK; @@ -914,19 +945,30 @@ try } CATCH_RETURN(); -STDAPI WslcSetProcessSettingsIoCallback( - _In_ WslcProcessSettings* processSettings, _In_ WslcProcessIoHandle ioHandle, _In_opt_ WslcStdIOCallback stdIOCallback, _In_opt_ PVOID context) +STDAPI WslcSetProcessSettingsIOCallback( + _In_ WslcProcessSettings* processSettings, _In_ WslcProcessIOHandle ioHandle, _In_opt_ WslcStdIOCallback stdIOCallback, _In_opt_ PVOID context) try { - UNREFERENCED_PARAMETER(processSettings); - UNREFERENCED_PARAMETER(ioHandle); - UNREFERENCED_PARAMETER(stdIOCallback); - UNREFERENCED_PARAMETER(context); - return E_NOTIMPL; + auto internalType = CheckAndGetInternalType(processSettings); + RETURN_HR_IF(E_INVALIDARG, ioHandle != WSLC_PROCESS_IO_HANDLE_STDOUT && ioHandle != WSLC_PROCESS_IO_HANDLE_STDERR); + RETURN_HR_IF(E_INVALIDARG, stdIOCallback == nullptr && context != nullptr); + + if (ioHandle == WSLC_PROCESS_IO_HANDLE_STDOUT) + { + internalType->ioCallbacks.stdOutCallback = stdIOCallback; + internalType->ioCallbacks.stdOutCallbackContext = context; + } + else if (ioHandle == WSLC_PROCESS_IO_HANDLE_STDERR) + { + internalType->ioCallbacks.stdErrCallback = stdIOCallback; + internalType->ioCallbacks.stdErrCallbackContext = context; + } + + return S_OK; } CATCH_RETURN(); -STDAPI WslcGetProcessIOHandle(_In_ WslcProcess process, _In_ WslcProcessIoHandle ioHandle, _Out_ HANDLE* handle) +STDAPI WslcGetProcessIOHandle(_In_ WslcProcess process, _In_ WslcProcessIOHandle ioHandle, _Out_ HANDLE* handle) try { auto internalType = CheckAndGetInternalType(process); @@ -935,17 +977,10 @@ try *handle = nullptr; - ULONG ulongHandle = 0; - - HRESULT hr = internalType->process->GetStdHandle( - static_cast(static_cast>(ioHandle)), &ulongHandle); + auto result = IOCallback::GetIOHandle(internalType->process.get(), ioHandle); + *handle = result.release(); - if (SUCCEEDED_LOG(hr)) - { - *handle = ULongToHandle(ulongHandle); - } - - return hr; + return S_OK; } CATCH_RETURN(); diff --git a/src/windows/WslcSDK/wslcsdk.def b/src/windows/WslcSDK/wslcsdk.def index 5868ca805..f62fac23e 100644 --- a/src/windows/WslcSDK/wslcsdk.def +++ b/src/windows/WslcSDK/wslcsdk.def @@ -46,7 +46,7 @@ WslcDeleteContainer WslcGetContainerID WslcGetContainerInitProcess -WslcSetProcessSettingsIoCallback +WslcSetProcessSettingsIOCallback WslcSetProcessSettingsCurrentDirectory WslcSetProcessSettingsCmdLine WslcSetProcessSettingsEnvVariables diff --git a/src/windows/WslcSDK/wslcsdk.h b/src/windows/WslcSDK/wslcsdk.h index ce5d7abf1..258aeb559 100644 --- a/src/windows/WslcSDK/wslcsdk.h +++ b/src/windows/WslcSDK/wslcsdk.h @@ -43,7 +43,7 @@ typedef struct WslcContainerSettings DECLARE_HANDLE(WslcContainer); // Process values -#define WSLC_CONTAINER_PROCESS_OPTIONS_SIZE 40 +#define WSLC_CONTAINER_PROCESS_OPTIONS_SIZE 72 #define WSLC_CONTAINER_PROCESS_OPTIONS_ALIGNMENT 8 typedef struct WslcProcessSettings { @@ -287,16 +287,17 @@ STDAPI WslcSetProcessSettingsEnvVariables(_In_ WslcProcessSettings* processSetti // - The buffer is not null-terminated; it is a raw byte sequence. // typedef __callback void(CALLBACK* WslcStdIOCallback)(_In_reads_bytes_(dataSize) const BYTE* data, _In_ uint32_t dataSize, _In_opt_ PVOID context); -typedef enum WslcProcessIoHandle +typedef enum WslcProcessIOHandle { WSLC_PROCESS_IO_HANDLE_STDIN = 0, WSLC_PROCESS_IO_HANDLE_STDOUT = 1, WSLC_PROCESS_IO_HANDLE_STDERR = 2 -} WslcProcessIoHandle; +} WslcProcessIOHandle; -// Pass in Null for WslcStdIOCallback to clear the callback for the given handle -STDAPI WslcSetProcessSettingsIoCallback( - _In_ WslcProcessSettings* processSettings, _In_ WslcProcessIoHandle ioHandle, _In_opt_ WslcStdIOCallback stdIOCallback, _In_opt_ PVOID context); +// Only WSLC_PROCESS_IO_HANDLE_STDOUT and WSLC_PROCESS_IO_HANDLE_STDERR are supported for callbacks. +// Pass in Null for WslcStdIOCallback to clear the callback for the given handle. +STDAPI WslcSetProcessSettingsIOCallback( + _In_ WslcProcessSettings* processSettings, _In_ WslcProcessIOHandle ioHandle, _In_opt_ WslcStdIOCallback stdIOCallback, _In_opt_ PVOID context); // PROCESS MANAGEMENT @@ -318,7 +319,7 @@ STDAPI WslcGetProcessExitCode(_In_ WslcProcess process, _Out_ PINT32 exitCode); STDAPI WslcSignalProcess(_In_ WslcProcess process, _In_ WslcSignal signal); -STDAPI WslcGetProcessIOHandle(_In_ WslcProcess process, _In_ WslcProcessIoHandle ioHandle, _Out_ HANDLE* handle); +STDAPI WslcGetProcessIOHandle(_In_ WslcProcess process, _In_ WslcProcessIOHandle ioHandle, _Out_ HANDLE* handle); STDAPI WslcReleaseProcess(_In_ WslcProcess process); diff --git a/test/windows/WslcSdkTests.cpp b/test/windows/WslcSdkTests.cpp index c50fccfcd..184baabf7 100644 --- a/test/windows/WslcSdkTests.cpp +++ b/test/windows/WslcSdkTests.cpp @@ -1386,6 +1386,265 @@ class WslcSdkTests VERIFY_ARE_EQUAL(WslcGetVersion(nullptr), E_POINTER); } + // ----------------------------------------------------------------------- + // WslcSetProcessSettingsIOCallback tests + // ----------------------------------------------------------------------- + + TEST_METHOD(ProcessIoCallbackUnit) + { + WSL2_TEST_ONLY(); + + auto noopCb = [](const BYTE*, uint32_t, PVOID) {}; + + // Negative: null processSettings must fail. + VERIFY_ARE_EQUAL(WslcSetProcessSettingsIOCallback(nullptr, WSLC_PROCESS_IO_HANDLE_STDOUT, noopCb, nullptr), E_POINTER); + + // Negative: STDIN is not a valid output handle. + { + WslcProcessSettings procSettings; + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); + VERIFY_ARE_EQUAL(WslcSetProcessSettingsIOCallback(&procSettings, WSLC_PROCESS_IO_HANDLE_STDIN, noopCb, nullptr), E_INVALIDARG); + } + + // Negative: out-of-range handle value must fail. + { + WslcProcessSettings procSettings; + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); + VERIFY_ARE_EQUAL(WslcSetProcessSettingsIOCallback(&procSettings, static_cast(99), noopCb, nullptr), E_INVALIDARG); + } + + // Negative: null callback with non-null context must fail. + { + int ctx = 0; + WslcProcessSettings procSettings; + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); + VERIFY_ARE_EQUAL(WslcSetProcessSettingsIOCallback(&procSettings, WSLC_PROCESS_IO_HANDLE_STDOUT, nullptr, &ctx), E_INVALIDARG); + } + + // Positive: clear STDOUT (null callback, null context) must succeed. + { + WslcProcessSettings procSettings; + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); + VERIFY_SUCCEEDED(WslcSetProcessSettingsIOCallback(&procSettings, WSLC_PROCESS_IO_HANDLE_STDOUT, nullptr, nullptr)); + } + + // Positive: clear STDERR (null callback, null context) must succeed. + { + WslcProcessSettings procSettings; + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); + VERIFY_SUCCEEDED(WslcSetProcessSettingsIOCallback(&procSettings, WSLC_PROCESS_IO_HANDLE_STDERR, nullptr, nullptr)); + } + + // Positive: set a valid STDOUT callback must succeed. + { + WslcProcessSettings procSettings; + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); + VERIFY_SUCCEEDED(WslcSetProcessSettingsIOCallback(&procSettings, WSLC_PROCESS_IO_HANDLE_STDOUT, noopCb, nullptr)); + } + + // Positive: set a valid STDERR callback must succeed. + { + WslcProcessSettings procSettings; + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); + VERIFY_SUCCEEDED(WslcSetProcessSettingsIOCallback(&procSettings, WSLC_PROCESS_IO_HANDLE_STDERR, noopCb, nullptr)); + } + + // Negative: StartContainer without ATTACH fails when callbacks are registered. + // The ATTACH flag is required so the IOCallback can claim the init process pipe handles. + { + WslcProcessSettings procSettings; + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); + const char* argv[] = {"/bin/sleep", "1"}; + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); + VERIFY_SUCCEEDED(WslcSetProcessSettingsIOCallback(&procSettings, WSLC_PROCESS_IO_HANDLE_STDOUT, noopCb, nullptr)); + + WslcContainerSettings containerSettings; + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); + + UniqueContainer container; + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); + VERIFY_ARE_EQUAL(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr), E_INVALIDARG); + } + } + + TEST_METHOD(ProcessIoCallbackInitProcess) + { + WSL2_TEST_ONLY(); + + std::string stdoutData; + std::string stderrData; + + auto appendToContextString = [](const BYTE* data, uint32_t size, PVOID ctx) { + static_cast(ctx)->append(reinterpret_cast(data), size); + }; + + WslcProcessSettings procSettings; + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); + const char* argv[] = {"/bin/sh", "-c", "echo STDOUT && echo STDERR >&2"}; + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); + VERIFY_SUCCEEDED(WslcSetProcessSettingsIOCallback(&procSettings, WSLC_PROCESS_IO_HANDLE_STDOUT, appendToContextString, &stdoutData)); + VERIFY_SUCCEEDED(WslcSetProcessSettingsIOCallback(&procSettings, WSLC_PROCESS_IO_HANDLE_STDERR, appendToContextString, &stderrData)); + + WslcContainerSettings containerSettings; + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); + + UniqueContainer container; + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr)); + + UniqueProcess process; + VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process)); + + HANDLE exitEvent = nullptr; + VERIFY_SUCCEEDED(WslcGetProcessExitEvent(process.get(), &exitEvent)); + VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 30 * 1000), static_cast(WAIT_OBJECT_0)); + + // Release the process handle first, then the container handle. + // Releasing the container destroys the WslcContainerImpl which joins the IOCallback + // thread, guaranteeing all bytes have been delivered before the assertions below. + process.reset(); + container.reset(); + + VERIFY_ARE_EQUAL(stdoutData, "STDOUT\n"); + VERIFY_ARE_EQUAL(stderrData, "STDERR\n"); + } + + TEST_METHOD(ProcessIoCallbackExecProcess) + { + WSL2_TEST_ONLY(); + + // Start a long-running container so we can exec into it. + WslcProcessSettings initProcSettings; + VERIFY_SUCCEEDED(WslcInitProcessSettings(&initProcSettings)); + const char* initArgv[] = {"/bin/sleep", "99"}; + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&initProcSettings, initArgv, ARRAYSIZE(initArgv))); + + WslcContainerSettings containerSettings; + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &initProcSettings)); + + UniqueContainer container; + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr)); + + std::string stdoutData; + std::string stderrData; + + auto stdoutCb = [](const BYTE* data, uint32_t size, PVOID ctx) { + static_cast(ctx)->append(reinterpret_cast(data), size); + }; + auto stderrCb = [](const BYTE* data, uint32_t size, PVOID ctx) { + static_cast(ctx)->append(reinterpret_cast(data), size); + }; + + WslcProcessSettings execProcSettings; + VERIFY_SUCCEEDED(WslcInitProcessSettings(&execProcSettings)); + const char* execArgv[] = {"/bin/sh", "-c", "echo EXEC_OUT && echo EXEC_ERR >&2"}; + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&execProcSettings, execArgv, ARRAYSIZE(execArgv))); + VERIFY_SUCCEEDED(WslcSetProcessSettingsIOCallback(&execProcSettings, WSLC_PROCESS_IO_HANDLE_STDOUT, stdoutCb, &stdoutData)); + VERIFY_SUCCEEDED(WslcSetProcessSettingsIOCallback(&execProcSettings, WSLC_PROCESS_IO_HANDLE_STDERR, stderrCb, &stderrData)); + + UniqueProcess execProcess; + VERIFY_SUCCEEDED(WslcCreateContainerProcess(container.get(), &execProcSettings, &execProcess, nullptr)); + + HANDLE exitEvent = nullptr; + VERIFY_SUCCEEDED(WslcGetProcessExitEvent(execProcess.get(), &exitEvent)); + VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 30 * 1000), static_cast(WAIT_OBJECT_0)); + + // Releasing the exec process handle destroys WslcProcessImpl and joins its IOCallback + // thread, ensuring all bytes are delivered before assertions. + execProcess.reset(); + + VERIFY_ARE_EQUAL(stdoutData, "EXEC_OUT\n"); + VERIFY_ARE_EQUAL(stderrData, "EXEC_ERR\n"); + } + + TEST_METHOD(ProcessIoCallbackHandleExclusion) + { + WSL2_TEST_ONLY(); + + // Register a stdout callback only — no stderr callback. + // The IOCallback object will claim stdout's pipe handle via GetStdHandle (ownership + // transfer); calling WslcGetProcessIOHandle for stdout afterwards must therefore fail + // with ERROR_INVALID_STATE. The stderr handle (no callback) must remain obtainable. + auto noopCb = [](const BYTE*, uint32_t, PVOID) {}; + + WslcProcessSettings procSettings; + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); + const char* argv[] = {"/bin/sleep", "99"}; + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); + VERIFY_SUCCEEDED(WslcSetProcessSettingsIOCallback(&procSettings, WSLC_PROCESS_IO_HANDLE_STDOUT, noopCb, nullptr)); + + WslcContainerSettings containerSettings; + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); + + UniqueContainer container; + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr)); + + UniqueProcess process; + VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process)); + + // stdout handle was consumed by the IOCallback — must not be obtainable. + { + HANDLE h = nullptr; + VERIFY_ARE_EQUAL(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &h), HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); + } + + // stderr handle was also consumed in order to drain it despite not being given a callback. + { + HANDLE h = nullptr; + VERIFY_ARE_EQUAL(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDERR, &h), HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); + } + } + + TEST_METHOD(ProcessIoCallbackLargeOutput) + { + WSL2_TEST_ONLY(); + + // Generate ~1 MiB of stdout via: dd if=/dev/zero bs=1024 count=1024 | base64 + // 1,048,576 zero bytes → base64 output is 1,398,104 bytes (ceil(1048576/3)*4 + newlines). + // We verify the accumulated size is at least 1,398,000 bytes to allow minor variance. + static constexpr size_t c_expectedMinBytes = 1'398'000; + + std::string stdoutData; + stdoutData.reserve(c_expectedMinBytes + 4096); + + auto stdoutCb = [](const BYTE* data, uint32_t size, PVOID ctx) { + static_cast(ctx)->append(reinterpret_cast(data), size); + }; + + WslcProcessSettings procSettings; + VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); + const char* argv[] = {"/bin/sh", "-c", "dd if=/dev/zero bs=1024 count=1024 2>/dev/null | base64"}; + VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); + VERIFY_SUCCEEDED(WslcSetProcessSettingsIOCallback(&procSettings, WSLC_PROCESS_IO_HANDLE_STDOUT, stdoutCb, &stdoutData)); + + WslcContainerSettings containerSettings; + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); + VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); + + UniqueContainer container; + VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); + VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr)); + + UniqueProcess process; + VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process)); + + HANDLE exitEvent = nullptr; + VERIFY_SUCCEEDED(WslcGetProcessExitEvent(process.get(), &exitEvent)); + VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 60 * 1000), static_cast(WAIT_OBJECT_0)); + + // Join the IOCallback thread before inspecting the accumulator. + process.reset(); + container.reset(); + + VERIFY_IS_TRUE(stdoutData.size() >= c_expectedMinBytes); + } + // ----------------------------------------------------------------------- // Stub tests for unimplemented (E_NOTIMPL) functions. // Each of these confirms the current state of the SDK; once the underlying @@ -1426,15 +1685,6 @@ class WslcSdkTests VERIFY_SUCCEEDED(WslcDeleteContainer(container.get(), WSLC_DELETE_CONTAINER_FLAG_NONE, nullptr)); } - TEST_METHOD(ProcessIoCallbackNotImplemented) - { - WSL2_TEST_ONLY(); - - WslcProcessSettings procSettings; - VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); - VERIFY_ARE_EQUAL(WslcSetProcessSettingsIoCallback(&procSettings, WSLC_PROCESS_IO_HANDLE_STDOUT, nullptr, nullptr), E_NOTIMPL); - } - TEST_METHOD(SessionCreateVhdNotImplemented) { WSL2_TEST_ONLY();