Wslc events (#40971)

This commit is contained in:
Kevin Vega authored and GitHub committed 2026-09-03 15:06:49 -07:00
1 parent b91e0f629b
commit 75cd0f0469
18 files changed
+1009 -92

No files matched your search

+4
View File
@@ -18,6 +18,8 @@
#define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */
#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */
#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */
#define WSLC_E_EVENTS_LOST MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */
#define WSLC_E_EVENT_STREAM_FINISHED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 18) /* 0x80040612 */
```
| Symbol | Hex Value |
@@ -39,5 +41,7 @@
| `WSLC_E_VOLUME_NOT_AVAILABLE` | `0x8004060E` |
| `WSLC_E_SESSION_NOT_FOUND` | `0x8004060F` |
| `WSLC_E_VM_NOT_RUNNING` | `0x80040610` |
| `WSLC_E_EVENTS_LOST` | `0x80040611` |
| `WSLC_E_EVENT_STREAM_FINISHED` | `0x80040612` |
---
@@ -2371,6 +2371,10 @@ For privacy information about this product please visit https://aka.ms/privacy.<
<value>Invalid name: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name = "MessageWslcEventsInvalidTimeWindow" xml:space = "preserve" >
<value>`since` time ({}) cannot be after `until` time ({})</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="`since`"}{Locked="`until`"}</comment>
</data>
<data name = "MessagePathNotAbsolute" xml:space = "preserve" >
<value>Path is not absolute: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
+8
View File
@@ -359,6 +359,14 @@
</RegistryKey>
</RegistryKey>
<!-- IWSLCEventStream-->
<RegistryKey Root="HKCR" Key="Interface\{7EC66D3B-D098-4D48-B69E-69166F6C4745}">
<RegistryValue Value="IWSLCEventStream" Type="string" />
<RegistryKey Key="ProxyStubClsid32">
<RegistryValue Value="{4EA0C6DD-E9FF-48E7-994E-13A31D10DC60}" Type="string" />
</RegistryKey>
</RegistryKey>
<!-- ICrashDumpCallback-->
<RegistryKey Root="HKCR" Key="Interface\{8C5A7B14-9D26-4FAE-AB31-7E5BC23F4801}">
<RegistryValue Value="ICrashDumpCallback" Type="string" />
+2
View File
@@ -44,6 +44,8 @@ EXTERN_C_START
#define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */
#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */
#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */
#define WSLC_E_EVENTS_LOST MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */
#define WSLC_E_EVENT_STREAM_FINISHED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 18) /* 0x80040612 */
// Session values
#define WSLC_SESSION_OPTIONS_SIZE 72
+2
View File
@@ -180,6 +180,8 @@ static const std::map<HRESULT, LPCWSTR> g_commonErrors{
X(WSLC_E_NETWORK_NOT_FOUND),
X(WSLC_E_SESSION_NOT_FOUND),
X(WSLC_E_VM_NOT_RUNNING),
X(WSLC_E_EVENTS_LOST),
X(WSLC_E_EVENT_STREAM_FINISHED),
X(WSLC_E_WU_SEARCH_FAILED),
X_WIN32(RPC_S_SERVER_UNAVAILABLE),
X_WIN32(ERROR_ELEVATION_REQUIRED),
+18
View File
@@ -348,4 +348,22 @@ struct VolumeListEntry
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(VolumeListEntry, Name, Driver, Mountpoint, Scope, Labels);
};
struct EventActor
{
std::string ID;
std::map<std::string, std::string> Attributes;
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(EventActor, ID, Attributes);
};
struct Event
{
std::string Type;
std::string Action;
EventActor Actor;
std::int64_t time{};
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Event, Type, Action, Actor, time);
};
} // namespace wsl::windows::common::wslc_schema
+26
View File
@@ -671,6 +671,18 @@ typedef struct _WSLCListContainersOptions
ULONG FiltersCount;
} WSLCListContainersOptions;
[
uuid(7EC66D3B-D098-4D48-B69E-69166F6C4745),
pointer_default(unique),
object
]
interface IWSLCEventStream : IUnknown
{
// Blocks until the next matching event, the until-time is reached, or the session terminates,
// then returns the event as a JSON object following the wslc_schema::Event format.
HRESULT GetNext([out, string] LPSTR* EventJson);
}
// Settings for IWSLCSession::Initialize - passed from service to per-user process
typedef struct _WSLCSessionInitSettings
{
@@ -708,6 +720,18 @@ interface IWSLCSession : IUnknown
// termination event has been signaled; before that the call fails.
HRESULT GetTerminationReason([out] WSLCVirtualMachineTerminationReason* Reason, [out] LPWSTR* Details);
// Opens an event stream that mirrors `docker events`. The window and filters are captured by the
// returned stream object; events are then pulled one at a time via IWSLCEventStream::GetNext.
// SinceTime / UntilTime bound the window by event time, in seconds since the Unix epoch. SinceTime
// is inclusive and UntilTime is exclusive. 0 means unbounded on that end.
// Filters are key/value pairs. Values sharing a key are OR'd, distinct keys are AND'd.
HRESULT GetEvents(
[in] LONGLONG SinceTime,
[in] LONGLONG UntilTime,
[in, unique, size_is(FiltersCount)] const WSLCFilter* Filters,
[in] ULONG FiltersCount,
[out] IWSLCEventStream** Stream);
// Image management.
HRESULT PullImage([in] LPCSTR Image, [in, unique] LPCSTR RegistryAuthenticationInformation, [in, unique] IProgressCallback* ProgressCallback, [in, unique] IWarningCallback* WarningCallback);
HRESULT BuildImage([in] const WSLCBuildImageOptions* Options, [in, unique] IProgressCallback* ProgressCallback, [in, unique, system_handle(sh_event)] HANDLE CancelEvent);
@@ -885,3 +909,5 @@ cpp_quote("#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILIT
// N.B. WSLC_E_VM_NOT_RUNNING is part of the plugin API contract and is also defined in WslPluginApi.h.
// The two definitions must stay in sync.
cpp_quote("#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */")
cpp_quote("#define WSLC_E_EVENTS_LOST MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */")
cpp_quote("#define WSLC_E_EVENT_STREAM_FINISHED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 18) /* 0x80040612 */")
+2
View File
@@ -26,6 +26,7 @@ set(SOURCES
# Supporting classes
DockerEventTracker.cpp
DockerHTTPClient.cpp
EventStore.cpp
IORelay.cpp
OptionParser.cpp
ServiceProcessLauncher.cpp
@@ -35,6 +36,7 @@ set(SOURCES
set(HEADERS
DockerEventTracker.h
DockerHTTPClient.h
EventStore.h
IORelay.h
OptionParser.h
ServiceProcessLauncher.h
+44 -56
View File
@@ -96,6 +96,7 @@ DockerEventTracker::~DockerEventTracker()
// N.B. No callback should be left when the tracker is destroyed.
WI_ASSERT(m_containerCallbacks.empty());
WI_ASSERT(m_volumeCallbacks.empty());
WI_ASSERT(m_containerCreateCallbacks.empty());
}
void DockerEventTracker::OnEvent(const std::string_view& event)
@@ -125,33 +126,16 @@ void DockerEventTracker::OnEvent(const std::string_view& event)
if (typeStr == "container")
{
OnContainerEvent(parsed, actionStr, eventTime);
if (actionStr == "create")
{
OnContainerCreated(parsed, eventTime);
}
}
else if (typeStr == "volume")
{
OnVolumeEvent(parsed, actionStr, eventTime);
}
// Track object creation for WaitForObjectCreated.
auto actor = parsed.find("Actor");
if (actor != parsed.end())
{
auto id = actor->find("ID");
if (id != actor->end())
{
auto objectId = id->get<std::string>();
if (actionStr == "create")
{
std::lock_guard lock{m_lock};
m_createdObjects.insert(objectId);
m_objectCreated.SetEvent();
}
else if (actionStr == "destroy")
{
std::lock_guard lock{m_lock};
m_createdObjects.erase(objectId);
}
}
}
}
void DockerEventTracker::OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::int64_t eventTime)
@@ -159,6 +143,7 @@ void DockerEventTracker::OnContainerEvent(const nlohmann::json& parsed, const st
static std::map<std::string, ContainerEvent> events{
{"start", ContainerEvent::Start},
{"die", ContainerEvent::Stop},
{"kill", ContainerEvent::Kill},
{"destroy", ContainerEvent::Destroy},
{"exec_die", ContainerEvent::ExecDied},
{"restart", ContainerEvent::Restart}};
@@ -241,30 +226,23 @@ void DockerEventTracker::OnVolumeEvent(const nlohmann::json& parsed, const std::
InvokeCallbacks(callbacks, [&](const VolumeCallback& e) { e.Callback(volumeName, it->second, eventTime); });
}
void DockerEventTracker::WaitForObjectCreated(const std::string& ObjectId)
void DockerEventTracker::OnContainerCreated(const nlohmann::json& parsed, std::int64_t eventTime)
{
constexpr auto c_timeout = std::chrono::seconds{60};
auto actor = parsed.find("Actor");
THROW_HR_IF_MSG(E_INVALIDARG, actor == parsed.end(), "Missing Actor in container event");
while (true)
auto id = actor->find("ID");
THROW_HR_IF_MSG(E_INVALIDARG, id == actor->end(), "Missing Actor.ID in container event");
auto containerId = id->get<std::string>();
std::vector<std::shared_ptr<ContainerCreateCallbackEntry>> callbacks;
{
{
std::lock_guard lock{m_lock};
if (m_createdObjects.contains(ObjectId))
{
return;
}
// Reset under the lock so a concurrent OnEvent() that runs after we release the lock
// and before the wait can re-signal the event and unblock us.
m_objectCreated.ResetEvent();
}
THROW_HR_IF_MSG(
HRESULT_FROM_WIN32(ERROR_TIMEOUT),
!m_session.WaitForEventOrSessionTerminating(m_objectCreated.get(), c_timeout),
"Timed out waiting for Docker create event for object '%hs'",
ObjectId.c_str());
std::lock_guard lock{m_lock};
callbacks = m_containerCreateCallbacks;
}
InvokeCallbacks(callbacks, [&](const ContainerCreateCallbackEntry& e) { e.Callback(containerId, eventTime); });
}
DockerEventTracker::EventTrackingReference DockerEventTracker::RegisterContainerStateUpdates(
@@ -302,6 +280,17 @@ DockerEventTracker::EventTrackingReference DockerEventTracker::RegisterVolumeUpd
return EventTrackingReference{this, id};
}
DockerEventTracker::EventTrackingReference DockerEventTracker::RegisterContainerCreate(ContainerCreateCallback&& Callback) noexcept
{
auto id = m_callbackId++;
auto entry = std::make_shared<ContainerCreateCallbackEntry>(id, std::move(Callback));
std::lock_guard lock{m_lock};
m_containerCreateCallbacks.emplace_back(std::move(entry));
return EventTrackingReference{this, id};
}
void DockerEventTracker::UnregisterCallback(size_t Id) noexcept
{
std::shared_ptr<CallbackRegistration> registration;
@@ -311,22 +300,21 @@ void DockerEventTracker::UnregisterCallback(size_t Id) noexcept
auto matches = [Id](const auto& e) { return e->CallbackId == Id; };
// Try container callbacks first, then volume callbacks.
if (auto container = std::ranges::find_if(m_containerCallbacks, matches); container != m_containerCallbacks.end())
{
registration = std::move(*container);
m_containerCallbacks.erase(container);
}
else
{
auto volume = std::ranges::find_if(m_volumeCallbacks, matches);
WI_ASSERT(volume != m_volumeCallbacks.end());
if (volume != m_volumeCallbacks.end())
auto take = [&](auto& Callbacks) {
auto entry = std::ranges::find_if(Callbacks, matches);
if (entry == Callbacks.end())
{
registration = std::move(*volume);
m_volumeCallbacks.erase(volume);
return false;
}
registration = std::move(*entry);
Callbacks.erase(entry);
return true;
};
if (!take(m_containerCallbacks) && !take(m_volumeCallbacks) && !take(m_containerCreateCallbacks))
{
WI_ASSERT(false);
}
}
+19 -6
View File
@@ -30,7 +30,8 @@ enum class ContainerEvent
Stop,
Exit,
Destroy,
ExecDied
ExecDied,
Kill
};
enum class VolumeEvent
@@ -64,6 +65,7 @@ public:
using ContainerStateChangeCallback = std::function<void(ContainerEvent, std::optional<int>, std::int64_t)>;
using VolumeEventCallback = std::function<void(const std::string&, VolumeEvent, std::int64_t)>;
using ContainerCreateCallback = std::function<void(const std::string& ContainerId, std::int64_t Time)>;
explicit DockerEventTracker(WSLCSession& session);
~DockerEventTracker();
@@ -76,13 +78,16 @@ public:
EventTrackingReference RegisterContainerStateUpdates(const std::string& ContainerId, ContainerStateChangeCallback&& Callback) noexcept;
EventTrackingReference RegisterExecStateUpdates(const std::string& ContainerId, const std::string& ExecId, ContainerStateChangeCallback&& Callback) noexcept;
EventTrackingReference RegisterVolumeUpdates(VolumeEventCallback&& Callback) noexcept;
void UnregisterCallback(size_t Id) noexcept;
void WaitForObjectCreated(const std::string& ObjectId);
// Invoked for every container create event, after the per-container state callbacks. Unlike those,
// this isn't keyed by container id, because the id isn't known until Docker assigns it.
EventTrackingReference RegisterContainerCreate(ContainerCreateCallback&& Callback) noexcept;
void UnregisterCallback(size_t Id) noexcept;
private:
void OnEvent(const std::string_view& event);
void OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::int64_t eventTime);
void OnContainerCreated(const nlohmann::json& parsed, std::int64_t eventTime);
void OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::int64_t eventTime);
// Callbacks are invoked without holding m_lock so that a callback can register or unregister callbacks, and so
@@ -125,8 +130,19 @@ private:
const VolumeEventCallback Callback;
};
struct ContainerCreateCallbackEntry : CallbackRegistration
{
ContainerCreateCallbackEntry(size_t Id, ContainerCreateCallback&& Callback) :
CallbackRegistration(Id), Callback(std::move(Callback))
{
}
const ContainerCreateCallback Callback;
};
_Guarded_by_(m_lock) std::vector<std::shared_ptr<ContainerCallback>> m_containerCallbacks;
_Guarded_by_(m_lock) std::vector<std::shared_ptr<VolumeCallback>> m_volumeCallbacks;
_Guarded_by_(m_lock) std::vector<std::shared_ptr<ContainerCreateCallbackEntry>> m_containerCreateCallbacks;
// Invokes a snapshot of callbacks taken under m_lock, skipping registrations that have since been unregistered.
template <typename TCallback, typename TInvoke>
@@ -142,9 +158,6 @@ private:
}
}
_Guarded_by_(m_lock) std::unordered_set<std::string> m_createdObjects;
_Guarded_by_(m_lock) wil::unique_event m_objectCreated { wil::EventOptions::ManualReset };
WSLCSession& m_session;
std::mutex m_lock;
std::atomic<size_t> m_callbackId{0};
+259
View File
@@ -0,0 +1,259 @@
// Copyright (C) Microsoft Corporation. All rights reserved.
#include "precomp.h"
#include "EventStore.h"
#include "WSLCSession.h"
#include "WSLCExecutionContext.h"
#include <chrono>
using wsl::shared::Localization;
namespace wsl::windows::service::wslc {
namespace {
std::optional<std::chrono::sys_seconds> ToTimeBound(int64_t TimeSeconds)
{
if (TimeSeconds == 0)
{
return std::nullopt;
}
// Waiting on a bound converts it to the system clock's 100ns ticks, which a far-future second would overflow.
constexpr auto c_maxBound = std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::time_point::max());
return std::min(std::chrono::sys_seconds{std::chrono::seconds{TimeSeconds}}, c_maxBound);
}
} // namespace
void EventStore::Append(wsl::windows::common::wslc_schema::Event Event)
{
std::lock_guard lock(m_lock);
// Events are recorded in Docker's delivery order, which is also timestamp order. Subscribers rely on
// this: they resume from a sequence number, so an out-of-order event could never be inserted where it
// belongs without hiding it from readers that already moved past that point.
WI_ASSERT(m_events.empty() || m_events.back().time <= Event.time);
m_events.push_back(std::move(Event));
if (m_events.size() > c_eventRingCapacity)
{
m_events.pop_front();
++m_firstSequenceNumber;
}
m_updated.notify_all();
}
void EventStore::Record(std::string&& Type, std::string&& Action, const std::string& ActorId, std::map<std::string, std::string> ActorAttributes, std::int64_t Time) noexcept
try
{
wsl::windows::common::wslc_schema::Event event;
event.Type = std::move(Type);
event.Action = std::move(Action);
event.Actor.ID = ActorId;
event.Actor.Attributes = std::move(ActorAttributes);
event.time = Time;
Append(std::move(event));
}
CATCH_LOG()
namespace {
// Values sharing a key are OR'd, distinct keys are AND'd. Unrecognized keys are ignored.
bool EventMatchesFilters(const wsl::windows::common::wslc_schema::Event& event, const std::map<std::string, std::vector<std::string>>& filters)
{
for (const auto& [key, values] : filters)
{
if (key == "type")
{
if (!std::ranges::any_of(values, [&](const std::string& v) { return event.Type == v; }))
{
return false;
}
}
else if (key == "event")
{
if (!std::ranges::any_of(values, [&](const std::string& v) { return event.Action == v; }))
{
return false;
}
}
else if (key == "container")
{
if (event.Type != "container" ||
!std::ranges::any_of(values, [&](const std::string& v) { return event.Actor.ID == v; }))
{
return false;
}
}
else if (key == "image")
{
if (event.Type != "image" || !std::ranges::any_of(values, [&](const std::string& v) { return event.Actor.ID == v; }))
{
return false;
}
}
}
return true;
}
} // namespace
Microsoft::WRL::ComPtr<IWSLCEventStream> EventStore::CreateStream(
Microsoft::WRL::ComPtr<WSLCSession> Session, int64_t SinceTime, int64_t UntilTime, std::map<std::string, std::vector<std::string>> Filters)
{
// Zero means unbounded on that end, so it never makes the window run backwards.
THROW_HR_WITH_USER_ERROR_IF(
E_INVALIDARG,
Localization::MessageWslcEventsInvalidTimeWindow(SinceTime, UntilTime),
SinceTime < 0 || UntilTime < 0 || (SinceTime != 0 && UntilTime != 0 && SinceTime > UntilTime));
Microsoft::WRL::ComPtr<EventStream> stream;
THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize<EventStream>(&stream, std::move(Session), this, SinceTime, UntilTime, std::move(Filters)));
return stream;
}
std::optional<wsl::windows::common::wslc_schema::Event> EventStore::GetLockHeld(uint64_t SequenceNumber)
{
// Callers resync a lagging reader before reaching here, so the requested event is never evicted.
WI_ASSERT(SequenceNumber >= m_firstSequenceNumber);
const uint64_t index = SequenceNumber - m_firstSequenceNumber;
if (index >= m_events.size())
{
return std::nullopt;
}
return m_events[index];
}
bool EventStore::WaitForEvent(std::unique_lock<std::mutex>& Lock, uint64_t SequenceNumber, std::optional<std::chrono::sys_seconds> Until)
{
// Ready once the reader's event is buffered, its slot is evicted, or the session terminates.
// Eviction while parked wakes us too, so the caller reports the gap on its next pass.
const auto ready = [&] { return m_terminating || SequenceNumber < m_firstSequenceNumber + m_events.size(); };
if (Until.has_value())
{
if (!m_updated.wait_until(Lock, Until.value(), ready))
{
return false;
}
}
else
{
m_updated.wait(Lock, ready);
}
THROW_HR_IF(E_ABORT, m_terminating);
return true;
}
std::optional<wsl::windows::common::wslc_schema::Event> EventStore::Get(
std::optional<uint64_t>& SequenceNumber,
std::optional<std::chrono::sys_seconds> Since,
std::optional<std::chrono::sys_seconds> Until,
const std::map<std::string, std::vector<std::string>>& Filters)
{
std::unique_lock lock(m_lock);
// Position the reader. A first read (no sequence number yet) starts at the oldest buffered
// event
SequenceNumber = SequenceNumber.value_or(m_firstSequenceNumber);
while (true)
{
// A reader that has fallen behind the ring missed events to eviction: reset it so the
// next call starts fresh at the oldest buffered event, and report the gap.
if (SequenceNumber.value() < m_firstSequenceNumber)
{
SequenceNumber = std::nullopt;
THROW_HR(WSLC_E_EVENTS_LOST);
}
if (!WaitForEvent(lock, SequenceNumber.value(), Until))
{
// The until window elapsed with no further event: the stream is finished.
return std::nullopt;
}
// Evicted while parked: loop back to reset and report the gap.
// TODO: A burst of more than c_eventRingCapacity events between the wake and reacquiring the
// lock can evict this reader's event before it is read, forcing a WSLC_E_EVENTS_LOST. Redesign
// so that every parked reader is guaranteed to observe an event before the next write can evict
// it.
if (SequenceNumber.value() < m_firstSequenceNumber)
{
continue;
}
const auto event = GetLockHeld(SequenceNumber.value()).value();
const std::chrono::sys_seconds eventTime{std::chrono::seconds{event.time}};
// Advance in delivery order before applying the time window.
SequenceNumber.value()++;
// Events are appended in non-decreasing timestamp order (see Append()), so once we reach the
// exclusive Until bound, the stream is finished.
if (Until.has_value() && eventTime >= Until.value())
{
return std::nullopt;
}
// Return the event if it falls within the since-bound and matches the caller's filters;
// otherwise loop to skip it.
if ((!Since.has_value() || eventTime >= Since.value()) && EventMatchesFilters(event, Filters))
{
return event;
}
}
}
void EventStore::OnSessionTerminating()
{
{
std::lock_guard lock(m_lock);
m_terminating = true;
}
m_updated.notify_all();
}
HRESULT EventStream::RuntimeClassInitialize(
Microsoft::WRL::ComPtr<WSLCSession> Session,
EventStore* Store,
int64_t SinceTime,
int64_t UntilTime,
std::map<std::string, std::vector<std::string>> Filters)
{
m_session = std::move(Session);
m_store = Store;
m_since = ToTimeBound(SinceTime);
m_until = ToTimeBound(UntilTime);
m_filters = std::move(Filters);
return S_OK;
}
HRESULT EventStream::GetNext(LPSTR* EventJson)
try
{
RETURN_HR_IF_NULL(E_POINTER, EventJson);
*EventJson = nullptr;
std::lock_guard lock(m_lock);
const auto event = m_store->Get(m_nextSequenceNumber, m_since, m_until, m_filters);
if (!event.has_value())
{
return WSLC_E_EVENT_STREAM_FINISHED;
}
*EventJson = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(wsl::shared::ToJson(event.value()).c_str()).release();
return S_OK;
}
CATCH_RETURN();
} // namespace wsl::windows::service::wslc
+88
View File
@@ -0,0 +1,88 @@
// Copyright (C) Microsoft Corporation. All rights reserved.
#pragma once
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <map>
#include <mutex>
#include <optional>
#include <string>
#include <vector>
#include "wslc.h"
#include "wslc_schema.h"
namespace wsl::windows::service::wslc {
class WSLCSession;
class EventStore
{
public:
static constexpr size_t c_eventRingCapacity = 256;
void Record(std::string&& Type, std::string&& Action, const std::string& ActorId, std::map<std::string, std::string> ActorAttributes, std::int64_t Time) noexcept;
Microsoft::WRL::ComPtr<IWSLCEventStream> CreateStream(
Microsoft::WRL::ComPtr<WSLCSession> Session, int64_t SinceTime, int64_t UntilTime, std::map<std::string, std::vector<std::string>> Filters);
// Returns the next event at or after SequenceNumber that falls within [Since, Until) and matches
// Filters, advancing SequenceNumber past it. A nullopt SequenceNumber starts a fresh reader at the
// oldest buffered event (no gap is reported). If the reader has since fallen behind the ring,
// resyncs SequenceNumber to the oldest buffered event and throws WSLC_E_EVENTS_LOST. Returns
// nullopt once the Until window has closed.
std::optional<wsl::windows::common::wslc_schema::Event> Get(
std::optional<uint64_t>& SequenceNumber,
std::optional<std::chrono::sys_seconds> Since,
std::optional<std::chrono::sys_seconds> Until,
const std::map<std::string, std::vector<std::string>>& Filters);
void OnSessionTerminating();
private:
void Append(wsl::windows::common::wslc_schema::Event Event);
// Blocks until the event at SequenceNumber is buffered, its slot is evicted, or the session
// terminates. Returns false only when Until elapsed with no event ready. Throws E_ABORT if the
// session terminated while waiting.
bool WaitForEvent(std::unique_lock<std::mutex>& Lock, uint64_t SequenceNumber, std::optional<std::chrono::sys_seconds> Until);
std::optional<wsl::windows::common::wslc_schema::Event> GetLockHeld(uint64_t SequenceNumber);
std::mutex m_lock;
std::condition_variable m_updated;
_Guarded_by_(m_lock) std::deque<wsl::windows::common::wslc_schema::Event> m_events;
_Guarded_by_(m_lock) uint64_t m_firstSequenceNumber = 1;
_Guarded_by_(m_lock) bool m_terminating = false;
};
class EventStream
: public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IWSLCEventStream, IFastRundown>
{
public:
HRESULT RuntimeClassInitialize(
Microsoft::WRL::ComPtr<WSLCSession> Session,
EventStore* Store,
int64_t SinceTime,
int64_t UntilTime,
std::map<std::string, std::vector<std::string>> Filters);
IFACEMETHOD(GetNext)(_Outptr_result_z_ LPSTR* EventJson) override;
private:
Microsoft::WRL::ComPtr<WSLCSession> m_session;
EventStore* m_store = nullptr;
std::optional<std::chrono::sys_seconds> m_since;
std::optional<std::chrono::sys_seconds> m_until;
std::map<std::string, std::vector<std::string>> m_filters;
std::mutex m_lock;
_Guarded_by_(m_lock) std::optional<uint64_t> m_nextSequenceNumber;
};
} // namespace wsl::windows::service::wslc
+53 -13
View File
@@ -474,6 +474,21 @@ WSLCContainerState DockerStateToWSLCState(ContainerState state)
}
}
std::string WSLCStateToEventAction(WSLCContainerState state)
{
switch (state)
{
case WslcContainerStateRunning:
return "start";
case WslcContainerStateExited:
return "stop";
case WslcContainerStateDeleted:
return "destroy";
default:
WI_ASSERT(false);
return "unknown";
}
}
std::string CleanContainerName(const std::string& name)
{
// Docker container names have a leading '/', strip it.
@@ -814,6 +829,7 @@ WSLCContainerImpl::WSLCContainerImpl(
std::vector<ContainerPortMapping>&& ports,
std::map<std::string, std::string>&& labels,
std::function<void(const WSLCContainerImpl*)>&& onDeleted,
EventStore& eventStore,
WSLCContainerState InitialState,
std::int64_t CreatedAt,
WSLCProcessFlags InitProcessFlags,
@@ -832,6 +848,7 @@ WSLCContainerImpl::WSLCContainerImpl(
m_comWrapper(wil::MakeOrThrow<WSLCContainer>(wslcSession, std::move(onDeleted))),
m_containerEvents(runtime.Events().RegisterContainerStateUpdates(
m_id, std::bind(&WSLCContainerImpl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))),
m_eventStore(eventStore),
m_state(InitialState),
m_createdAt(CreatedAt),
m_initProcessFlags(InitProcessFlags),
@@ -1224,12 +1241,34 @@ __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::CompleteTransitio
transition->Completed.SetEvent();
}
void WSLCContainerImpl::RecordEvent(std::string&& Action, std::int64_t Time, std::optional<int> ExitCode) noexcept
try
{
auto attributes = StripInternalLabels(m_labels);
attributes["name"] = m_name;
attributes["image"] = m_image;
if (ExitCode.has_value())
{
attributes["exitCode"] = std::to_string(ExitCode.value());
}
m_eventStore.Record("container", std::move(Action), m_id, std::move(attributes), Time);
}
CATCH_LOG()
void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional<int> exitCode, std::int64_t eventTime) noexcept
{
// Either owner may disconnect the COM wrapper, so both must outlive m_lock.
unique_com_disconnect comWrapper;
std::shared_ptr<StateTransition> transition;
if (event == ContainerEvent::Kill)
{
RecordEvent("kill", eventTime);
return;
}
{
auto lifecycleLock = m_lifecycleLock.lock_exclusive();
auto lock = m_lock.lock_exclusive();
@@ -1396,7 +1435,7 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill)
}
}
__requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exitCode, std::optional<std::int64_t> stopTimestamp)
__requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exitCode, std::int64_t stopTime)
{
auto transition = m_transition;
@@ -1427,7 +1466,7 @@ __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exi
// Ignore duplicate or late Stop events so they do not overwrite an already committed state.
if (m_state == WslcContainerStateRunning)
{
CommitState(WslcContainerStateExited, stopTimestamp);
CommitState(WslcContainerStateExited, stopTime, exitCode);
}
std::exception_ptr transitionException;
@@ -2048,11 +2087,11 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
WSLCSessionRuntime& runtime,
IWSLCPluginNotifier* pluginNotifier,
const std::unordered_map<std::string, NetworkEntry>& sessionNetworks,
std::function<void(const WSLCContainerImpl*)>&& OnDeleted)
std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
EventStore& eventStore)
{
auto& virtualMachine = runtime.Vm();
auto& DockerClient = runtime.Docker();
auto& EventTracker = runtime.Events();
const auto mounts = ConvertAndValidateMounts(containerOptions);
common::docker_schema::CreateContainer request;
@@ -2486,11 +2525,6 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
name.c_str());
}
// Wait for the container create event to be delivered on the Docker event stream so that
// any events for objects created for the container (e.g. volumes) are delivered before we return
// from this function.
EventTracker.WaitForObjectCreated(result.Id);
// Collect the names of referenced docker named volumes so Start() can verify
// they are available before running the container.
std::vector<std::string> namedVolumes;
@@ -2509,6 +2543,7 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
}
auto mergedLabels = StripInternalLabels(std::move(inspectData.Config.Labels));
const auto createdAt = wsl::windows::common::timestamp::Rfc3339ToEpoch(inspectData.Created);
auto container = std::make_shared<WSLCContainerImpl>(
wslcSession,
@@ -2523,8 +2558,9 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
std::move(mappedPorts),
std::move(mergedLabels),
std::move(OnDeleted),
eventStore,
WslcContainerStateCreated,
wsl::windows::common::timestamp::Rfc3339ToEpoch(inspectData.Created),
createdAt,
containerOptions.InitProcessOptions.Flags,
containerOptions.Flags);
@@ -2539,7 +2575,8 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
WSLCSession& wslcSession,
WSLCSessionRuntime& runtime,
IWSLCPluginNotifier* pluginNotifier,
std::function<void(const WSLCContainerImpl*)>&& OnDeleted)
std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
EventStore& eventStore)
{
auto& virtualMachine = runtime.Vm();
auto& DockerClient = runtime.Docker();
@@ -2611,6 +2648,7 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
std::move(ports),
std::move(labels),
std::move(OnDeleted),
eventStore,
DockerStateToWSLCState(dockerContainer.State),
dockerContainer.Created,
metadata.InitProcessFlags,
@@ -2917,7 +2955,7 @@ __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::
return unique_com_disconnect{std::exchange(m_comWrapper, nullptr)};
}
__requires_lock_held(m_lock) void WSLCContainerImpl::CommitState(WSLCContainerState State, std::optional<std::int64_t> stateChangedAt) noexcept
__requires_lock_held(m_lock) void WSLCContainerImpl::CommitState(WSLCContainerState State, std::int64_t Time, std::optional<int> ExitCode) noexcept
{
// N.B. A deleted container cannot transition back to any other state.
WI_ASSERT(m_state != WslcContainerStateDeleted);
@@ -2930,7 +2968,9 @@ __requires_lock_held(m_lock) void WSLCContainerImpl::CommitState(WSLCContainerSt
m_state = State;
m_stateGeneration++;
m_stateChangedAt = stateChangedAt.value_or(static_cast<std::int64_t>(std::time(nullptr)));
m_stateChangedAt = Time;
RecordEvent(WSLCStateToEventAction(State), Time, ExitCode);
// Keep the VM alive while this container is Running and release the hold once it leaves that
// state, even when no client holds the wrapper (e.g. a detached `run -d` container). Dropping
+13 -4
View File
@@ -35,6 +35,7 @@ class WSLCContainer;
class WSLCSession;
class WSLCSessionRuntime;
class WSLCVolumes;
class EventStore;
class unique_com_disconnect
{
@@ -85,6 +86,7 @@ public:
std::vector<ContainerPortMapping>&& ports,
std::map<std::string, std::string>&& labels,
std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
EventStore& eventStore,
WSLCContainerState InitialState,
std::int64_t CreatedAt,
WSLCProcessFlags InitProcessFlags,
@@ -123,7 +125,7 @@ public:
// Re-registers a stopped container's VM-scoped port allocations against the restarted VM.
void RecoverPorts(const common::docker_schema::ContainerInfo& dockerContainer);
__requires_lock_held(m_lock) void CommitState(WSLCContainerState State, std::optional<std::int64_t> stateChangedAt = std::nullopt) noexcept;
__requires_lock_held(m_lock) void CommitState(WSLCContainerState State, std::int64_t Time, std::optional<int> ExitCode = std::nullopt) noexcept;
const std::string& ID() const noexcept;
@@ -141,14 +143,20 @@ public:
WSLCSessionRuntime& runtime,
IWSLCPluginNotifier* pluginNotifier,
const std::unordered_map<std::string, NetworkEntry>& SessionNetworks,
std::function<void(const WSLCContainerImpl*)>&& OnDeleted);
std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
EventStore& eventStore);
static std::shared_ptr<WSLCContainerImpl> Open(
const common::docker_schema::ContainerInfo& DockerContainer,
WSLCSession& wslcSession,
WSLCSessionRuntime& runtime,
IWSLCPluginNotifier* pluginNotifier,
std::function<void(const WSLCContainerImpl*)>&& OnDeleted);
std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
EventStore& eventStore);
// Appends an event for this container to the session's event stream. Must be called from the Docker
// event stream thread so that recorded events keep Docker's delivery order.
void RecordEvent(std::string&& Action, std::int64_t Time, std::optional<int> ExitCode = std::nullopt) noexcept;
private:
enum class TransitionKind
@@ -197,7 +205,7 @@ private:
__requires_exclusive_lock_held(m_lock) void ReleaseProcesses();
__requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect PrepareDisconnectComWrapper();
__requires_exclusive_lock_held(m_lock) void OnStopped(int exitCode, std::optional<std::int64_t> stopTimestamp);
__requires_exclusive_lock_held(m_lock) void OnStopped(int exitCode, std::int64_t stopTime);
void SetExitCode(int ExitCode) noexcept;
void SignalInitProcessExit() noexcept;
@@ -252,6 +260,7 @@ private:
std::map<std::string, std::string> m_labels;
Microsoft::WRL::ComPtr<WSLCContainer> m_comWrapper;
DockerEventTracker::EventTrackingReference m_containerEvents;
EventStore& m_eventStore;
std::string m_networkMode;
// Held (non-empty) exactly while the container is Running so the session's VM stays alive even
@@ -172,7 +172,7 @@ void DockerExecProcessControl::SetExitCode(int ExitCode)
}
}
void DockerExecProcessControl::OnEvent(ContainerEvent Event, std::optional<int> ExitCode, std::int64_t /*eventTime*/)
void DockerExecProcessControl::OnEvent(ContainerEvent Event, std::optional<int> ExitCode, std::int64_t)
{
if (Event == ContainerEvent::ExecDied && !m_exitEvent.is_signaled())
{
+135 -7
View File
@@ -41,6 +41,7 @@ using wsl::windows::service::wslc::WSLCVirtualMachine;
constexpr auto c_containerdSocket = "/run/containerd/containerd.sock";
constexpr auto c_storageVhdFilename = wsl::windows::wslc::DefaultStorageVhdName;
constexpr uint32_t c_progressPrecision = 4;
constexpr auto c_containerCreateEventTimeout = std::chrono::seconds{60};
// Default grace period to keep an otherwise-idle VM running before tearing it down (used when the
// session's IdleTimeoutSec setting is 0/unset). This avoids thrashing the VM (repeated
@@ -499,6 +500,9 @@ try
m_runtime.Initialize(m_vmFactoryGitCookie, m_git, &m_settings, idleGracePeriod, std::move(sessionContext), std::move(hooks));
m_containerEventTracking = m_runtime.Events().RegisterContainerCreate(
std::bind(&WSLCSession::OnContainerCreated, this, std::placeholders::_1, std::placeholders::_2));
return S_OK;
}
CATCH_RETURN()
@@ -2312,7 +2316,9 @@ void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptio
try
{
std::scoped_lock lock(m_containersLock, m_networksLock);
std::unique_lock containersLock{m_containersLock};
WaitForConflictingCreateToComplete(containersLock);
std::unique_lock networksLock{m_networksLock};
// Generate a unique container name if the user didn't provide one.
std::string containerName;
@@ -2350,13 +2356,24 @@ void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptio
m_runtime,
m_pluginNotifier.get(),
m_networks,
std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1));
std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1),
m_eventStore);
// Key the map by Docker's container ID, which is set in the WSLCContainerImpl constructor and stable for its lifetime.
auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container));
WI_ASSERT(inserted);
auto pendingCreate = StartPendingCreate(container);
it->second->CopyTo(Container);
containersLock.unlock();
networksLock.unlock();
// m_pendingCreate is published under m_containersLock before the event thread can observe it, so
// OnContainerCreated() is guaranteed to complete this create unless the session tears down first.
WaitForPendingCreateCompletion(pendingCreate);
if (pendingCreate->Exception)
{
std::rethrow_exception(pendingCreate->Exception);
}
container->CopyTo(Container);
}
catch (const DockerHTTPException& e)
{
@@ -2372,6 +2389,97 @@ void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptio
}
}
__requires_lock_held(m_containersLock) std::shared_ptr<WSLCSession::PendingContainerCreate> WSLCSession::StartPendingCreate(std::shared_ptr<WSLCContainerImpl> Container)
{
WI_ASSERT(!m_pendingCreate);
m_pendingCreate = std::make_shared<PendingContainerCreate>();
m_pendingCreate->Container = std::move(Container);
return m_pendingCreate;
}
void WSLCSession::WaitForPendingCreateCompletion(const std::shared_ptr<PendingContainerCreate>& PendingCreate)
{
auto io = CreateIOContext();
io.AddHandle(std::make_unique<io::EventHandle>(PendingCreate->Completed.get()));
try
{
io.Run(c_containerCreateEventTimeout);
}
catch (...)
{
if (wil::ResultFromCaughtException() != HRESULT_FROM_WIN32(ERROR_TIMEOUT))
{
throw;
}
// Fail this create rather than leaving m_pendingCreate set, which would wedge every later one.
// Any container docker did manage to create is left behind; the same broken event stream makes
// deleting it unreliable, and a late create event is ignored once m_pendingCreate is cleared.
std::lock_guard containersLock{m_containersLock};
if (m_pendingCreate == PendingCreate)
{
CompletePendingCreate(PendingCreate, std::current_exception());
}
}
WI_ASSERT(PendingCreate->Completed.is_signaled());
}
__requires_lock_held(m_containersLock) void WSLCSession::CompletePendingCreate(
const std::shared_ptr<PendingContainerCreate>& PendingCreate, std::exception_ptr Exception) noexcept
{
WI_ASSERT(m_pendingCreate == PendingCreate);
PendingCreate->Exception = std::move(Exception);
m_pendingCreate.reset();
PendingCreate->Completed.SetEvent();
}
void WSLCSession::WaitForConflictingCreateToComplete(std::unique_lock<std::mutex>& ContainersLock)
{
while (m_pendingCreate)
{
auto pendingCreate = m_pendingCreate;
ContainersLock.unlock();
WaitForPendingCreateCompletion(pendingCreate);
ContainersLock.lock();
}
}
void WSLCSession::OnContainerCreated(const std::string& ContainerId, std::int64_t Time) noexcept
try
{
std::lock_guard containersLock{m_containersLock};
// Containers created behind our back (BuildKit, for instance) have no pending create to match.
if (!m_pendingCreate || m_pendingCreate->Container->ID() != ContainerId)
{
return;
}
auto pendingCreate = m_pendingCreate;
std::exception_ptr exception;
try
{
// Key the map by Docker's container ID, which is set in the WSLCContainerImpl constructor and stable for its lifetime.
WI_VERIFY(m_containers.emplace(ContainerId, pendingCreate->Container).second);
pendingCreate->Container->RecordEvent("create", Time);
}
catch (...)
{
// Hand the failure to the waiting create rather than letting it return a container the session isn't tracking.
exception = std::current_exception();
}
CompletePendingCreate(pendingCreate, std::move(exception));
}
CATCH_LOG()
HRESULT WSLCSession::OpenContainer(LPCSTR Id, IWSLCContainer** Container)
try
{
@@ -3342,6 +3450,9 @@ try
if (!m_sessionTerminatingEvent.is_signaled())
{
m_sessionTerminatingEvent.SetEvent();
// Wake any readers parked in an event stream so they abort instead of waiting forever.
m_eventStore.OnSessionTerminating();
}
// Cancel any pending IO on user-provided handles to unblock operations
@@ -3907,6 +4018,23 @@ try
}
CATCH_RETURN();
HRESULT WSLCSession::GetEvents(LONGLONG SinceTime, LONGLONG UntilTime, const WSLCFilter* Filters, ULONG FiltersCount, IWSLCEventStream** Stream)
try
{
WSLCExecutionContext context(this);
RETURN_HR_IF_NULL(E_POINTER, Stream);
*Stream = nullptr;
auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
auto stream = m_eventStore.CreateStream(Microsoft::WRL::ComPtr<WSLCSession>{this}, SinceTime, UntilTime, std::move(filters));
*Stream = stream.Detach();
return S_OK;
}
CATCH_RETURN();
void WSLCSession::RecoverExistingContainers()
{
WI_ASSERT(m_runtime.HasDocker());
@@ -3940,7 +4068,7 @@ void WSLCSession::RecoverExistingContainers()
try
{
auto container = WSLCContainerImpl::Open(
dockerContainer, *this, m_runtime, m_pluginNotifier.get(), std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1));
dockerContainer, *this, m_runtime, m_pluginNotifier.get(), std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), m_eventStore);
auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container));
WI_ASSERT(inserted);
+42 -5
View File
@@ -24,6 +24,7 @@ Abstract:
#include "WSLCNetworkMetadata.h"
#include "DockerEventTracker.h"
#include "DockerHTTPClient.h"
#include "EventStore.h"
#include "IORelay.h"
#include <atomic>
#include <list>
@@ -114,6 +115,15 @@ public:
IFACEMETHOD(GetTerminationEvent)(_Out_ HANDLE* Event) override;
IFACEMETHOD(GetTerminationReason)(_Out_ WSLCVirtualMachineTerminationReason* Reason, _Out_ LPWSTR* Details) override;
// Event streaming. Opens a stream object that yields matching events one at a time via
// IWSLCEventStream::GetNext.
IFACEMETHOD(GetEvents)(
_In_ LONGLONG SinceTime,
_In_ LONGLONG UntilTime,
_In_reads_opt_(FiltersCount) const WSLCFilter* Filters,
_In_ ULONG FiltersCount,
_Outptr_ IWSLCEventStream** Stream) override;
// Image management.
IFACEMETHOD(PullImage)(
_In_ LPCSTR Image,
@@ -262,11 +272,6 @@ public:
UserCOMCallback RegisterUserCOMCallback();
void UnregisterUserCOMCallback(DWORD ThreadId);
HANDLE SessionTerminatingEvent() const noexcept
{
return m_sessionTerminatingEvent.get();
}
ULONG Id() const noexcept
{
return m_id;
@@ -319,6 +324,30 @@ private:
void CreateContainerImpl(const WSLCContainerOptions* Options, IWSLCContainer** Container);
// A create RPC hands the container off to the Docker event stream thread here and waits, so the
// container is committed and its create event recorded from that thread. Recording it from the RPC
// thread instead would let another container's event be recorded first and regress the event
// stream's timestamps.
struct PendingContainerCreate
{
wil::unique_event Completed{wil::EventOptions::ManualReset};
std::shared_ptr<WSLCContainerImpl> Container;
std::exception_ptr Exception;
};
__requires_lock_held(m_containersLock) std::shared_ptr<PendingContainerCreate> StartPendingCreate(std::shared_ptr<WSLCContainerImpl> Container);
__requires_lock_held(m_containersLock) void CompletePendingCreate(
const std::shared_ptr<PendingContainerCreate>& PendingCreate, std::exception_ptr Exception) noexcept;
void WaitForPendingCreateCompletion(const std::shared_ptr<PendingContainerCreate>& PendingCreate);
// Returns with the lock held once no create is in flight. Only one fits: the slot is unkeyed,
// because the container ID isn't known until Docker assigns it.
void WaitForConflictingCreateToComplete(std::unique_lock<std::mutex>& ContainersLock);
void OnContainerCreated(const std::string& ContainerId, std::int64_t Time) noexcept;
void ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID UserSid);
void Ext4Format(const std::string& Device);
@@ -388,6 +417,14 @@ private:
wil::com_ptr<IWSLCPluginNotifier> m_pluginNotifier;
// Bounded in-memory ring of recent lifecycle events, shared by all event-stream subscribers.
EventStore m_eventStore;
__guarded_by(m_containersLock) std::shared_ptr<PendingContainerCreate> m_pendingCreate;
// N.B. Declared after everything OnContainerCreated() touches so the callback is unregistered first.
DockerEventTracker::EventTrackingReference m_containerEventTracking;
// User-provided handles that the session is currently doing IO on.
std::mutex m_userHandlesLock;
__guarded_by(m_userHandlesLock) std::vector<HANDLE> m_userHandles;
+289
View File
@@ -27,6 +27,7 @@ Abstract:
#include "WSLCSessionDefaults.h"
#include <nlohmann/json.hpp>
using namespace std::chrono;
using namespace std::literals::chrono_literals;
using namespace wsl::windows::common::registry;
using wsl::windows::common::ClientRunningWSLCProcess;
@@ -6857,6 +6858,294 @@ class WSLCTests
}
}
WSLC_TEST_METHOD(EventStream)
{
constexpr auto c_containerName = "wslc-test-events";
constexpr auto c_imageName = "debian:latest";
constexpr auto c_labelKey = "event-label";
constexpr auto c_labelValue = "event-value";
const auto expectedExitCode = std::to_string(128 + WSLCSignalSIGKILL);
auto now = [] { return duration_cast<seconds>(system_clock::now().time_since_epoch()).count(); };
// Drains a bounded event stream to completion (GetNext returns WSLC_E_EVENT_STREAM_FINISHED
// once the until-time has passed and the backlog is exhausted), parsing each event's JSON.
auto drain = [](IWSLCEventStream* stream) {
std::vector<wsl::windows::common::wslc_schema::Event> events;
wil::unique_cotaskmem_ansistring eventJson;
HRESULT result;
while (SUCCEEDED(result = stream->GetNext(&eventJson)))
{
events.push_back(wsl::shared::FromJson<wsl::windows::common::wslc_schema::Event>(eventJson.get()));
}
VERIFY_ARE_EQUAL(WSLC_E_EVENT_STREAM_FINISHED, result);
return events;
};
// Verifies the given events match the expected actions in order for a given actor.
auto verifyEvents = [&](const std::vector<wsl::windows::common::wslc_schema::Event>& events,
const std::string& actorId,
const std::vector<std::string>& expectedActions) {
VERIFY_ARE_EQUAL(events.size(), expectedActions.size());
for (size_t i = 0; i < expectedActions.size(); ++i)
{
const auto& action = expectedActions[i];
const auto& event = events[i];
VERIFY_ARE_EQUAL(action, event.Action);
VERIFY_ARE_EQUAL(actorId, event.Actor.ID);
VERIFY_ARE_EQUAL(c_containerName, event.Actor.Attributes.at("name"));
VERIFY_ARE_EQUAL(c_imageName, event.Actor.Attributes.at("image"));
VERIFY_ARE_EQUAL(c_labelValue, event.Actor.Attributes.at(c_labelKey));
VERIFY_IS_FALSE(event.Actor.Attributes.contains("com.microsoft.wsl.container.metadata"));
if (action == "stop")
{
VERIFY_ARE_EQUAL(expectedExitCode, event.Actor.Attributes.at("exitCode"));
}
else
{
VERIFY_IS_FALSE(event.Actor.Attributes.contains("exitCode"));
}
}
};
// Run a container through its create/start/kill/stop lifecycle inside a bounded time window.
const LONGLONG since = now();
std::string id;
{
WSLCContainerLauncher launcher(c_imageName, c_containerName, {"sleep", "99999"});
launcher.AddLabel(c_labelKey, c_labelValue);
auto container = launcher.Launch(*m_defaultSession);
id = container.Id();
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
// Kill (rather than Stop) so Docker emits a 'kill' event ahead of the 'die' that stops it.
VERIFY_SUCCEEDED(container.Get().Kill(WSLCSignalSIGKILL));
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
}
const LONGLONG until = now() + 1;
std::vector<wsl::windows::common::wslc_schema::Event> lifecycleEvents;
// The container's create, start, kill, stop, then destroy events are reported in order, each carrying
// the container's 64-hex id as the actor.
{
WSLCFilter filter{"container", id.c_str()};
wil::com_ptr<IWSLCEventStream> stream;
VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since, until, &filter, 1, &stream));
lifecycleEvents = drain(stream.get());
verifyEvents(lifecycleEvents, id, {"create", "start", "kill", "stop", "destroy"});
// The whole lifecycle falls inside the requested window.
VERIFY_IS_TRUE(lifecycleEvents[0].time >= since);
VERIFY_IS_TRUE(lifecycleEvents[4].time < until);
}
// Each lifecycle action is independently selectable: an 'event=<action>' filter, AND'd with
// the container filter, returns exactly that one event out of the five recorded above.
auto verifyEventFilter = [&](const char* action) {
WSLCFilter filters[]{{"container", id.c_str()}, {"event", action}};
wil::com_ptr<IWSLCEventStream> stream;
VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since, until, filters, ARRAYSIZE(filters), &stream));
verifyEvents(drain(stream.get()), id, {action});
};
verifyEventFilter("create");
verifyEventFilter("start");
verifyEventFilter("kill");
verifyEventFilter("stop");
verifyEventFilter("destroy");
// Values sharing a filter key are OR'd.
{
WSLCFilter filters[]{{"container", id.c_str()}, {"event", "create"}, {"event", "destroy"}};
wil::com_ptr<IWSLCEventStream> stream;
VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since, until, filters, ARRAYSIZE(filters), &stream));
verifyEvents(drain(stream.get()), id, {"create", "destroy"});
}
// Image events are not recorded yet, so a 'type=image' filter excludes the container's
// events and leaves the stream empty.
{
WSLCFilter filter{"type", "image"};
wil::com_ptr<IWSLCEventStream> stream;
VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since, until, &filter, 1, &stream));
VERIFY_IS_TRUE(drain(stream.get()).empty());
}
// An unmatched container id yields an empty stream, and GetNext validates its out-pointer.
{
WSLCFilter filter{"container", "0000000000000000000000000000000000000000000000000000000000000000"};
wil::com_ptr<IWSLCEventStream> stream;
VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since, until, &filter, 1, &stream));
VERIFY_IS_TRUE(drain(stream.get()).empty());
}
// A since-time later than a non-zero until-time describes a backwards window and is rejected.
{
wil::com_ptr<IWSLCEventStream> stream;
VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->GetEvents(since + 1, since, nullptr, 0, &stream));
ValidateCOMErrorMessage(wsl::shared::Localization::MessageWslcEventsInvalidTimeWindow(since + 1, since));
}
}
WSLC_TEST_METHOD(EventStreamReportsLostEvents)
{
// One more than the store's ring capacity, so the reader's next slot is guaranteed evicted.
constexpr size_t c_signalsToEvictReader = 257;
WSLCContainerLauncher launcher("debian:latest", "wslc-test-event-stream-overrun", {"sleep", "99999"});
auto container = launcher.Launch(*m_defaultSession);
const auto id = container.Id();
WSLCFilter filter{"container", id.c_str()};
wil::com_ptr<IWSLCEventStream> stream;
VERIFY_SUCCEEDED(m_defaultSession->GetEvents(0, 0, &filter, 1, &stream));
// Read one event to place the reader's cursor inside the ring.
wil::unique_cotaskmem_ansistring eventJson;
VERIFY_SUCCEEDED(stream->GetNext(&eventJson));
const auto firstEvent = wsl::shared::FromJson<wsl::windows::common::wslc_schema::Event>(eventJson.get());
VERIFY_ARE_EQUAL("create", firstEvent.Action);
VERIFY_ARE_EQUAL(id, firstEvent.Actor.ID);
// Docker emits a 'kill' event per signal. SIGWINCH is ignored by an unhandling init process, so the
// container keeps running and each signal costs only one event.
for (size_t i = 0; i < c_signalsToEvictReader; ++i)
{
VERIFY_SUCCEEDED(container.Get().Kill(WSLCSignalSIGWINCH));
}
// Stopping waits for the 'die' event, which Docker delivers after every preceding 'kill'. Without this
// barrier the reader could be checked before the ring has overrun it.
VERIFY_SUCCEEDED(container.Get().Kill(WSLCSignalSIGKILL));
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
VERIFY_ARE_EQUAL(WSLC_E_EVENTS_LOST, stream->GetNext(&eventJson));
// Reporting the gap resyncs the reader, so it resumes from the oldest event still buffered.
VERIFY_SUCCEEDED(stream->GetNext(&eventJson));
const auto resumedEvent = wsl::shared::FromJson<wsl::windows::common::wslc_schema::Event>(eventJson.get());
VERIFY_ARE_EQUAL("kill", resumedEvent.Action);
VERIFY_ARE_EQUAL(id, resumedEvent.Actor.ID);
}
WSLC_TEST_METHOD(EventStreamSerializesConcurrentReaders)
{
constexpr auto c_containerName = "wslc-test-concurrent-event-readers";
WSLCContainerLauncher launcher("debian:latest", c_containerName, {"sleep", "99999"});
auto container = launcher.Launch(*m_defaultSession);
const auto id = container.Id();
WSLCFilter filters[]{{"container", id.c_str()}, {"event", "kill"}};
wil::com_ptr<IWSLCEventStream> stream;
// The window doubles as a hang guard, so it must comfortably outlast the waits below.
const LONGLONG until = duration_cast<seconds>(system_clock::now().time_since_epoch()).count() + 120;
VERIFY_SUCCEEDED(m_defaultSession->GetEvents(0, until, filters, ARRAYSIZE(filters), &stream));
wil::unique_cotaskmem_ansistring firstEventJson;
wil::unique_cotaskmem_ansistring secondEventJson;
HRESULT firstResult{};
HRESULT secondResult{};
wil::unique_event firstReaderStarted{wil::EventOptions::ManualReset};
wil::unique_event secondReaderStarted{wil::EventOptions::ManualReset};
std::thread firstReader;
std::thread secondReader;
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
LOG_IF_FAILED(container.Get().Kill(WSLCSignalSIGWINCH));
LOG_IF_FAILED(container.Get().Kill(WSLCSignalSIGWINCH));
if (firstReader.joinable())
{
firstReader.join();
}
if (secondReader.joinable())
{
secondReader.join();
}
});
firstReader = std::thread([&]() {
firstReaderStarted.SetEvent();
firstResult = stream->GetNext(&firstEventJson);
});
VERIFY_IS_TRUE(firstReaderStarted.wait(30 * 1000));
VERIFY_ARE_EQUAL(WAIT_TIMEOUT, WaitForSingleObject(firstReader.native_handle(), 100));
secondReader = std::thread([&]() {
secondReaderStarted.SetEvent();
secondResult = stream->GetNext(&secondEventJson);
});
VERIFY_IS_TRUE(secondReaderStarted.wait(30 * 1000));
VERIFY_ARE_EQUAL(WAIT_TIMEOUT, WaitForSingleObject(secondReader.native_handle(), 100));
HANDLE readers[]{firstReader.native_handle(), secondReader.native_handle()};
VERIFY_SUCCEEDED(container.Get().Kill(WSLCSignalSIGWINCH));
const DWORD completedReader = WaitForMultipleObjects(ARRAYSIZE(readers), readers, FALSE, 30 * 1000);
VERIFY_IS_TRUE(completedReader == WAIT_OBJECT_0 || completedReader == WAIT_OBJECT_0 + 1);
// One event completes exactly one call; the other stays serialized until another event arrives.
const DWORD pendingReader = completedReader == WAIT_OBJECT_0 ? 1 : 0;
VERIFY_ARE_EQUAL(WAIT_TIMEOUT, WaitForSingleObject(readers[pendingReader], 100));
VERIFY_SUCCEEDED(container.Get().Kill(WSLCSignalSIGWINCH));
VERIFY_ARE_EQUAL(WAIT_OBJECT_0, WaitForMultipleObjects(ARRAYSIZE(readers), readers, TRUE, 30 * 1000));
firstReader.join();
secondReader.join();
cleanup.release();
VERIFY_SUCCEEDED(firstResult);
VERIFY_SUCCEEDED(secondResult);
const auto firstEvent = wsl::shared::FromJson<wsl::windows::common::wslc_schema::Event>(firstEventJson.get());
const auto secondEvent = wsl::shared::FromJson<wsl::windows::common::wslc_schema::Event>(secondEventJson.get());
VERIFY_ARE_EQUAL("kill", firstEvent.Action);
VERIFY_ARE_EQUAL(id, firstEvent.Actor.ID);
VERIFY_ARE_EQUAL("kill", secondEvent.Action);
VERIFY_ARE_EQUAL(id, secondEvent.Actor.ID);
}
WSLC_TEST_METHOD(EventStreamSessionTerminationAbortsReader)
{
WSLCFilter filter{"type", "container"};
const LONGLONG since = duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
wil::com_ptr<IWSLCEventStream> stream;
VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since, 0, &filter, 1, &stream));
std::promise<HRESULT> getNextResult;
std::thread readerThread([&]() {
wil::unique_cotaskmem_ansistring eventJson;
getNextResult.set_value(stream->GetNext(&eventJson));
});
auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { readerThread.join(); });
auto future = getNextResult.get_future();
VERIFY_SUCCEEDED(m_defaultSession->Terminate());
auto restore = ResetTestSession();
// Termination wakes the parked reader; it must finish quickly and report E_ABORT.
FAIL_FAST_IF_MSG(
future.wait_for(10s) != std::future_status::ready, "event stream reader did not abort after session termination");
VERIFY_ARE_EQUAL(E_ABORT, future.get());
}
WSLC_TEST_METHOD(OpenContainer)
{
auto expectOpen = [&](const char* Id, HRESULT expectedResult = S_OK) {