Wslc events (#40971)

Kevin Vega committed Sep 3, 2026 at 18:06 UTC 75cd0f046957bbd9ffeb4e862621f8801dcb2651
18 files changed +1008 -91
doc/docs/api-reference/c/error-codes.md
+4
@@ -18,6 +18,8 @@
18 #define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */
19 #define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */
20 #define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */
21 +#define WSLC_E_EVENTS_LOST MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */
22 +#define WSLC_E_EVENT_STREAM_FINISHED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 18) /* 0x80040612 */
23 ```
24
25 | Symbol | Hex Value |
@@ -39,5 +41,7 @@
41 | `WSLC_E_VOLUME_NOT_AVAILABLE` | `0x8004060E` |
42 | `WSLC_E_SESSION_NOT_FOUND` | `0x8004060F` |
43 | `WSLC_E_VM_NOT_RUNNING` | `0x80040610` |
44 +| `WSLC_E_EVENTS_LOST` | `0x80040611` |
45 +| `WSLC_E_EVENT_STREAM_FINISHED` | `0x80040612` |
46
47 ---
localization/strings/en-US/Resources.resw
+4
@@ -2371,6 +2371,10 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2371 <value>Invalid name: '{}'</value>
2372 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2373 </data>
2374 + <data name = "MessageWslcEventsInvalidTimeWindow" xml:space = "preserve" >
2375 + <value>`since` time ({}) cannot be after `until` time ({})</value>
2376 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="`since`"}{Locked="`until`"}</comment>
2377 + </data>
2378 <data name = "MessagePathNotAbsolute" xml:space = "preserve" >
2379 <value>Path is not absolute: '{}'</value>
2380 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
msipackage/package.wix.in
+8
@@ -359,6 +359,14 @@
359 </RegistryKey>
360 </RegistryKey>
361
362 + <!-- IWSLCEventStream-->
363 + <RegistryKey Root="HKCR" Key="Interface\{7EC66D3B-D098-4D48-B69E-69166F6C4745}">
364 + <RegistryValue Value="IWSLCEventStream" Type="string" />
365 + <RegistryKey Key="ProxyStubClsid32">
366 + <RegistryValue Value="{4EA0C6DD-E9FF-48E7-994E-13A31D10DC60}" Type="string" />
367 + </RegistryKey>
368 + </RegistryKey>
369 +
370 <!-- ICrashDumpCallback-->
371 <RegistryKey Root="HKCR" Key="Interface\{8C5A7B14-9D26-4FAE-AB31-7E5BC23F4801}">
372 <RegistryValue Value="ICrashDumpCallback" Type="string" />
src/windows/WslcSDK/wslcsdk.h
+2
@@ -44,6 +44,8 @@ EXTERN_C_START
44 #define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */
45 #define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */
46 #define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */
47 +#define WSLC_E_EVENTS_LOST MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */
48 +#define WSLC_E_EVENT_STREAM_FINISHED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 18) /* 0x80040612 */
49
50 // Session values
51 #define WSLC_SESSION_OPTIONS_SIZE 72
src/windows/common/wslutil.cpp
+2
@@ -180,6 +180,8 @@ static const std::map<HRESULT, LPCWSTR> g_commonErrors{
180 X(WSLC_E_NETWORK_NOT_FOUND),
181 X(WSLC_E_SESSION_NOT_FOUND),
182 X(WSLC_E_VM_NOT_RUNNING),
183 + X(WSLC_E_EVENTS_LOST),
184 + X(WSLC_E_EVENT_STREAM_FINISHED),
185 X(WSLC_E_WU_SEARCH_FAILED),
186 X_WIN32(RPC_S_SERVER_UNAVAILABLE),
187 X_WIN32(ERROR_ELEVATION_REQUIRED),
src/windows/inc/wslc_schema.h
+18
@@ -348,4 +348,22 @@ struct VolumeListEntry
348 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(VolumeListEntry, Name, Driver, Mountpoint, Scope, Labels);
349 };
350
351 +struct EventActor
352 +{
353 + std::string ID;
354 + std::map<std::string, std::string> Attributes;
355 +
356 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(EventActor, ID, Attributes);
357 +};
358 +
359 +struct Event
360 +{
361 + std::string Type;
362 + std::string Action;
363 + EventActor Actor;
364 + std::int64_t time{};
365 +
366 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Event, Type, Action, Actor, time);
367 +};
368 +
369 } // namespace wsl::windows::common::wslc_schema
src/windows/service/inc/wslc.idl
+26
@@ -671,6 +671,18 @@ typedef struct _WSLCListContainersOptions
671 ULONG FiltersCount;
672 } WSLCListContainersOptions;
673
674 +[
675 + uuid(7EC66D3B-D098-4D48-B69E-69166F6C4745),
676 + pointer_default(unique),
677 + object
678 +]
679 +interface IWSLCEventStream : IUnknown
680 +{
681 + // Blocks until the next matching event, the until-time is reached, or the session terminates,
682 + // then returns the event as a JSON object following the wslc_schema::Event format.
683 + HRESULT GetNext([out, string] LPSTR* EventJson);
684 +}
685 +
686 // Settings for IWSLCSession::Initialize - passed from service to per-user process
687 typedef struct _WSLCSessionInitSettings
688 {
@@ -708,6 +720,18 @@ interface IWSLCSession : IUnknown
720 // termination event has been signaled; before that the call fails.
721 HRESULT GetTerminationReason([out] WSLCVirtualMachineTerminationReason* Reason, [out] LPWSTR* Details);
722
723 + // Opens an event stream that mirrors `docker events`. The window and filters are captured by the
724 + // returned stream object; events are then pulled one at a time via IWSLCEventStream::GetNext.
725 + // SinceTime / UntilTime bound the window by event time, in seconds since the Unix epoch. SinceTime
726 + // is inclusive and UntilTime is exclusive. 0 means unbounded on that end.
727 + // Filters are key/value pairs. Values sharing a key are OR'd, distinct keys are AND'd.
728 + HRESULT GetEvents(
729 + [in] LONGLONG SinceTime,
730 + [in] LONGLONG UntilTime,
731 + [in, unique, size_is(FiltersCount)] const WSLCFilter* Filters,
732 + [in] ULONG FiltersCount,
733 + [out] IWSLCEventStream** Stream);
734 +
735 // Image management.
736 HRESULT PullImage([in] LPCSTR Image, [in, unique] LPCSTR RegistryAuthenticationInformation, [in, unique] IProgressCallback* ProgressCallback, [in, unique] IWarningCallback* WarningCallback);
737 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
909 // N.B. WSLC_E_VM_NOT_RUNNING is part of the plugin API contract and is also defined in WslPluginApi.h.
910 // The two definitions must stay in sync.
911 cpp_quote("#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */")
912 +cpp_quote("#define WSLC_E_EVENTS_LOST MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */")
913 +cpp_quote("#define WSLC_E_EVENT_STREAM_FINISHED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 18) /* 0x80040612 */")
src/windows/wslcsession/CMakeLists.txt
+2
@@ -26,6 +26,7 @@ set(SOURCES
26 # Supporting classes
27 DockerEventTracker.cpp
28 DockerHTTPClient.cpp
29 + EventStore.cpp
30 IORelay.cpp
31 OptionParser.cpp
32 ServiceProcessLauncher.cpp
@@ -35,6 +36,7 @@ set(SOURCES
36 set(HEADERS
37 DockerEventTracker.h
38 DockerHTTPClient.h
39 + EventStore.h
40 IORelay.h
41 OptionParser.h
42 ServiceProcessLauncher.h
src/windows/wslcsession/DockerEventTracker.cpp
+43 -55
@@ -96,6 +96,7 @@ DockerEventTracker::~DockerEventTracker()
96 // N.B. No callback should be left when the tracker is destroyed.
97 WI_ASSERT(m_containerCallbacks.empty());
98 WI_ASSERT(m_volumeCallbacks.empty());
99 + WI_ASSERT(m_containerCreateCallbacks.empty());
100 }
101
102 void DockerEventTracker::OnEvent(const std::string_view& event)
@@ -125,33 +126,16 @@ void DockerEventTracker::OnEvent(const std::string_view& event)
126 if (typeStr == "container")
127 {
128 OnContainerEvent(parsed, actionStr, eventTime);
129 +
130 + if (actionStr == "create")
131 + {
132 + OnContainerCreated(parsed, eventTime);
133 + }
134 }
135 else if (typeStr == "volume")
136 {
137 OnVolumeEvent(parsed, actionStr, eventTime);
138 }
133 -
134 - // Track object creation for WaitForObjectCreated.
135 - auto actor = parsed.find("Actor");
136 - if (actor != parsed.end())
137 - {
138 - auto id = actor->find("ID");
139 - if (id != actor->end())
140 - {
141 - auto objectId = id->get<std::string>();
142 - if (actionStr == "create")
143 - {
144 - std::lock_guard lock{m_lock};
145 - m_createdObjects.insert(objectId);
146 - m_objectCreated.SetEvent();
147 - }
148 - else if (actionStr == "destroy")
149 - {
150 - std::lock_guard lock{m_lock};
151 - m_createdObjects.erase(objectId);
152 - }
153 - }
154 - }
139 }
140
141 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
143 static std::map<std::string, ContainerEvent> events{
144 {"start", ContainerEvent::Start},
145 {"die", ContainerEvent::Stop},
146 + {"kill", ContainerEvent::Kill},
147 {"destroy", ContainerEvent::Destroy},
148 {"exec_die", ContainerEvent::ExecDied},
149 {"restart", ContainerEvent::Restart}};
@@ -241,30 +226,23 @@ void DockerEventTracker::OnVolumeEvent(const nlohmann::json& parsed, const std::
226 InvokeCallbacks(callbacks, [&](const VolumeCallback& e) { e.Callback(volumeName, it->second, eventTime); });
227 }
228
244 -void DockerEventTracker::WaitForObjectCreated(const std::string& ObjectId)
229 +void DockerEventTracker::OnContainerCreated(const nlohmann::json& parsed, std::int64_t eventTime)
230 {
246 - constexpr auto c_timeout = std::chrono::seconds{60};
231 + auto actor = parsed.find("Actor");
232 + THROW_HR_IF_MSG(E_INVALIDARG, actor == parsed.end(), "Missing Actor in container event");
233
248 - while (true)
249 - {
250 - {
251 - std::lock_guard lock{m_lock};
252 - if (m_createdObjects.contains(ObjectId))
253 - {
254 - return;
255 - }
234 + auto id = actor->find("ID");
235 + THROW_HR_IF_MSG(E_INVALIDARG, id == actor->end(), "Missing Actor.ID in container event");
236
257 - // Reset under the lock so a concurrent OnEvent() that runs after we release the lock
258 - // and before the wait can re-signal the event and unblock us.
259 - m_objectCreated.ResetEvent();
260 - }
237 + auto containerId = id->get<std::string>();
238
262 - THROW_HR_IF_MSG(
263 - HRESULT_FROM_WIN32(ERROR_TIMEOUT),
264 - !m_session.WaitForEventOrSessionTerminating(m_objectCreated.get(), c_timeout),
265 - "Timed out waiting for Docker create event for object '%hs'",
266 - ObjectId.c_str());
239 + std::vector<std::shared_ptr<ContainerCreateCallbackEntry>> callbacks;
240 + {
241 + std::lock_guard lock{m_lock};
242 + callbacks = m_containerCreateCallbacks;
243 }
244 +
245 + InvokeCallbacks(callbacks, [&](const ContainerCreateCallbackEntry& e) { e.Callback(containerId, eventTime); });
246 }
247
248 DockerEventTracker::EventTrackingReference DockerEventTracker::RegisterContainerStateUpdates(
@@ -302,6 +280,17 @@ DockerEventTracker::EventTrackingReference DockerEventTracker::RegisterVolumeUpd
280 return EventTrackingReference{this, id};
281 }
282
283 +DockerEventTracker::EventTrackingReference DockerEventTracker::RegisterContainerCreate(ContainerCreateCallback&& Callback) noexcept
284 +{
285 + auto id = m_callbackId++;
286 + auto entry = std::make_shared<ContainerCreateCallbackEntry>(id, std::move(Callback));
287 +
288 + std::lock_guard lock{m_lock};
289 + m_containerCreateCallbacks.emplace_back(std::move(entry));
290 +
291 + return EventTrackingReference{this, id};
292 +}
293 +
294 void DockerEventTracker::UnregisterCallback(size_t Id) noexcept
295 {
296 std::shared_ptr<CallbackRegistration> registration;
@@ -311,22 +300,21 @@ void DockerEventTracker::UnregisterCallback(size_t Id) noexcept
300
301 auto matches = [Id](const auto& e) { return e->CallbackId == Id; };
302
314 - // Try container callbacks first, then volume callbacks.
315 - if (auto container = std::ranges::find_if(m_containerCallbacks, matches); container != m_containerCallbacks.end())
316 - {
317 - registration = std::move(*container);
318 - m_containerCallbacks.erase(container);
319 - }
320 - else
321 - {
322 - auto volume = std::ranges::find_if(m_volumeCallbacks, matches);
323 - WI_ASSERT(volume != m_volumeCallbacks.end());
324 -
325 - if (volume != m_volumeCallbacks.end())
303 + auto take = [&](auto& Callbacks) {
304 + auto entry = std::ranges::find_if(Callbacks, matches);
305 + if (entry == Callbacks.end())
306 {
327 - registration = std::move(*volume);
328 - m_volumeCallbacks.erase(volume);
307 + return false;
308 }
309 +
310 + registration = std::move(*entry);
311 + Callbacks.erase(entry);
312 + return true;
313 + };
314 +
315 + if (!take(m_containerCallbacks) && !take(m_volumeCallbacks) && !take(m_containerCreateCallbacks))
316 + {
317 + WI_ASSERT(false);
318 }
319 }
320
src/windows/wslcsession/DockerEventTracker.h
+19 -6
@@ -30,7 +30,8 @@ enum class ContainerEvent
30 Stop,
31 Exit,
32 Destroy,
33 - ExecDied
33 + ExecDied,
34 + Kill
35 };
36
37 enum class VolumeEvent
@@ -64,6 +65,7 @@ public:
65
66 using ContainerStateChangeCallback = std::function<void(ContainerEvent, std::optional<int>, std::int64_t)>;
67 using VolumeEventCallback = std::function<void(const std::string&, VolumeEvent, std::int64_t)>;
68 + using ContainerCreateCallback = std::function<void(const std::string& ContainerId, std::int64_t Time)>;
69
70 explicit DockerEventTracker(WSLCSession& session);
71 ~DockerEventTracker();
@@ -76,13 +78,16 @@ public:
78 EventTrackingReference RegisterContainerStateUpdates(const std::string& ContainerId, ContainerStateChangeCallback&& Callback) noexcept;
79 EventTrackingReference RegisterExecStateUpdates(const std::string& ContainerId, const std::string& ExecId, ContainerStateChangeCallback&& Callback) noexcept;
80 EventTrackingReference RegisterVolumeUpdates(VolumeEventCallback&& Callback) noexcept;
79 - void UnregisterCallback(size_t Id) noexcept;
81
81 - void WaitForObjectCreated(const std::string& ObjectId);
82 + // Invoked for every container create event, after the per-container state callbacks. Unlike those,
83 + // this isn't keyed by container id, because the id isn't known until Docker assigns it.
84 + EventTrackingReference RegisterContainerCreate(ContainerCreateCallback&& Callback) noexcept;
85 + void UnregisterCallback(size_t Id) noexcept;
86
87 private:
88 void OnEvent(const std::string_view& event);
89 void OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::int64_t eventTime);
90 + void OnContainerCreated(const nlohmann::json& parsed, std::int64_t eventTime);
91 void OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::int64_t eventTime);
92
93 // Callbacks are invoked without holding m_lock so that a callback can register or unregister callbacks, and so
@@ -125,8 +130,19 @@ private:
130 const VolumeEventCallback Callback;
131 };
132
133 + struct ContainerCreateCallbackEntry : CallbackRegistration
134 + {
135 + ContainerCreateCallbackEntry(size_t Id, ContainerCreateCallback&& Callback) :
136 + CallbackRegistration(Id), Callback(std::move(Callback))
137 + {
138 + }
139 +
140 + const ContainerCreateCallback Callback;
141 + };
142 +
143 _Guarded_by_(m_lock) std::vector<std::shared_ptr<ContainerCallback>> m_containerCallbacks;
144 _Guarded_by_(m_lock) std::vector<std::shared_ptr<VolumeCallback>> m_volumeCallbacks;
145 + _Guarded_by_(m_lock) std::vector<std::shared_ptr<ContainerCreateCallbackEntry>> m_containerCreateCallbacks;
146
147 // Invokes a snapshot of callbacks taken under m_lock, skipping registrations that have since been unregistered.
148 template <typename TCallback, typename TInvoke>
@@ -142,9 +158,6 @@ private:
158 }
159 }
160
145 - _Guarded_by_(m_lock) std::unordered_set<std::string> m_createdObjects;
146 - _Guarded_by_(m_lock) wil::unique_event m_objectCreated { wil::EventOptions::ManualReset };
147 -
161 WSLCSession& m_session;
162 std::mutex m_lock;
163 std::atomic<size_t> m_callbackId{0};
src/windows/wslcsession/EventStore.cpp new
+259
@@ -0,0 +1,259 @@
1 +// Copyright (C) Microsoft Corporation. All rights reserved.
2 +
3 +#include "precomp.h"
4 +#include "EventStore.h"
5 +#include "WSLCSession.h"
6 +#include "WSLCExecutionContext.h"
7 +#include <chrono>
8 +
9 +using wsl::shared::Localization;
10 +
11 +namespace wsl::windows::service::wslc {
12 +
13 +namespace {
14 +
15 + std::optional<std::chrono::sys_seconds> ToTimeBound(int64_t TimeSeconds)
16 + {
17 + if (TimeSeconds == 0)
18 + {
19 + return std::nullopt;
20 + }
21 +
22 + // Waiting on a bound converts it to the system clock's 100ns ticks, which a far-future second would overflow.
23 + constexpr auto c_maxBound = std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::time_point::max());
24 + return std::min(std::chrono::sys_seconds{std::chrono::seconds{TimeSeconds}}, c_maxBound);
25 + }
26 +
27 +} // namespace
28 +
29 +void EventStore::Append(wsl::windows::common::wslc_schema::Event Event)
30 +{
31 + std::lock_guard lock(m_lock);
32 +
33 + // Events are recorded in Docker's delivery order, which is also timestamp order. Subscribers rely on
34 + // this: they resume from a sequence number, so an out-of-order event could never be inserted where it
35 + // belongs without hiding it from readers that already moved past that point.
36 + WI_ASSERT(m_events.empty() || m_events.back().time <= Event.time);
37 +
38 + m_events.push_back(std::move(Event));
39 +
40 + if (m_events.size() > c_eventRingCapacity)
41 + {
42 + m_events.pop_front();
43 + ++m_firstSequenceNumber;
44 + }
45 +
46 + m_updated.notify_all();
47 +}
48 +
49 +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
50 +try
51 +{
52 + wsl::windows::common::wslc_schema::Event event;
53 + event.Type = std::move(Type);
54 + event.Action = std::move(Action);
55 + event.Actor.ID = ActorId;
56 + event.Actor.Attributes = std::move(ActorAttributes);
57 + event.time = Time;
58 +
59 + Append(std::move(event));
60 +}
61 +CATCH_LOG()
62 +
63 +namespace {
64 +
65 + // Values sharing a key are OR'd, distinct keys are AND'd. Unrecognized keys are ignored.
66 + bool EventMatchesFilters(const wsl::windows::common::wslc_schema::Event& event, const std::map<std::string, std::vector<std::string>>& filters)
67 + {
68 + for (const auto& [key, values] : filters)
69 + {
70 + if (key == "type")
71 + {
72 + if (!std::ranges::any_of(values, [&](const std::string& v) { return event.Type == v; }))
73 + {
74 + return false;
75 + }
76 + }
77 + else if (key == "event")
78 + {
79 + if (!std::ranges::any_of(values, [&](const std::string& v) { return event.Action == v; }))
80 + {
81 + return false;
82 + }
83 + }
84 + else if (key == "container")
85 + {
86 + if (event.Type != "container" ||
87 + !std::ranges::any_of(values, [&](const std::string& v) { return event.Actor.ID == v; }))
88 + {
89 + return false;
90 + }
91 + }
92 + else if (key == "image")
93 + {
94 + if (event.Type != "image" || !std::ranges::any_of(values, [&](const std::string& v) { return event.Actor.ID == v; }))
95 + {
96 + return false;
97 + }
98 + }
99 + }
100 + return true;
101 + }
102 +
103 +} // namespace
104 +
105 +Microsoft::WRL::ComPtr<IWSLCEventStream> EventStore::CreateStream(
106 + Microsoft::WRL::ComPtr<WSLCSession> Session, int64_t SinceTime, int64_t UntilTime, std::map<std::string, std::vector<std::string>> Filters)
107 +{
108 + // Zero means unbounded on that end, so it never makes the window run backwards.
109 + THROW_HR_WITH_USER_ERROR_IF(
110 + E_INVALIDARG,
111 + Localization::MessageWslcEventsInvalidTimeWindow(SinceTime, UntilTime),
112 + SinceTime < 0 || UntilTime < 0 || (SinceTime != 0 && UntilTime != 0 && SinceTime > UntilTime));
113 +
114 + Microsoft::WRL::ComPtr<EventStream> stream;
115 + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize<EventStream>(&stream, std::move(Session), this, SinceTime, UntilTime, std::move(Filters)));
116 +
117 + return stream;
118 +}
119 +
120 +std::optional<wsl::windows::common::wslc_schema::Event> EventStore::GetLockHeld(uint64_t SequenceNumber)
121 +{
122 + // Callers resync a lagging reader before reaching here, so the requested event is never evicted.
123 + WI_ASSERT(SequenceNumber >= m_firstSequenceNumber);
124 +
125 + const uint64_t index = SequenceNumber - m_firstSequenceNumber;
126 + if (index >= m_events.size())
127 + {
128 + return std::nullopt;
129 + }
130 +
131 + return m_events[index];
132 +}
133 +
134 +bool EventStore::WaitForEvent(std::unique_lock<std::mutex>& Lock, uint64_t SequenceNumber, std::optional<std::chrono::sys_seconds> Until)
135 +{
136 + // Ready once the reader's event is buffered, its slot is evicted, or the session terminates.
137 + // Eviction while parked wakes us too, so the caller reports the gap on its next pass.
138 + const auto ready = [&] { return m_terminating || SequenceNumber < m_firstSequenceNumber + m_events.size(); };
139 +
140 + if (Until.has_value())
141 + {
142 + if (!m_updated.wait_until(Lock, Until.value(), ready))
143 + {
144 + return false;
145 + }
146 + }
147 + else
148 + {
149 + m_updated.wait(Lock, ready);
150 + }
151 +
152 + THROW_HR_IF(E_ABORT, m_terminating);
153 + return true;
154 +}
155 +
156 +std::optional<wsl::windows::common::wslc_schema::Event> EventStore::Get(
157 + std::optional<uint64_t>& SequenceNumber,
158 + std::optional<std::chrono::sys_seconds> Since,
159 + std::optional<std::chrono::sys_seconds> Until,
160 + const std::map<std::string, std::vector<std::string>>& Filters)
161 +{
162 + std::unique_lock lock(m_lock);
163 +
164 + // Position the reader. A first read (no sequence number yet) starts at the oldest buffered
165 + // event
166 + SequenceNumber = SequenceNumber.value_or(m_firstSequenceNumber);
167 +
168 + while (true)
169 + {
170 + // A reader that has fallen behind the ring missed events to eviction: reset it so the
171 + // next call starts fresh at the oldest buffered event, and report the gap.
172 + if (SequenceNumber.value() < m_firstSequenceNumber)
173 + {
174 + SequenceNumber = std::nullopt;
175 + THROW_HR(WSLC_E_EVENTS_LOST);
176 + }
177 +
178 + if (!WaitForEvent(lock, SequenceNumber.value(), Until))
179 + {
180 + // The until window elapsed with no further event: the stream is finished.
181 + return std::nullopt;
182 + }
183 +
184 + // Evicted while parked: loop back to reset and report the gap.
185 + // TODO: A burst of more than c_eventRingCapacity events between the wake and reacquiring the
186 + // lock can evict this reader's event before it is read, forcing a WSLC_E_EVENTS_LOST. Redesign
187 + // so that every parked reader is guaranteed to observe an event before the next write can evict
188 + // it.
189 + if (SequenceNumber.value() < m_firstSequenceNumber)
190 + {
191 + continue;
192 + }
193 +
194 + const auto event = GetLockHeld(SequenceNumber.value()).value();
195 + const std::chrono::sys_seconds eventTime{std::chrono::seconds{event.time}};
196 +
197 + // Advance in delivery order before applying the time window.
198 + SequenceNumber.value()++;
199 +
200 + // Events are appended in non-decreasing timestamp order (see Append()), so once we reach the
201 + // exclusive Until bound, the stream is finished.
202 + if (Until.has_value() && eventTime >= Until.value())
203 + {
204 + return std::nullopt;
205 + }
206 +
207 + // Return the event if it falls within the since-bound and matches the caller's filters;
208 + // otherwise loop to skip it.
209 + if ((!Since.has_value() || eventTime >= Since.value()) && EventMatchesFilters(event, Filters))
210 + {
211 + return event;
212 + }
213 + }
214 +}
215 +
216 +void EventStore::OnSessionTerminating()
217 +{
218 + {
219 + std::lock_guard lock(m_lock);
220 + m_terminating = true;
221 + }
222 +
223 + m_updated.notify_all();
224 +}
225 +
226 +HRESULT EventStream::RuntimeClassInitialize(
227 + Microsoft::WRL::ComPtr<WSLCSession> Session,
228 + EventStore* Store,
229 + int64_t SinceTime,
230 + int64_t UntilTime,
231 + std::map<std::string, std::vector<std::string>> Filters)
232 +{
233 + m_session = std::move(Session);
234 + m_store = Store;
235 + m_since = ToTimeBound(SinceTime);
236 + m_until = ToTimeBound(UntilTime);
237 + m_filters = std::move(Filters);
238 + return S_OK;
239 +}
240 +
241 +HRESULT EventStream::GetNext(LPSTR* EventJson)
242 +try
243 +{
244 + RETURN_HR_IF_NULL(E_POINTER, EventJson);
245 + *EventJson = nullptr;
246 +
247 + std::lock_guard lock(m_lock);
248 + const auto event = m_store->Get(m_nextSequenceNumber, m_since, m_until, m_filters);
249 + if (!event.has_value())
250 + {
251 + return WSLC_E_EVENT_STREAM_FINISHED;
252 + }
253 +
254 + *EventJson = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(wsl::shared::ToJson(event.value()).c_str()).release();
255 + return S_OK;
256 +}
257 +CATCH_RETURN();
258 +
259 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/EventStore.h new
+88
@@ -0,0 +1,88 @@
1 +// Copyright (C) Microsoft Corporation. All rights reserved.
2 +
3 +#pragma once
4 +
5 +#include <chrono>
6 +#include <condition_variable>
7 +#include <cstdint>
8 +#include <deque>
9 +#include <map>
10 +#include <mutex>
11 +#include <optional>
12 +#include <string>
13 +#include <vector>
14 +#include "wslc.h"
15 +#include "wslc_schema.h"
16 +
17 +namespace wsl::windows::service::wslc {
18 +
19 +class WSLCSession;
20 +
21 +class EventStore
22 +{
23 +public:
24 + static constexpr size_t c_eventRingCapacity = 256;
25 +
26 + void Record(std::string&& Type, std::string&& Action, const std::string& ActorId, std::map<std::string, std::string> ActorAttributes, std::int64_t Time) noexcept;
27 +
28 + Microsoft::WRL::ComPtr<IWSLCEventStream> CreateStream(
29 + Microsoft::WRL::ComPtr<WSLCSession> Session, int64_t SinceTime, int64_t UntilTime, std::map<std::string, std::vector<std::string>> Filters);
30 +
31 + // Returns the next event at or after SequenceNumber that falls within [Since, Until) and matches
32 + // Filters, advancing SequenceNumber past it. A nullopt SequenceNumber starts a fresh reader at the
33 + // oldest buffered event (no gap is reported). If the reader has since fallen behind the ring,
34 + // resyncs SequenceNumber to the oldest buffered event and throws WSLC_E_EVENTS_LOST. Returns
35 + // nullopt once the Until window has closed.
36 + std::optional<wsl::windows::common::wslc_schema::Event> Get(
37 + std::optional<uint64_t>& SequenceNumber,
38 + std::optional<std::chrono::sys_seconds> Since,
39 + std::optional<std::chrono::sys_seconds> Until,
40 + const std::map<std::string, std::vector<std::string>>& Filters);
41 +
42 + void OnSessionTerminating();
43 +
44 +private:
45 + void Append(wsl::windows::common::wslc_schema::Event Event);
46 +
47 + // Blocks until the event at SequenceNumber is buffered, its slot is evicted, or the session
48 + // terminates. Returns false only when Until elapsed with no event ready. Throws E_ABORT if the
49 + // session terminated while waiting.
50 + bool WaitForEvent(std::unique_lock<std::mutex>& Lock, uint64_t SequenceNumber, std::optional<std::chrono::sys_seconds> Until);
51 +
52 + std::optional<wsl::windows::common::wslc_schema::Event> GetLockHeld(uint64_t SequenceNumber);
53 +
54 + std::mutex m_lock;
55 + std::condition_variable m_updated;
56 +
57 + _Guarded_by_(m_lock) std::deque<wsl::windows::common::wslc_schema::Event> m_events;
58 + _Guarded_by_(m_lock) uint64_t m_firstSequenceNumber = 1;
59 +
60 + _Guarded_by_(m_lock) bool m_terminating = false;
61 +};
62 +
63 +class EventStream
64 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IWSLCEventStream, IFastRundown>
65 +{
66 +public:
67 + HRESULT RuntimeClassInitialize(
68 + Microsoft::WRL::ComPtr<WSLCSession> Session,
69 + EventStore* Store,
70 + int64_t SinceTime,
71 + int64_t UntilTime,
72 + std::map<std::string, std::vector<std::string>> Filters);
73 +
74 + IFACEMETHOD(GetNext)(_Outptr_result_z_ LPSTR* EventJson) override;
75 +
76 +private:
77 + Microsoft::WRL::ComPtr<WSLCSession> m_session;
78 + EventStore* m_store = nullptr;
79 +
80 + std::optional<std::chrono::sys_seconds> m_since;
81 + std::optional<std::chrono::sys_seconds> m_until;
82 + std::map<std::string, std::vector<std::string>> m_filters;
83 +
84 + std::mutex m_lock;
85 + _Guarded_by_(m_lock) std::optional<uint64_t> m_nextSequenceNumber;
86 +};
87 +
88 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCContainer.cpp
+53 -13
@@ -474,6 +474,21 @@ WSLCContainerState DockerStateToWSLCState(ContainerState state)
474 }
475 }
476
477 +std::string WSLCStateToEventAction(WSLCContainerState state)
478 +{
479 + switch (state)
480 + {
481 + case WslcContainerStateRunning:
482 + return "start";
483 + case WslcContainerStateExited:
484 + return "stop";
485 + case WslcContainerStateDeleted:
486 + return "destroy";
487 + default:
488 + WI_ASSERT(false);
489 + return "unknown";
490 + }
491 +}
492 std::string CleanContainerName(const std::string& name)
493 {
494 // Docker container names have a leading '/', strip it.
@@ -814,6 +829,7 @@ WSLCContainerImpl::WSLCContainerImpl(
829 std::vector<ContainerPortMapping>&& ports,
830 std::map<std::string, std::string>&& labels,
831 std::function<void(const WSLCContainerImpl*)>&& onDeleted,
832 + EventStore& eventStore,
833 WSLCContainerState InitialState,
834 std::int64_t CreatedAt,
835 WSLCProcessFlags InitProcessFlags,
@@ -832,6 +848,7 @@ WSLCContainerImpl::WSLCContainerImpl(
848 m_comWrapper(wil::MakeOrThrow<WSLCContainer>(wslcSession, std::move(onDeleted))),
849 m_containerEvents(runtime.Events().RegisterContainerStateUpdates(
850 m_id, std::bind(&WSLCContainerImpl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))),
851 + m_eventStore(eventStore),
852 m_state(InitialState),
853 m_createdAt(CreatedAt),
854 m_initProcessFlags(InitProcessFlags),
@@ -1224,12 +1241,34 @@ __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::CompleteTransitio
1241 transition->Completed.SetEvent();
1242 }
1243
1244 +void WSLCContainerImpl::RecordEvent(std::string&& Action, std::int64_t Time, std::optional<int> ExitCode) noexcept
1245 +try
1246 +{
1247 + auto attributes = StripInternalLabels(m_labels);
1248 + attributes["name"] = m_name;
1249 + attributes["image"] = m_image;
1250 +
1251 + if (ExitCode.has_value())
1252 + {
1253 + attributes["exitCode"] = std::to_string(ExitCode.value());
1254 + }
1255 +
1256 + m_eventStore.Record("container", std::move(Action), m_id, std::move(attributes), Time);
1257 +}
1258 +CATCH_LOG()
1259 +
1260 void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional<int> exitCode, std::int64_t eventTime) noexcept
1261 {
1262 // Either owner may disconnect the COM wrapper, so both must outlive m_lock.
1263 unique_com_disconnect comWrapper;
1264 std::shared_ptr<StateTransition> transition;
1265
1266 + if (event == ContainerEvent::Kill)
1267 + {
1268 + RecordEvent("kill", eventTime);
1269 + return;
1270 + }
1271 +
1272 {
1273 auto lifecycleLock = m_lifecycleLock.lock_exclusive();
1274 auto lock = m_lock.lock_exclusive();
@@ -1396,7 +1435,7 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill)
1435 }
1436 }
1437
1399 -__requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exitCode, std::optional<std::int64_t> stopTimestamp)
1438 +__requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exitCode, std::int64_t stopTime)
1439 {
1440 auto transition = m_transition;
1441
@@ -1427,7 +1466,7 @@ __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exi
1466 // Ignore duplicate or late Stop events so they do not overwrite an already committed state.
1467 if (m_state == WslcContainerStateRunning)
1468 {
1430 - CommitState(WslcContainerStateExited, stopTimestamp);
1469 + CommitState(WslcContainerStateExited, stopTime, exitCode);
1470 }
1471
1472 std::exception_ptr transitionException;
@@ -2048,11 +2087,11 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2087 WSLCSessionRuntime& runtime,
2088 IWSLCPluginNotifier* pluginNotifier,
2089 const std::unordered_map<std::string, NetworkEntry>& sessionNetworks,
2051 - std::function<void(const WSLCContainerImpl*)>&& OnDeleted)
2090 + std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
2091 + EventStore& eventStore)
2092 {
2093 auto& virtualMachine = runtime.Vm();
2094 auto& DockerClient = runtime.Docker();
2055 - auto& EventTracker = runtime.Events();
2095 const auto mounts = ConvertAndValidateMounts(containerOptions);
2096
2097 common::docker_schema::CreateContainer request;
@@ -2486,11 +2525,6 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2525 name.c_str());
2526 }
2527
2489 - // Wait for the container create event to be delivered on the Docker event stream so that
2490 - // any events for objects created for the container (e.g. volumes) are delivered before we return
2491 - // from this function.
2492 - EventTracker.WaitForObjectCreated(result.Id);
2493 -
2528 // Collect the names of referenced docker named volumes so Start() can verify
2529 // they are available before running the container.
2530 std::vector<std::string> namedVolumes;
@@ -2509,6 +2543,7 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2543 }
2544
2545 auto mergedLabels = StripInternalLabels(std::move(inspectData.Config.Labels));
2546 + const auto createdAt = wsl::windows::common::timestamp::Rfc3339ToEpoch(inspectData.Created);
2547
2548 auto container = std::make_shared<WSLCContainerImpl>(
2549 wslcSession,
@@ -2523,8 +2558,9 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2558 std::move(mappedPorts),
2559 std::move(mergedLabels),
2560 std::move(OnDeleted),
2561 + eventStore,
2562 WslcContainerStateCreated,
2527 - wsl::windows::common::timestamp::Rfc3339ToEpoch(inspectData.Created),
2563 + createdAt,
2564 containerOptions.InitProcessOptions.Flags,
2565 containerOptions.Flags);
2566
@@ -2539,7 +2575,8 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
2575 WSLCSession& wslcSession,
2576 WSLCSessionRuntime& runtime,
2577 IWSLCPluginNotifier* pluginNotifier,
2542 - std::function<void(const WSLCContainerImpl*)>&& OnDeleted)
2578 + std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
2579 + EventStore& eventStore)
2580 {
2581 auto& virtualMachine = runtime.Vm();
2582 auto& DockerClient = runtime.Docker();
@@ -2611,6 +2648,7 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
2648 std::move(ports),
2649 std::move(labels),
2650 std::move(OnDeleted),
2651 + eventStore,
2652 DockerStateToWSLCState(dockerContainer.State),
2653 dockerContainer.Created,
2654 metadata.InitProcessFlags,
@@ -2917,7 +2955,7 @@ __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::
2955 return unique_com_disconnect{std::exchange(m_comWrapper, nullptr)};
2956 }
2957
2920 -__requires_lock_held(m_lock) void WSLCContainerImpl::CommitState(WSLCContainerState State, std::optional<std::int64_t> stateChangedAt) noexcept
2958 +__requires_lock_held(m_lock) void WSLCContainerImpl::CommitState(WSLCContainerState State, std::int64_t Time, std::optional<int> ExitCode) noexcept
2959 {
2960 // N.B. A deleted container cannot transition back to any other state.
2961 WI_ASSERT(m_state != WslcContainerStateDeleted);
@@ -2930,7 +2968,9 @@ __requires_lock_held(m_lock) void WSLCContainerImpl::CommitState(WSLCContainerSt
2968
2969 m_state = State;
2970 m_stateGeneration++;
2933 - m_stateChangedAt = stateChangedAt.value_or(static_cast<std::int64_t>(std::time(nullptr)));
2971 + m_stateChangedAt = Time;
2972 +
2973 + RecordEvent(WSLCStateToEventAction(State), Time, ExitCode);
2974
2975 // Keep the VM alive while this container is Running and release the hold once it leaves that
2976 // state, even when no client holds the wrapper (e.g. a detached `run -d` container). Dropping
src/windows/wslcsession/WSLCContainer.h
+13 -4
@@ -35,6 +35,7 @@ class WSLCContainer;
35 class WSLCSession;
36 class WSLCSessionRuntime;
37 class WSLCVolumes;
38 +class EventStore;
39
40 class unique_com_disconnect
41 {
@@ -85,6 +86,7 @@ public:
86 std::vector<ContainerPortMapping>&& ports,
87 std::map<std::string, std::string>&& labels,
88 std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
89 + EventStore& eventStore,
90 WSLCContainerState InitialState,
91 std::int64_t CreatedAt,
92 WSLCProcessFlags InitProcessFlags,
@@ -123,7 +125,7 @@ public:
125 // Re-registers a stopped container's VM-scoped port allocations against the restarted VM.
126 void RecoverPorts(const common::docker_schema::ContainerInfo& dockerContainer);
127
126 - __requires_lock_held(m_lock) void CommitState(WSLCContainerState State, std::optional<std::int64_t> stateChangedAt = std::nullopt) noexcept;
128 + __requires_lock_held(m_lock) void CommitState(WSLCContainerState State, std::int64_t Time, std::optional<int> ExitCode = std::nullopt) noexcept;
129
130 const std::string& ID() const noexcept;
131
@@ -141,14 +143,20 @@ public:
143 WSLCSessionRuntime& runtime,
144 IWSLCPluginNotifier* pluginNotifier,
145 const std::unordered_map<std::string, NetworkEntry>& SessionNetworks,
144 - std::function<void(const WSLCContainerImpl*)>&& OnDeleted);
146 + std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
147 + EventStore& eventStore);
148
149 static std::shared_ptr<WSLCContainerImpl> Open(
150 const common::docker_schema::ContainerInfo& DockerContainer,
151 WSLCSession& wslcSession,
152 WSLCSessionRuntime& runtime,
153 IWSLCPluginNotifier* pluginNotifier,
151 - std::function<void(const WSLCContainerImpl*)>&& OnDeleted);
154 + std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
155 + EventStore& eventStore);
156 +
157 + // Appends an event for this container to the session's event stream. Must be called from the Docker
158 + // event stream thread so that recorded events keep Docker's delivery order.
159 + void RecordEvent(std::string&& Action, std::int64_t Time, std::optional<int> ExitCode = std::nullopt) noexcept;
160
161 private:
162 enum class TransitionKind
@@ -197,7 +205,7 @@ private:
205 __requires_exclusive_lock_held(m_lock) void ReleaseProcesses();
206 __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect PrepareDisconnectComWrapper();
207
200 - __requires_exclusive_lock_held(m_lock) void OnStopped(int exitCode, std::optional<std::int64_t> stopTimestamp);
208 + __requires_exclusive_lock_held(m_lock) void OnStopped(int exitCode, std::int64_t stopTime);
209
210 void SetExitCode(int ExitCode) noexcept;
211 void SignalInitProcessExit() noexcept;
@@ -252,6 +260,7 @@ private:
260 std::map<std::string, std::string> m_labels;
261 Microsoft::WRL::ComPtr<WSLCContainer> m_comWrapper;
262 DockerEventTracker::EventTrackingReference m_containerEvents;
263 + EventStore& m_eventStore;
264 std::string m_networkMode;
265
266 // Held (non-empty) exactly while the container is Running so the session's VM stays alive even
src/windows/wslcsession/WSLCProcessControl.cpp
+1 -1
@@ -172,7 +172,7 @@ void DockerExecProcessControl::SetExitCode(int ExitCode)
172 }
173 }
174
175 -void DockerExecProcessControl::OnEvent(ContainerEvent Event, std::optional<int> ExitCode, std::int64_t /*eventTime*/)
175 +void DockerExecProcessControl::OnEvent(ContainerEvent Event, std::optional<int> ExitCode, std::int64_t)
176 {
177 if (Event == ContainerEvent::ExecDied && !m_exitEvent.is_signaled())
178 {
src/windows/wslcsession/WSLCSession.cpp
+135 -7
@@ -41,6 +41,7 @@ using wsl::windows::service::wslc::WSLCVirtualMachine;
41 constexpr auto c_containerdSocket = "/run/containerd/containerd.sock";
42 constexpr auto c_storageVhdFilename = wsl::windows::wslc::DefaultStorageVhdName;
43 constexpr uint32_t c_progressPrecision = 4;
44 +constexpr auto c_containerCreateEventTimeout = std::chrono::seconds{60};
45
46 // Default grace period to keep an otherwise-idle VM running before tearing it down (used when the
47 // session's IdleTimeoutSec setting is 0/unset). This avoids thrashing the VM (repeated
@@ -499,6 +500,9 @@ try
500
501 m_runtime.Initialize(m_vmFactoryGitCookie, m_git, &m_settings, idleGracePeriod, std::move(sessionContext), std::move(hooks));
502
503 + m_containerEventTracking = m_runtime.Events().RegisterContainerCreate(
504 + std::bind(&WSLCSession::OnContainerCreated, this, std::placeholders::_1, std::placeholders::_2));
505 +
506 return S_OK;
507 }
508 CATCH_RETURN()
@@ -2312,7 +2316,9 @@ void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptio
2316
2317 try
2318 {
2315 - std::scoped_lock lock(m_containersLock, m_networksLock);
2319 + std::unique_lock containersLock{m_containersLock};
2320 + WaitForConflictingCreateToComplete(containersLock);
2321 + std::unique_lock networksLock{m_networksLock};
2322
2323 // Generate a unique container name if the user didn't provide one.
2324 std::string containerName;
@@ -2350,13 +2356,24 @@ void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptio
2356 m_runtime,
2357 m_pluginNotifier.get(),
2358 m_networks,
2353 - std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1));
2359 + std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1),
2360 + m_eventStore);
2361
2355 - // Key the map by Docker's container ID, which is set in the WSLCContainerImpl constructor and stable for its lifetime.
2356 - auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container));
2357 - WI_ASSERT(inserted);
2362 + auto pendingCreate = StartPendingCreate(container);
2363 +
2364 + containersLock.unlock();
2365 + networksLock.unlock();
2366
2359 - it->second->CopyTo(Container);
2367 + // m_pendingCreate is published under m_containersLock before the event thread can observe it, so
2368 + // OnContainerCreated() is guaranteed to complete this create unless the session tears down first.
2369 + WaitForPendingCreateCompletion(pendingCreate);
2370 +
2371 + if (pendingCreate->Exception)
2372 + {
2373 + std::rethrow_exception(pendingCreate->Exception);
2374 + }
2375 +
2376 + container->CopyTo(Container);
2377 }
2378 catch (const DockerHTTPException& e)
2379 {
@@ -2372,6 +2389,97 @@ void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptio
2389 }
2390 }
2391
2392 +__requires_lock_held(m_containersLock) std::shared_ptr<WSLCSession::PendingContainerCreate> WSLCSession::StartPendingCreate(std::shared_ptr<WSLCContainerImpl> Container)
2393 +{
2394 + WI_ASSERT(!m_pendingCreate);
2395 +
2396 + m_pendingCreate = std::make_shared<PendingContainerCreate>();
2397 + m_pendingCreate->Container = std::move(Container);
2398 +
2399 + return m_pendingCreate;
2400 +}
2401 +
2402 +void WSLCSession::WaitForPendingCreateCompletion(const std::shared_ptr<PendingContainerCreate>& PendingCreate)
2403 +{
2404 + auto io = CreateIOContext();
2405 + io.AddHandle(std::make_unique<io::EventHandle>(PendingCreate->Completed.get()));
2406 +
2407 + try
2408 + {
2409 + io.Run(c_containerCreateEventTimeout);
2410 + }
2411 + catch (...)
2412 + {
2413 + if (wil::ResultFromCaughtException() != HRESULT_FROM_WIN32(ERROR_TIMEOUT))
2414 + {
2415 + throw;
2416 + }
2417 +
2418 + // Fail this create rather than leaving m_pendingCreate set, which would wedge every later one.
2419 + // Any container docker did manage to create is left behind; the same broken event stream makes
2420 + // deleting it unreliable, and a late create event is ignored once m_pendingCreate is cleared.
2421 + std::lock_guard containersLock{m_containersLock};
2422 + if (m_pendingCreate == PendingCreate)
2423 + {
2424 + CompletePendingCreate(PendingCreate, std::current_exception());
2425 + }
2426 + }
2427 +
2428 + WI_ASSERT(PendingCreate->Completed.is_signaled());
2429 +}
2430 +
2431 +__requires_lock_held(m_containersLock) void WSLCSession::CompletePendingCreate(
2432 + const std::shared_ptr<PendingContainerCreate>& PendingCreate, std::exception_ptr Exception) noexcept
2433 +{
2434 + WI_ASSERT(m_pendingCreate == PendingCreate);
2435 + PendingCreate->Exception = std::move(Exception);
2436 + m_pendingCreate.reset();
2437 + PendingCreate->Completed.SetEvent();
2438 +}
2439 +
2440 +void WSLCSession::WaitForConflictingCreateToComplete(std::unique_lock<std::mutex>& ContainersLock)
2441 +{
2442 + while (m_pendingCreate)
2443 + {
2444 + auto pendingCreate = m_pendingCreate;
2445 + ContainersLock.unlock();
2446 +
2447 + WaitForPendingCreateCompletion(pendingCreate);
2448 +
2449 + ContainersLock.lock();
2450 + }
2451 +}
2452 +
2453 +void WSLCSession::OnContainerCreated(const std::string& ContainerId, std::int64_t Time) noexcept
2454 +try
2455 +{
2456 + std::lock_guard containersLock{m_containersLock};
2457 +
2458 + // Containers created behind our back (BuildKit, for instance) have no pending create to match.
2459 + if (!m_pendingCreate || m_pendingCreate->Container->ID() != ContainerId)
2460 + {
2461 + return;
2462 + }
2463 +
2464 + auto pendingCreate = m_pendingCreate;
2465 + std::exception_ptr exception;
2466 +
2467 + try
2468 + {
2469 + // Key the map by Docker's container ID, which is set in the WSLCContainerImpl constructor and stable for its lifetime.
2470 + WI_VERIFY(m_containers.emplace(ContainerId, pendingCreate->Container).second);
2471 + pendingCreate->Container->RecordEvent("create", Time);
2472 + }
2473 + catch (...)
2474 + {
2475 + // Hand the failure to the waiting create rather than letting it return a container the session isn't tracking.
2476 + exception = std::current_exception();
2477 + }
2478 +
2479 + CompletePendingCreate(pendingCreate, std::move(exception));
2480 +}
2481 +CATCH_LOG()
2482 +
2483 HRESULT WSLCSession::OpenContainer(LPCSTR Id, IWSLCContainer** Container)
2484 try
2485 {
@@ -3342,6 +3450,9 @@ try
3450 if (!m_sessionTerminatingEvent.is_signaled())
3451 {
3452 m_sessionTerminatingEvent.SetEvent();
3453 +
3454 + // Wake any readers parked in an event stream so they abort instead of waiting forever.
3455 + m_eventStore.OnSessionTerminating();
3456 }
3457
3458 // Cancel any pending IO on user-provided handles to unblock operations
@@ -3907,6 +4018,23 @@ try
4018 }
4019 CATCH_RETURN();
4020
4021 +HRESULT WSLCSession::GetEvents(LONGLONG SinceTime, LONGLONG UntilTime, const WSLCFilter* Filters, ULONG FiltersCount, IWSLCEventStream** Stream)
4022 +try
4023 +{
4024 + WSLCExecutionContext context(this);
4025 +
4026 + RETURN_HR_IF_NULL(E_POINTER, Stream);
4027 +
4028 + *Stream = nullptr;
4029 +
4030 + auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
4031 + auto stream = m_eventStore.CreateStream(Microsoft::WRL::ComPtr<WSLCSession>{this}, SinceTime, UntilTime, std::move(filters));
4032 +
4033 + *Stream = stream.Detach();
4034 + return S_OK;
4035 +}
4036 +CATCH_RETURN();
4037 +
4038 void WSLCSession::RecoverExistingContainers()
4039 {
4040 WI_ASSERT(m_runtime.HasDocker());
@@ -3940,7 +4068,7 @@ void WSLCSession::RecoverExistingContainers()
4068 try
4069 {
4070 auto container = WSLCContainerImpl::Open(
3943 - dockerContainer, *this, m_runtime, m_pluginNotifier.get(), std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1));
4071 + dockerContainer, *this, m_runtime, m_pluginNotifier.get(), std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), m_eventStore);
4072
4073 auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container));
4074 WI_ASSERT(inserted);
src/windows/wslcsession/WSLCSession.h
+42 -5
@@ -24,6 +24,7 @@ Abstract:
24 #include "WSLCNetworkMetadata.h"
25 #include "DockerEventTracker.h"
26 #include "DockerHTTPClient.h"
27 +#include "EventStore.h"
28 #include "IORelay.h"
29 #include <atomic>
30 #include <list>
@@ -114,6 +115,15 @@ public:
115 IFACEMETHOD(GetTerminationEvent)(_Out_ HANDLE* Event) override;
116 IFACEMETHOD(GetTerminationReason)(_Out_ WSLCVirtualMachineTerminationReason* Reason, _Out_ LPWSTR* Details) override;
117
118 + // Event streaming. Opens a stream object that yields matching events one at a time via
119 + // IWSLCEventStream::GetNext.
120 + IFACEMETHOD(GetEvents)(
121 + _In_ LONGLONG SinceTime,
122 + _In_ LONGLONG UntilTime,
123 + _In_reads_opt_(FiltersCount) const WSLCFilter* Filters,
124 + _In_ ULONG FiltersCount,
125 + _Outptr_ IWSLCEventStream** Stream) override;
126 +
127 // Image management.
128 IFACEMETHOD(PullImage)(
129 _In_ LPCSTR Image,
@@ -262,11 +272,6 @@ public:
272 UserCOMCallback RegisterUserCOMCallback();
273 void UnregisterUserCOMCallback(DWORD ThreadId);
274
265 - HANDLE SessionTerminatingEvent() const noexcept
266 - {
267 - return m_sessionTerminatingEvent.get();
268 - }
269 -
275 ULONG Id() const noexcept
276 {
277 return m_id;
@@ -319,6 +324,30 @@ private:
324
325 void CreateContainerImpl(const WSLCContainerOptions* Options, IWSLCContainer** Container);
326
327 + // A create RPC hands the container off to the Docker event stream thread here and waits, so the
328 + // container is committed and its create event recorded from that thread. Recording it from the RPC
329 + // thread instead would let another container's event be recorded first and regress the event
330 + // stream's timestamps.
331 + struct PendingContainerCreate
332 + {
333 + wil::unique_event Completed{wil::EventOptions::ManualReset};
334 + std::shared_ptr<WSLCContainerImpl> Container;
335 + std::exception_ptr Exception;
336 + };
337 +
338 + __requires_lock_held(m_containersLock) std::shared_ptr<PendingContainerCreate> StartPendingCreate(std::shared_ptr<WSLCContainerImpl> Container);
339 +
340 + __requires_lock_held(m_containersLock) void CompletePendingCreate(
341 + const std::shared_ptr<PendingContainerCreate>& PendingCreate, std::exception_ptr Exception) noexcept;
342 +
343 + void WaitForPendingCreateCompletion(const std::shared_ptr<PendingContainerCreate>& PendingCreate);
344 +
345 + // Returns with the lock held once no create is in flight. Only one fits: the slot is unkeyed,
346 + // because the container ID isn't known until Docker assigns it.
347 + void WaitForConflictingCreateToComplete(std::unique_lock<std::mutex>& ContainersLock);
348 +
349 + void OnContainerCreated(const std::string& ContainerId, std::int64_t Time) noexcept;
350 +
351 void ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID UserSid);
352
353 void Ext4Format(const std::string& Device);
@@ -388,6 +417,14 @@ private:
417
418 wil::com_ptr<IWSLCPluginNotifier> m_pluginNotifier;
419
420 + // Bounded in-memory ring of recent lifecycle events, shared by all event-stream subscribers.
421 + EventStore m_eventStore;
422 +
423 + __guarded_by(m_containersLock) std::shared_ptr<PendingContainerCreate> m_pendingCreate;
424 +
425 + // N.B. Declared after everything OnContainerCreated() touches so the callback is unregistered first.
426 + DockerEventTracker::EventTrackingReference m_containerEventTracking;
427 +
428 // User-provided handles that the session is currently doing IO on.
429 std::mutex m_userHandlesLock;
430 __guarded_by(m_userHandlesLock) std::vector<HANDLE> m_userHandles;
test/windows/WSLCTests.cpp
+289
@@ -27,6 +27,7 @@ Abstract:
27 #include "WSLCSessionDefaults.h"
28 #include <nlohmann/json.hpp>
29
30 +using namespace std::chrono;
31 using namespace std::literals::chrono_literals;
32 using namespace wsl::windows::common::registry;
33 using wsl::windows::common::ClientRunningWSLCProcess;
@@ -6857,6 +6858,294 @@ class WSLCTests
6858 }
6859 }
6860
6861 + WSLC_TEST_METHOD(EventStream)
6862 + {
6863 + constexpr auto c_containerName = "wslc-test-events";
6864 + constexpr auto c_imageName = "debian:latest";
6865 + constexpr auto c_labelKey = "event-label";
6866 + constexpr auto c_labelValue = "event-value";
6867 + const auto expectedExitCode = std::to_string(128 + WSLCSignalSIGKILL);
6868 +
6869 + auto now = [] { return duration_cast<seconds>(system_clock::now().time_since_epoch()).count(); };
6870 +
6871 + // Drains a bounded event stream to completion (GetNext returns WSLC_E_EVENT_STREAM_FINISHED
6872 + // once the until-time has passed and the backlog is exhausted), parsing each event's JSON.
6873 + auto drain = [](IWSLCEventStream* stream) {
6874 + std::vector<wsl::windows::common::wslc_schema::Event> events;
6875 +
6876 + wil::unique_cotaskmem_ansistring eventJson;
6877 + HRESULT result;
6878 + while (SUCCEEDED(result = stream->GetNext(&eventJson)))
6879 + {
6880 + events.push_back(wsl::shared::FromJson<wsl::windows::common::wslc_schema::Event>(eventJson.get()));
6881 + }
6882 +
6883 + VERIFY_ARE_EQUAL(WSLC_E_EVENT_STREAM_FINISHED, result);
6884 + return events;
6885 + };
6886 +
6887 + // Verifies the given events match the expected actions in order for a given actor.
6888 + auto verifyEvents = [&](const std::vector<wsl::windows::common::wslc_schema::Event>& events,
6889 + const std::string& actorId,
6890 + const std::vector<std::string>& expectedActions) {
6891 + VERIFY_ARE_EQUAL(events.size(), expectedActions.size());
6892 +
6893 + for (size_t i = 0; i < expectedActions.size(); ++i)
6894 + {
6895 + const auto& action = expectedActions[i];
6896 + const auto& event = events[i];
6897 +
6898 + VERIFY_ARE_EQUAL(action, event.Action);
6899 + VERIFY_ARE_EQUAL(actorId, event.Actor.ID);
6900 + VERIFY_ARE_EQUAL(c_containerName, event.Actor.Attributes.at("name"));
6901 + VERIFY_ARE_EQUAL(c_imageName, event.Actor.Attributes.at("image"));
6902 + VERIFY_ARE_EQUAL(c_labelValue, event.Actor.Attributes.at(c_labelKey));
6903 + VERIFY_IS_FALSE(event.Actor.Attributes.contains("com.microsoft.wsl.container.metadata"));
6904 +
6905 + if (action == "stop")
6906 + {
6907 + VERIFY_ARE_EQUAL(expectedExitCode, event.Actor.Attributes.at("exitCode"));
6908 + }
6909 + else
6910 + {
6911 + VERIFY_IS_FALSE(event.Actor.Attributes.contains("exitCode"));
6912 + }
6913 + }
6914 + };
6915 +
6916 + // Run a container through its create/start/kill/stop lifecycle inside a bounded time window.
6917 + const LONGLONG since = now();
6918 + std::string id;
6919 + {
6920 + WSLCContainerLauncher launcher(c_imageName, c_containerName, {"sleep", "99999"});
6921 + launcher.AddLabel(c_labelKey, c_labelValue);
6922 + auto container = launcher.Launch(*m_defaultSession);
6923 + id = container.Id();
6924 +
6925 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
6926 +
6927 + // Kill (rather than Stop) so Docker emits a 'kill' event ahead of the 'die' that stops it.
6928 + VERIFY_SUCCEEDED(container.Get().Kill(WSLCSignalSIGKILL));
6929 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
6930 + }
6931 +
6932 + const LONGLONG until = now() + 1;
6933 + std::vector<wsl::windows::common::wslc_schema::Event> lifecycleEvents;
6934 +
6935 + // The container's create, start, kill, stop, then destroy events are reported in order, each carrying
6936 + // the container's 64-hex id as the actor.
6937 + {
6938 + WSLCFilter filter{"container", id.c_str()};
6939 + wil::com_ptr<IWSLCEventStream> stream;
6940 + VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since, until, &filter, 1, &stream));
6941 +
6942 + lifecycleEvents = drain(stream.get());
6943 + verifyEvents(lifecycleEvents, id, {"create", "start", "kill", "stop", "destroy"});
6944 +
6945 + // The whole lifecycle falls inside the requested window.
6946 + VERIFY_IS_TRUE(lifecycleEvents[0].time >= since);
6947 + VERIFY_IS_TRUE(lifecycleEvents[4].time < until);
6948 + }
6949 +
6950 + // Each lifecycle action is independently selectable: an 'event=<action>' filter, AND'd with
6951 + // the container filter, returns exactly that one event out of the five recorded above.
6952 + auto verifyEventFilter = [&](const char* action) {
6953 + WSLCFilter filters[]{{"container", id.c_str()}, {"event", action}};
6954 + wil::com_ptr<IWSLCEventStream> stream;
6955 + VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since, until, filters, ARRAYSIZE(filters), &stream));
6956 +
6957 + verifyEvents(drain(stream.get()), id, {action});
6958 + };
6959 +
6960 + verifyEventFilter("create");
6961 + verifyEventFilter("start");
6962 + verifyEventFilter("kill");
6963 + verifyEventFilter("stop");
6964 + verifyEventFilter("destroy");
6965 +
6966 + // Values sharing a filter key are OR'd.
6967 + {
6968 + WSLCFilter filters[]{{"container", id.c_str()}, {"event", "create"}, {"event", "destroy"}};
6969 + wil::com_ptr<IWSLCEventStream> stream;
6970 + VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since, until, filters, ARRAYSIZE(filters), &stream));
6971 +
6972 + verifyEvents(drain(stream.get()), id, {"create", "destroy"});
6973 + }
6974 +
6975 + // Image events are not recorded yet, so a 'type=image' filter excludes the container's
6976 + // events and leaves the stream empty.
6977 + {
6978 + WSLCFilter filter{"type", "image"};
6979 + wil::com_ptr<IWSLCEventStream> stream;
6980 + VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since, until, &filter, 1, &stream));
6981 +
6982 + VERIFY_IS_TRUE(drain(stream.get()).empty());
6983 + }
6984 +
6985 + // An unmatched container id yields an empty stream, and GetNext validates its out-pointer.
6986 + {
6987 + WSLCFilter filter{"container", "0000000000000000000000000000000000000000000000000000000000000000"};
6988 + wil::com_ptr<IWSLCEventStream> stream;
6989 + VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since, until, &filter, 1, &stream));
6990 +
6991 + VERIFY_IS_TRUE(drain(stream.get()).empty());
6992 + }
6993 +
6994 + // A since-time later than a non-zero until-time describes a backwards window and is rejected.
6995 + {
6996 + wil::com_ptr<IWSLCEventStream> stream;
6997 + VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->GetEvents(since + 1, since, nullptr, 0, &stream));
6998 + ValidateCOMErrorMessage(wsl::shared::Localization::MessageWslcEventsInvalidTimeWindow(since + 1, since));
6999 + }
7000 + }
7001 +
7002 + WSLC_TEST_METHOD(EventStreamReportsLostEvents)
7003 + {
7004 + // One more than the store's ring capacity, so the reader's next slot is guaranteed evicted.
7005 + constexpr size_t c_signalsToEvictReader = 257;
7006 +
7007 + WSLCContainerLauncher launcher("debian:latest", "wslc-test-event-stream-overrun", {"sleep", "99999"});
7008 + auto container = launcher.Launch(*m_defaultSession);
7009 + const auto id = container.Id();
7010 +
7011 + WSLCFilter filter{"container", id.c_str()};
7012 + wil::com_ptr<IWSLCEventStream> stream;
7013 + VERIFY_SUCCEEDED(m_defaultSession->GetEvents(0, 0, &filter, 1, &stream));
7014 +
7015 + // Read one event to place the reader's cursor inside the ring.
7016 + wil::unique_cotaskmem_ansistring eventJson;
7017 + VERIFY_SUCCEEDED(stream->GetNext(&eventJson));
7018 + const auto firstEvent = wsl::shared::FromJson<wsl::windows::common::wslc_schema::Event>(eventJson.get());
7019 + VERIFY_ARE_EQUAL("create", firstEvent.Action);
7020 + VERIFY_ARE_EQUAL(id, firstEvent.Actor.ID);
7021 +
7022 + // Docker emits a 'kill' event per signal. SIGWINCH is ignored by an unhandling init process, so the
7023 + // container keeps running and each signal costs only one event.
7024 + for (size_t i = 0; i < c_signalsToEvictReader; ++i)
7025 + {
7026 + VERIFY_SUCCEEDED(container.Get().Kill(WSLCSignalSIGWINCH));
7027 + }
7028 +
7029 + // Stopping waits for the 'die' event, which Docker delivers after every preceding 'kill'. Without this
7030 + // barrier the reader could be checked before the ring has overrun it.
7031 + VERIFY_SUCCEEDED(container.Get().Kill(WSLCSignalSIGKILL));
7032 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
7033 +
7034 + VERIFY_ARE_EQUAL(WSLC_E_EVENTS_LOST, stream->GetNext(&eventJson));
7035 +
7036 + // Reporting the gap resyncs the reader, so it resumes from the oldest event still buffered.
7037 + VERIFY_SUCCEEDED(stream->GetNext(&eventJson));
7038 + const auto resumedEvent = wsl::shared::FromJson<wsl::windows::common::wslc_schema::Event>(eventJson.get());
7039 + VERIFY_ARE_EQUAL("kill", resumedEvent.Action);
7040 + VERIFY_ARE_EQUAL(id, resumedEvent.Actor.ID);
7041 + }
7042 +
7043 + WSLC_TEST_METHOD(EventStreamSerializesConcurrentReaders)
7044 + {
7045 + constexpr auto c_containerName = "wslc-test-concurrent-event-readers";
7046 +
7047 + WSLCContainerLauncher launcher("debian:latest", c_containerName, {"sleep", "99999"});
7048 + auto container = launcher.Launch(*m_defaultSession);
7049 + const auto id = container.Id();
7050 +
7051 + WSLCFilter filters[]{{"container", id.c_str()}, {"event", "kill"}};
7052 + wil::com_ptr<IWSLCEventStream> stream;
7053 +
7054 + // The window doubles as a hang guard, so it must comfortably outlast the waits below.
7055 + const LONGLONG until = duration_cast<seconds>(system_clock::now().time_since_epoch()).count() + 120;
7056 + VERIFY_SUCCEEDED(m_defaultSession->GetEvents(0, until, filters, ARRAYSIZE(filters), &stream));
7057 +
7058 + wil::unique_cotaskmem_ansistring firstEventJson;
7059 + wil::unique_cotaskmem_ansistring secondEventJson;
7060 + HRESULT firstResult{};
7061 + HRESULT secondResult{};
7062 + wil::unique_event firstReaderStarted{wil::EventOptions::ManualReset};
7063 + wil::unique_event secondReaderStarted{wil::EventOptions::ManualReset};
7064 + std::thread firstReader;
7065 + std::thread secondReader;
7066 +
7067 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
7068 + LOG_IF_FAILED(container.Get().Kill(WSLCSignalSIGWINCH));
7069 + LOG_IF_FAILED(container.Get().Kill(WSLCSignalSIGWINCH));
7070 +
7071 + if (firstReader.joinable())
7072 + {
7073 + firstReader.join();
7074 + }
7075 +
7076 + if (secondReader.joinable())
7077 + {
7078 + secondReader.join();
7079 + }
7080 + });
7081 +
7082 + firstReader = std::thread([&]() {
7083 + firstReaderStarted.SetEvent();
7084 + firstResult = stream->GetNext(&firstEventJson);
7085 + });
7086 + VERIFY_IS_TRUE(firstReaderStarted.wait(30 * 1000));
7087 + VERIFY_ARE_EQUAL(WAIT_TIMEOUT, WaitForSingleObject(firstReader.native_handle(), 100));
7088 +
7089 + secondReader = std::thread([&]() {
7090 + secondReaderStarted.SetEvent();
7091 + secondResult = stream->GetNext(&secondEventJson);
7092 + });
7093 + VERIFY_IS_TRUE(secondReaderStarted.wait(30 * 1000));
7094 + VERIFY_ARE_EQUAL(WAIT_TIMEOUT, WaitForSingleObject(secondReader.native_handle(), 100));
7095 +
7096 + HANDLE readers[]{firstReader.native_handle(), secondReader.native_handle()};
7097 + VERIFY_SUCCEEDED(container.Get().Kill(WSLCSignalSIGWINCH));
7098 +
7099 + const DWORD completedReader = WaitForMultipleObjects(ARRAYSIZE(readers), readers, FALSE, 30 * 1000);
7100 + VERIFY_IS_TRUE(completedReader == WAIT_OBJECT_0 || completedReader == WAIT_OBJECT_0 + 1);
7101 +
7102 + // One event completes exactly one call; the other stays serialized until another event arrives.
7103 + const DWORD pendingReader = completedReader == WAIT_OBJECT_0 ? 1 : 0;
7104 + VERIFY_ARE_EQUAL(WAIT_TIMEOUT, WaitForSingleObject(readers[pendingReader], 100));
7105 +
7106 + VERIFY_SUCCEEDED(container.Get().Kill(WSLCSignalSIGWINCH));
7107 + VERIFY_ARE_EQUAL(WAIT_OBJECT_0, WaitForMultipleObjects(ARRAYSIZE(readers), readers, TRUE, 30 * 1000));
7108 +
7109 + firstReader.join();
7110 + secondReader.join();
7111 + cleanup.release();
7112 +
7113 + VERIFY_SUCCEEDED(firstResult);
7114 + VERIFY_SUCCEEDED(secondResult);
7115 +
7116 + const auto firstEvent = wsl::shared::FromJson<wsl::windows::common::wslc_schema::Event>(firstEventJson.get());
7117 + const auto secondEvent = wsl::shared::FromJson<wsl::windows::common::wslc_schema::Event>(secondEventJson.get());
7118 + VERIFY_ARE_EQUAL("kill", firstEvent.Action);
7119 + VERIFY_ARE_EQUAL(id, firstEvent.Actor.ID);
7120 + VERIFY_ARE_EQUAL("kill", secondEvent.Action);
7121 + VERIFY_ARE_EQUAL(id, secondEvent.Actor.ID);
7122 + }
7123 +
7124 + WSLC_TEST_METHOD(EventStreamSessionTerminationAbortsReader)
7125 + {
7126 + WSLCFilter filter{"type", "container"};
7127 + const LONGLONG since = duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
7128 + wil::com_ptr<IWSLCEventStream> stream;
7129 + VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since, 0, &filter, 1, &stream));
7130 +
7131 + std::promise<HRESULT> getNextResult;
7132 + std::thread readerThread([&]() {
7133 + wil::unique_cotaskmem_ansistring eventJson;
7134 + getNextResult.set_value(stream->GetNext(&eventJson));
7135 + });
7136 + auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { readerThread.join(); });
7137 +
7138 + auto future = getNextResult.get_future();
7139 +
7140 + VERIFY_SUCCEEDED(m_defaultSession->Terminate());
7141 + auto restore = ResetTestSession();
7142 +
7143 + // Termination wakes the parked reader; it must finish quickly and report E_ABORT.
7144 + FAIL_FAST_IF_MSG(
7145 + future.wait_for(10s) != std::future_status::ready, "event stream reader did not abort after session termination");
7146 + VERIFY_ARE_EQUAL(E_ABORT, future.get());
7147 + }
7148 +
7149 WSLC_TEST_METHOD(OpenContainer)
7150 {
7151 auto expectOpen = [&](const char* Id, HRESULT expectedResult = S_OK) {