Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/windows/WslcSDK/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
82 changes: 82 additions & 0 deletions src/windows/WslcSDK/IOCallback.cpp
Original file line number Diff line number Diff line change
@@ -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<void(const gsl::span<char>& Buffer)> function;
if (callback)
{
function = [callback, context](const gsl::span<char>& buffer) {
callback(reinterpret_cast<const BYTE*>(buffer.data()), static_cast<uint32_t>(buffer.size()), context);
};
}
else
{
function = [](const gsl::span<char>&) {};
}

m_io.AddHandle(std::make_unique<ReadHandle>(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<EventHandle>(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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Looks like this is never called separately, so might as well have everything in the destructor

}

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<ULONG>(static_cast<std::underlying_type_t<WslcProcessIOHandle>>(ioHandle)), &ulongHandle));

return wil::unique_handle{ULongToHandle(ulongHandle)};
}
38 changes: 38 additions & 0 deletions src/windows/WslcSDK/IOCallback.h
Original file line number Diff line number Diff line change
@@ -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 <thread>

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};
};
15 changes: 14 additions & 1 deletion src/windows/WslcSDK/WslcsdkPrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Module Name:
#include <windows.h>
#include "wslcsdk.h"
#include "wslaservice.h"
#include "IOCallback.h"
#include <stdint.h>
#include <wil/com.h> // COM helpers
// #include <wil/resource.h> // handle wrappers
Expand Down Expand Up @@ -45,6 +46,14 @@ static_assert(std::is_trivial_v<WslcSessionOptionsInternal>, "WSLC_SESSION_OPTIO

WslcSessionOptionsInternal* GetInternalType(WslcSessionSettings* settings);

struct WslcContainerProcessIOCallbackOptions
{
WslcStdIOCallback stdOutCallback;
PVOID stdOutCallbackContext;
WslcStdIOCallback stdErrCallback;
PVOID stdErrCallbackContext;
};

// PROCESS DEFINITIONS
typedef struct WslcContainerProcessOptionsInternal
{
Expand All @@ -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");
Expand Down Expand Up @@ -106,13 +116,16 @@ WslcSessionImpl* GetInternalType(WslcSession handle);
struct WslcContainerImpl
{
wil::com_ptr<IWSLAContainer> container;
WslcContainerProcessIOCallbackOptions ioCallbackOptions{};
std::atomic<std::shared_ptr<IOCallback>> ioCallbacks;
};

WslcContainerImpl* GetInternalType(WslcContainer handle);

struct WslcProcessImpl
{
wil::com_ptr<IWSLAProcess> process;
std::shared_ptr<IOCallback> ioCallbacks;
};

WslcProcessImpl* GetInternalType(WslcProcess handle);
Expand Down
73 changes: 54 additions & 19 deletions src/windows/WslcSDK/wslcsdk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<WslcContainer>(result.release());
}

Expand All @@ -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<IWSLAProcess> process;
RETURN_IF_FAILED(internalType->container->GetInitProcess(&process));
wsl::windows::common::security::ConfigureForCOMImpersonation(process.get());
internalType->ioCallbacks = std::make_shared<IOCallback>(process.get(), internalType->ioCallbackOptions);
}
}

return errorInfoWrapper;
}
CATCH_RETURN();

Expand Down Expand Up @@ -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<IOCallback>(result->process.get(), internalProcessSettings->ioCallbacks);
}

*newProcess = reinterpret_cast<WslcProcess>(result.release());
}

Expand Down Expand Up @@ -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<WslcProcess>(result.release());

return S_OK;
Expand Down Expand Up @@ -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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just realizing this now, but we need a way to unregister the IO callbacks and synchronize them.

Something like:


HRESULT WslcClearProcessIOCallbacks( WslcProcessSettings* processSettings, _In_ WslcProcessIOHandle ioHandle);

(Once that method returns, the caller is guaranteed that all pending callbacks are completed and that no future callback can be received).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current design is that releasing all of the process/container objects related to the process does that. Container+all retrieved InitProcess instances for init processes, just the one process for any created afterward.

We could additionally have a way to explicitly do it, but I don't think that it is strictly necessary. It would have to take in a WslcProcess handle, and we would need to consider if we wanted a second function to take in a container, removing the need to get the init process just to stop the IO callback.

A reason that this might be needed is to ensure that ALL output has been received (the buffers flushed) before teardown as the current release design cancels any future data flow. If the process is still running that isn't really an issue. If it has stopped, we know that there is an end to the pipe and we should get there in reasonably short order.

I think the caller flow would be like:

process = CreateContainerProcess(withIOCallbacks);
WaitForSingle(GetProcessExitEvent(process));
DisableIOCallbacksAndFlushBuffers(process, timeout); // without this, output could be lost in transit when the release is called
ReleaseProcess(process);

the implementation for (pseudoname) DisableIOCallbacksAndFlushBuffers would be like:

if (process is still running right now) // if you don't want this race, wait for the process exit event
{
  IOCancel();
}
if (!WaitForIOWithTimeout(timeout))
{
  IOCancel();
}
JoinThread();

Copy link
Collaborator

@OneBlue OneBlue Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately this won't be sufficient because the service doesn't guarantee that all the pending IO has been written the pipe after the exit event is signaled.

For context, two things happen when a process terminates

  1. A zero byte read is signaled on the pipes
  2. The process exit event is signaled

But those can happen in either order.

Thinking through this, I think this might be something that we've overlooked when we designed this. The only way I can think of fixing this in a way that's not potentially confusing for the other would be something like:

using WslcOnProcessExitedCallback = Function<void(int, Context)>;
using WslcStdIOCallback = Function<void(WslcProcessIOHandle, Context)>;

struct ProcessIOCallback
{
    WslcStdIOCallback OnStdout;
    WslcStdIOCallback OnStderr;
    WslcOnProcessExitedCallback OnExited;
}

STDAPI WslcSetProcessSettingsIOCallback(
_In_ WslcProcessSettings* processSettings,
 _In_ const ProcessIOCallback* Callbacks,
 _In_opt_ WslcStdIOCallback stdIOCallback,
 _In_opt_ PVOID context);

We could make this work by having IOCallback watch all of:

  1. The stdout handle
  2. The stderr handle
  3. The process exit event

And only call OnExited() once all of those are completed, which would guarantee that all IO has been flushed.

Forcing the user to register all callbacks at once will push them to implement this correctly and only release the process after OnExited is called.

_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);
Expand All @@ -935,17 +977,10 @@ try

*handle = nullptr;

ULONG ulongHandle = 0;

HRESULT hr = internalType->process->GetStdHandle(
static_cast<ULONG>(static_cast<std::underlying_type_t<WslcProcessIoHandle>>(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();

Expand Down
2 changes: 1 addition & 1 deletion src/windows/WslcSDK/wslcsdk.def
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ WslcDeleteContainer
WslcGetContainerID
WslcGetContainerInitProcess

WslcSetProcessSettingsIoCallback
WslcSetProcessSettingsIOCallback
WslcSetProcessSettingsCurrentDirectory
WslcSetProcessSettingsCmdLine
WslcSetProcessSettingsEnvVariables
Expand Down
15 changes: 8 additions & 7 deletions src/windows/WslcSDK/wslcsdk.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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

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

Expand Down
Loading