diff --git a/doc/docs/api-reference/c/error-codes.md b/doc/docs/api-reference/c/error-codes.md
index 91e22551..e704cf8c 100644
--- a/doc/docs/api-reference/c/error-codes.md
+++ b/doc/docs/api-reference/c/error-codes.md
@@ -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` |
---
diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw
index 8db58645..24c395e3 100644
--- a/localization/strings/en-US/Resources.resw
+++ b/localization/strings/en-US/Resources.resw
@@ -2371,6 +2371,10 @@ For privacy information about this product please visit https://aka.ms/privacy.<
Invalid name: '{}'
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+ `since` time ({}) cannot be after `until` time ({})
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="`since`"}{Locked="`until`"}
+
Path is not absolute: '{}'
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
diff --git a/msipackage/package.wix.in b/msipackage/package.wix.in
index 38b01cd2..aea50478 100644
--- a/msipackage/package.wix.in
+++ b/msipackage/package.wix.in
@@ -359,6 +359,14 @@
+
+
+
+
+
+
+
+
diff --git a/src/windows/WslcSDK/wslcsdk.h b/src/windows/WslcSDK/wslcsdk.h
index 56f05a61..5b86444f 100644
--- a/src/windows/WslcSDK/wslcsdk.h
+++ b/src/windows/WslcSDK/wslcsdk.h
@@ -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
diff --git a/src/windows/common/wslutil.cpp b/src/windows/common/wslutil.cpp
index 50065721..027304fc 100644
--- a/src/windows/common/wslutil.cpp
+++ b/src/windows/common/wslutil.cpp
@@ -180,6 +180,8 @@ static const std::map 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),
diff --git a/src/windows/inc/wslc_schema.h b/src/windows/inc/wslc_schema.h
index 0df438a7..023e0334 100644
--- a/src/windows/inc/wslc_schema.h
+++ b/src/windows/inc/wslc_schema.h
@@ -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 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
diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl
index c4ee8321..dd2c165f 100644
--- a/src/windows/service/inc/wslc.idl
+++ b/src/windows/service/inc/wslc.idl
@@ -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 */")
diff --git a/src/windows/wslcsession/CMakeLists.txt b/src/windows/wslcsession/CMakeLists.txt
index 50c80365..da076197 100644
--- a/src/windows/wslcsession/CMakeLists.txt
+++ b/src/windows/wslcsession/CMakeLists.txt
@@ -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
diff --git a/src/windows/wslcsession/DockerEventTracker.cpp b/src/windows/wslcsession/DockerEventTracker.cpp
index a809ddc7..9a5868f8 100644
--- a/src/windows/wslcsession/DockerEventTracker.cpp
+++ b/src/windows/wslcsession/DockerEventTracker.cpp
@@ -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();
- 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 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::vector> 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(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 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);
}
}
diff --git a/src/windows/wslcsession/DockerEventTracker.h b/src/windows/wslcsession/DockerEventTracker.h
index f65fdea0..812a62f5 100644
--- a/src/windows/wslcsession/DockerEventTracker.h
+++ b/src/windows/wslcsession/DockerEventTracker.h
@@ -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, std::int64_t)>;
using VolumeEventCallback = std::function;
+ using ContainerCreateCallback = std::function;
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> m_containerCallbacks;
_Guarded_by_(m_lock) std::vector> m_volumeCallbacks;
+ _Guarded_by_(m_lock) std::vector> m_containerCreateCallbacks;
// Invokes a snapshot of callbacks taken under m_lock, skipping registrations that have since been unregistered.
template
@@ -142,9 +158,6 @@ private:
}
}
- _Guarded_by_(m_lock) std::unordered_set m_createdObjects;
- _Guarded_by_(m_lock) wil::unique_event m_objectCreated { wil::EventOptions::ManualReset };
-
WSLCSession& m_session;
std::mutex m_lock;
std::atomic m_callbackId{0};
diff --git a/src/windows/wslcsession/EventStore.cpp b/src/windows/wslcsession/EventStore.cpp
new file mode 100644
index 00000000..6a2b19cc
--- /dev/null
+++ b/src/windows/wslcsession/EventStore.cpp
@@ -0,0 +1,259 @@
+// Copyright (C) Microsoft Corporation. All rights reserved.
+
+#include "precomp.h"
+#include "EventStore.h"
+#include "WSLCSession.h"
+#include "WSLCExecutionContext.h"
+#include
+
+using wsl::shared::Localization;
+
+namespace wsl::windows::service::wslc {
+
+namespace {
+
+ std::optional 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::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 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>& 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 EventStore::CreateStream(
+ Microsoft::WRL::ComPtr Session, int64_t SinceTime, int64_t UntilTime, std::map> 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 stream;
+ THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&stream, std::move(Session), this, SinceTime, UntilTime, std::move(Filters)));
+
+ return stream;
+}
+
+std::optional 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& Lock, uint64_t SequenceNumber, std::optional 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 EventStore::Get(
+ std::optional& SequenceNumber,
+ std::optional Since,
+ std::optional Until,
+ const std::map>& 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 Session,
+ EventStore* Store,
+ int64_t SinceTime,
+ int64_t UntilTime,
+ std::map> 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(wsl::shared::ToJson(event.value()).c_str()).release();
+ return S_OK;
+}
+CATCH_RETURN();
+
+} // namespace wsl::windows::service::wslc
diff --git a/src/windows/wslcsession/EventStore.h b/src/windows/wslcsession/EventStore.h
new file mode 100644
index 00000000..b47aed6b
--- /dev/null
+++ b/src/windows/wslcsession/EventStore.h
@@ -0,0 +1,88 @@
+// Copyright (C) Microsoft Corporation. All rights reserved.
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include