diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index fccec5f14..60cdd00ed 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2291,6 +2291,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 ({}) + {Locked="`since`"}{Locked="`until`"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + 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 115bf6e65..0aec98437 100644 --- a/msipackage/package.wix.in +++ b/msipackage/package.wix.in @@ -308,6 +308,14 @@ + + + + + + + + diff --git a/src/windows/WslcSDK/wslcsdk.h b/src/windows/WslcSDK/wslcsdk.h index 97dc2b09f..d36ee66a0 100644 --- a/src/windows/WslcSDK/wslcsdk.h +++ b/src/windows/WslcSDK/wslcsdk.h @@ -43,6 +43,8 @@ EXTERN_C_START #define WSLC_E_REGISTRY_BLOCKED_BY_POLICY MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 13) /* 0x8004060D */ #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_EVENTS_LOST MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */ +#define WSLC_E_EVENT_STREAM_FINISHED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */ // Session values #define WSLC_SESSION_OPTIONS_SIZE 72 diff --git a/src/windows/inc/wslc_schema.h b/src/windows/inc/wslc_schema.h index a936a3355..540a5e9a2 100644 --- a/src/windows/inc/wslc_schema.h +++ b/src/windows/inc/wslc_schema.h @@ -200,4 +200,22 @@ struct Network NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Network, Id, Name, Driver, Scope, Internal, IPAM, 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; + uint64_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 9fc0fa4e5..416ded421 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -561,6 +561,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 { @@ -597,6 +609,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. 0 means + // unbounded on that end. + // Filters are key/value pair. Values sharing a key are OR'd, distinct keys are AND'd. + HRESULT GetEvents( + [in] ULONGLONG SinceTime, + [in] ULONGLONG 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); @@ -749,3 +773,5 @@ cpp_quote("#define WSLC_E_CONTAINER_DISABLED MAKE_HRESULT(SEVERITY_ERROR, FACILI cpp_quote("#define WSLC_E_REGISTRY_BLOCKED_BY_POLICY MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 13) /* 0x8004060D */") cpp_quote("#define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */") cpp_quote("#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */") +cpp_quote("#define WSLC_E_EVENTS_LOST MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */") +cpp_quote("#define WSLC_E_EVENT_STREAM_FINISHED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */") \ No newline at end of file diff --git a/src/windows/wslcsession/CMakeLists.txt b/src/windows/wslcsession/CMakeLists.txt index 6002df79f..03dda0d1a 100644 --- a/src/windows/wslcsession/CMakeLists.txt +++ b/src/windows/wslcsession/CMakeLists.txt @@ -25,6 +25,7 @@ set(SOURCES # Supporting classes DockerEventTracker.cpp DockerHTTPClient.cpp + EventStore.cpp IORelay.cpp OptionParser.cpp ServiceProcessLauncher.cpp @@ -34,6 +35,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 f5018d3bc..385d3b685 100644 --- a/src/windows/wslcsession/DockerEventTracker.cpp +++ b/src/windows/wslcsession/DockerEventTracker.cpp @@ -153,7 +153,11 @@ void DockerEventTracker::OnEvent(const std::string_view& event) void DockerEventTracker::OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime) { static std::map events{ - {"start", ContainerEvent::Start}, {"die", ContainerEvent::Stop}, {"destroy", ContainerEvent::Destroy}, {"exec_die", ContainerEvent::ExecDied}}; + {"start", ContainerEvent::Start}, + {"die", ContainerEvent::Stop}, + {"kill", ContainerEvent::Kill}, + {"destroy", ContainerEvent::Destroy}, + {"exec_die", ContainerEvent::ExecDied}}; auto actor = parsed.find("Actor"); THROW_HR_IF_MSG(E_INVALIDARG, actor == parsed.end(), "Missing Actor in container event"); diff --git a/src/windows/wslcsession/DockerEventTracker.h b/src/windows/wslcsession/DockerEventTracker.h index e040c7592..1ec7531ad 100644 --- a/src/windows/wslcsession/DockerEventTracker.h +++ b/src/windows/wslcsession/DockerEventTracker.h @@ -29,7 +29,8 @@ enum class ContainerEvent Stop, Exit, Destroy, - ExecDied + ExecDied, + Kill }; enum class VolumeEvent diff --git a/src/windows/wslcsession/EventStore.cpp b/src/windows/wslcsession/EventStore.cpp new file mode 100644 index 000000000..f3954bf7a --- /dev/null +++ b/src/windows/wslcsession/EventStore.cpp @@ -0,0 +1,252 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +#include "precomp.h" +#include "EventStore.h" +#include "WSLCSession.h" +#include "WSLCExecutionContext.h" +#include +#include "wslc_schema.h" + +using wsl::shared::Localization; + +namespace wsl::windows::service::wslc { + +namespace { + + // Normalizes a seconds-since-epoch window bound from the COM boundary, treating zero as "unset". + std::optional ToTimeBound(uint64_t TimeSeconds) + { + if (TimeSeconds == 0) + { + return std::nullopt; + } + + return TimeSeconds; + } + +} // namespace + +void EventStore::Append(wsl::windows::common::wslc_schema::Event Event) +{ + std::lock_guard lock(m_lock); + + 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::optional TimeSeconds) noexcept +try +{ + wsl::windows::common::wslc_schema::Event event; + event.Type = std::move(Type); + event.Action = std::move(Action); + event.Actor.ID = ActorId; + + event.time = TimeSeconds.value_or(static_cast( + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count())); + + 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, uint64_t SinceTime, uint64_t UntilTime, std::map> Filters) +{ + // A non-zero until earlier than since describes a backwards, empty window. + THROW_HR_WITH_USER_ERROR_IF( + E_INVALIDARG, Localization::MessageWslcEventsInvalidTimeWindow(SinceTime, UntilTime), 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()) + { + // Until is Unix seconds and system_clock shares that epoch, so it doubles as the wait deadline. + const std::chrono::sys_seconds deadline{std::chrono::seconds{static_cast(Until.value())}}; + if (!m_updated.wait_until(Lock, deadline, 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(); + + // An event past the until-bound closes the window: the stream is finished. + if (Until.has_value() && event.time > Until.value()) + { + return std::nullopt; + } + + // Advance past this event so the next read resumes at the following one. + SequenceNumber.value()++; + + // 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() || event.time >= 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, + uint64_t SinceTime, + uint64_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; + + 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 000000000..9572efc5d --- /dev/null +++ b/src/windows/wslcsession/EventStore.h @@ -0,0 +1,86 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#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::optional TimeSeconds = std::nullopt) noexcept; + + Microsoft::WRL::ComPtr CreateStream( + Microsoft::WRL::ComPtr Session, uint64_t SinceTime, uint64_t UntilTime, std::map> 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 Get( + std::optional& SequenceNumber, + std::optional Since, + std::optional Until, + const std::map>& 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 (Unix seconds) elapsed with no event ready. Throws + // E_ABORT if the session terminated while waiting. + bool WaitForEvent(std::unique_lock& Lock, uint64_t SequenceNumber, std::optional Until); + + std::optional GetLockHeld(uint64_t SequenceNumber); + + std::mutex m_lock; + std::condition_variable m_updated; + + _Guarded_by_(m_lock) std::deque 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, IWSLCEventStream, IFastRundown> +{ +public: + HRESULT RuntimeClassInitialize( + Microsoft::WRL::ComPtr Session, + EventStore* Store, + uint64_t SinceTime, + uint64_t UntilTime, + std::map> Filters); + + IFACEMETHOD(GetNext)(_Outptr_result_z_ LPSTR* EventJson) override; + +private: + Microsoft::WRL::ComPtr m_session; + EventStore* m_store = nullptr; + + std::optional m_since; + std::optional m_until; + std::map> m_filters; + + std::optional m_nextSequenceNumber; +}; + +} // namespace wsl::windows::service::wslc diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 674b0b0cd..8694728f9 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -389,6 +389,25 @@ WSLCContainerState DockerStateToWSLCState(ContainerState state) } } +// Maps a container state to the lifecycle event action recorded when it transitions into that +// state. Only states reached via Transition() are expected here; "kill" is a signal, not a state, +// and is recorded separately in OnEvent(). +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::uint64_t ParseDockerTimestamp(const std::string& timestamp) { // Docker timestamps are UTC ISO 8601, e.g. "2026-03-05T10:30:00.123456789Z". @@ -543,6 +562,7 @@ WSLCContainerImpl::WSLCContainerImpl( DockerEventTracker& EventTracker, DockerHTTPClient& DockerClient, IORelay& Relay, + EventStore& eventStore, WSLCContainerState InitialState, std::uint64_t CreatedAt, WSLCProcessFlags InitProcessFlags, @@ -563,6 +583,7 @@ WSLCContainerImpl::WSLCContainerImpl( m_dockerClient(DockerClient), m_eventTracker(EventTracker), m_ioRelay(Relay), + m_eventStore(eventStore), m_containerEvents(EventTracker.RegisterContainerStateUpdates( m_id, std::bind(&WSLCContainerImpl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))), m_state(InitialState), @@ -838,7 +859,12 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt CATCH_LOG(); } - auto inspectJson = InspectLockHeld(); + auto dockerInspect = m_dockerClient.InspectContainer(m_id); + auto startedAtSeconds = ParseDockerTimestamp(dockerInspect.State.StartedAt); + + auto wslcInspect = BuildInspectContainer(dockerInspect); + auto inspectJson = wsl::shared::ToJson(wslcInspect); + const auto pluginResult = m_pluginNotifier->OnContainerStarted(inspectJson.c_str()); if (FAILED(pluginResult)) { @@ -870,7 +896,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt portCleanup.release(); volumeCleanup.release(); - Transition(WslcContainerStateRunning); + Transition(WslcContainerStateRunning, startedAtSeconds); cleanup.release(); } @@ -880,7 +906,11 @@ void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional exitCod // Disconnect(), so in-flight COM callers can drain from COMImplClass::m_callers. unique_com_disconnect comWrapper; - if (event == ContainerEvent::Stop) + if (event == ContainerEvent::Kill) + { + m_eventStore.Record("container", "kill", m_id, eventTime); + } + else if (event == ContainerEvent::Stop) { THROW_HR_IF(E_UNEXPECTED, !exitCode.has_value()); SetExitCode(exitCode.value()); @@ -990,13 +1020,13 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill) } // Wait for the stop event to get the Docker timestamp. - std::optional stopTimestamp; + std::optional stopTimestampSeconds; if (m_wslcSession.WaitForEventOrSessionTerminating(m_stopNotification.Event.get(), 60s)) { - stopTimestamp = m_stopNotification.EventTime.load(std::memory_order_acquire); + stopTimestampSeconds = m_stopNotification.EventTime.load(std::memory_order_acquire); } - comWrapper = OnStopped(stopTimestamp); + comWrapper = OnStopped(stopTimestampSeconds); if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsRm)) { @@ -1008,7 +1038,7 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill) } } -__requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::OnStopped(std::optional stopTimestamp) +__requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::OnStopped(std::optional stopTimestampSeconds) { unique_com_disconnect comWrapper; @@ -1029,7 +1059,7 @@ __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl:: // Delete() may have already moved us to Deleted. if (m_state == WslcContainerStateRunning) { - Transition(WslcContainerStateExited, stopTimestamp); + Transition(WslcContainerStateExited, stopTimestampSeconds); if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsRm)) { @@ -1395,7 +1425,8 @@ std::unique_ptr WSLCContainerImpl::Create( std::function&& OnDeleted, DockerEventTracker& EventTracker, DockerHTTPClient& DockerClient, - IORelay& IoRelay) + IORelay& IoRelay, + EventStore& eventStore) { common::docker_schema::CreateContainer request; request.Image = containerOptions.Image; @@ -1777,6 +1808,9 @@ std::unique_ptr WSLCContainerImpl::Create( namedVolumes.emplace_back(containerOptions.NamedVolumes[i].Name); } + auto createTime = ParseDockerTimestamp(inspectData.Created); + eventStore.Record("container", "create", result.Id, createTime); + auto container = std::make_unique( wslcSession, virtualMachine, @@ -1794,12 +1828,14 @@ std::unique_ptr WSLCContainerImpl::Create( EventTracker, DockerClient, IoRelay, + eventStore, WslcContainerStateCreated, ParseDockerTimestamp(inspectData.Created), containerOptions.InitProcessOptions.Flags, containerOptions.Flags); deleteOnFailure.release(); + return container; } @@ -1812,7 +1848,8 @@ std::unique_ptr WSLCContainerImpl::Open( std::function&& OnDeleted, DockerEventTracker& EventTracker, DockerHTTPClient& DockerClient, - IORelay& ioRelay) + IORelay& ioRelay, + EventStore& eventStore) { // Extract container name from Docker's names list. std::string name = ExtractContainerName(dockerContainer.Names, dockerContainer.Id); @@ -1886,6 +1923,7 @@ std::unique_ptr WSLCContainerImpl::Open( EventTracker, DockerClient, ioRelay, + eventStore, DockerStateToWSLCState(dockerContainer.State), static_cast(dockerContainer.Created), metadata.InitProcessFlags, @@ -2164,7 +2202,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::Transition(WSLCContainerState State, std::optional stateChangedAt) noexcept +__requires_lock_held(m_lock) void WSLCContainerImpl::Transition(WSLCContainerState State, std::optional stateChangedAtSeconds) noexcept { // N.B. A deleted container cannot transition back to any other state. WI_ASSERT(m_state != WslcContainerStateDeleted); @@ -2176,7 +2214,11 @@ __requires_lock_held(m_lock) void WSLCContainerImpl::Transition(WSLCContainerSta TraceLoggingValue(m_id.c_str(), "ID")); m_state = State; - m_stateChangedAt = stateChangedAt.value_or(static_cast(std::time(nullptr))); + + // m_stateChangedAt is tracked in unix seconds, matching Docker's event timestamps. + m_stateChangedAt = stateChangedAtSeconds.value_or(static_cast(std::time(nullptr))); + + m_eventStore.Record("container", WSLCStateToEventAction(State), m_id, m_stateChangedAt); } WSLCContainer::WSLCContainer(WSLCContainerImpl* impl, WSLCSession& session, std::function&& OnDeleted) : diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index 10fd57fd3..e97e0aa5e 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -33,6 +33,7 @@ namespace wsl::windows::service::wslc { class WSLCContainer; class WSLCSession; class WSLCVolumes; +class EventStore; class unique_com_disconnect { @@ -87,6 +88,7 @@ class WSLCContainerImpl DockerEventTracker& EventTracker, DockerHTTPClient& DockerClient, IORelay& Relay, + EventStore& eventStore, WSLCContainerState InitialState, std::uint64_t CreatedAt, WSLCProcessFlags InitProcessFlags, @@ -118,7 +120,7 @@ class WSLCContainerImpl WSLCContainerState State() const noexcept; std::vector GetPorts() const; - __requires_lock_held(m_lock) void Transition(WSLCContainerState State, std::optional stateChangedAt = std::nullopt) noexcept; + __requires_lock_held(m_lock) void Transition(WSLCContainerState State, std::optional stateChangedAtSeconds = std::nullopt) noexcept; const std::string& ID() const noexcept; @@ -140,7 +142,8 @@ class WSLCContainerImpl std::function&& OnDeleted, DockerEventTracker& EventTracker, DockerHTTPClient& DockerClient, - IORelay& Relay); + IORelay& Relay, + EventStore& eventStore); static std::unique_ptr Open( const common::docker_schema::ContainerInfo& DockerContainer, @@ -151,7 +154,8 @@ class WSLCContainerImpl std::function&& OnDeleted, DockerEventTracker& EventTracker, DockerHTTPClient& DockerClient, - IORelay& Relay); + IORelay& Relay, + EventStore& eventStore); private: __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect DeleteExclusiveLockHeld(WSLCDeleteFlags Flags); @@ -164,7 +168,7 @@ class WSLCContainerImpl __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) [[nodiscard]] unique_com_disconnect OnStopped(std::optional stopTimestamp); + __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect OnStopped(std::optional stopTimestampSeconds); void SetExitCode(int ExitCode) noexcept; void SignalInitProcessExit() noexcept; @@ -219,6 +223,7 @@ class WSLCContainerImpl DockerEventTracker& m_eventTracker; DockerEventTracker::EventTrackingReference m_containerEvents; IORelay& m_ioRelay; + EventStore& m_eventStore; std::string m_networkMode; }; diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 0ae835011..230522ce3 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -23,6 +23,7 @@ Module Name: #include "WslCoreFilesystem.h" #include "wslpolicies.h" #include "APICompat.h" +#include using namespace wsl::windows::common; using io::MultiHandleWait; @@ -1966,7 +1967,8 @@ void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptio std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), m_eventTracker.value(), m_dockerClient.value(), - m_ioRelay); + m_ioRelay, + 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)); @@ -2806,6 +2808,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 @@ -3423,6 +3428,23 @@ try } CATCH_RETURN(); +HRESULT WSLCSession::GetEvents(ULONGLONG SinceTime, ULONGLONG 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{this}, SinceTime, UntilTime, std::move(filters)); + + *Stream = stream.Detach(); + return S_OK; +} +CATCH_RETURN(); + void WSLCSession::RecoverExistingContainers() { WI_ASSERT(m_dockerClient.has_value()); @@ -3444,7 +3466,8 @@ void WSLCSession::RecoverExistingContainers() std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), m_eventTracker.value(), m_dockerClient.value(), - m_ioRelay); + m_ioRelay, + m_eventStore); auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container)); WI_ASSERT(inserted); diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index b9f36e2f9..e7dc4213b 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -22,6 +22,7 @@ Module Name: #include "WSLCNetworkMetadata.h" #include "DockerEventTracker.h" #include "DockerHTTPClient.h" +#include "EventStore.h" #include "IORelay.h" #include #include @@ -103,6 +104,15 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession 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_ ULONGLONG SinceTime, + _In_ ULONGLONG UntilTime, + _In_reads_opt_(FiltersCount) const WSLCFilter* Filters, + _In_ ULONG FiltersCount, + _Outptr_ IWSLCEventStream** Stream) override; + // Image management. IFACEMETHOD(PullImage)( _In_ LPCSTR Image, @@ -244,11 +254,6 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession UserCOMCallback RegisterUserCOMCallback(); void UnregisterUserCOMCallback(DWORD ThreadId); - HANDLE SessionTerminatingEvent() const noexcept - { - return m_sessionTerminatingEvent.get(); - } - ULONG Id() const noexcept { return m_id; @@ -331,6 +336,9 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession wil::com_ptr m_pluginNotifier; + // Bounded in-memory ring of recent lifecycle events, shared by all event-stream subscribers. + EventStore m_eventStore; + // User-provided handles that the session is currently doing IO on. std::mutex m_userHandlesLock; __guarded_by(m_userHandlesLock) std::vector m_userHandles; diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 04b07f9fa..35a6c4c62 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -25,6 +25,7 @@ Module Name: #include "HttpHeaderEndDetector.h" #include +using namespace std::chrono; using namespace std::literals::chrono_literals; using namespace wsl::windows::common::registry; using wsl::windows::common::RunningWSLCContainer; @@ -6013,6 +6014,146 @@ class WSLCTests } } + WSLC_TEST_METHOD(EventStream) + { + auto now = [] { return duration_cast(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 events; + + wil::unique_cotaskmem_ansistring eventJson; + HRESULT result; + while (SUCCEEDED(result = stream->GetNext(&eventJson))) + { + events.push_back(wsl::shared::FromJson(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& events, + const std::string& actorId, + const std::vector& 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); + + // Events are reported in non-decreasing time order. + if (i > 0) + { + VERIFY_IS_TRUE(event.time >= events[i - 1].time); + } + } + }; + + // Run a container through its create/start/kill/stop lifecycle inside a bounded time window. + const ULONGLONG since = now(); + std::string id; + { + WSLCContainerLauncher launcher("debian:latest", "wslc-test-events", {"sleep", "99999"}); + 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); + } + + ULONGLONG until = now(); + + // The container's create, start, kill then stop 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 stream; + VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since, until, &filter, 1, &stream)); + + auto events = drain(stream.get()); + verifyEvents(events, id, {"create", "start", "kill", "stop", "destroy"}); + + // The whole lifecycle falls inside the requested window. + VERIFY_IS_TRUE(events[0].time >= since); + VERIFY_IS_TRUE(events[4].time <= until); + } + + // Each lifecycle action is independently selectable: an 'event=' filter, AND'd with + // the container filter, returns exactly that one event out of the four recorded above. + auto verifyEventFilter = [&](const char* action) { + WSLCFilter filters[]{{"container", id.c_str()}, {"event", action}}; + wil::com_ptr 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"); + + // 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 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 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 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(EventStreamSessionTerminationAbortsReader) + { + WSLCFilter filter{"type", "container"}; + const ULONGLONG since = duration_cast(system_clock::now().time_since_epoch()).count(); + wil::com_ptr stream; + VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since, 0, &filter, 1, &stream)); + + std::promise 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. + VERIFY_ARE_EQUAL(std::future_status::ready, future.wait_for(10s)); + VERIFY_ARE_EQUAL(E_ABORT, future.get()); + } + WSLC_TEST_METHOD(OpenContainer) { auto expectOpen = [&](const char* Id, HRESULT expectedResult = S_OK) {