From ba81b75d6f654dd3a8190fcfdb76f2ef9ee02aba Mon Sep 17 00:00:00 2001 From: kvega005 Date: Thu, 5 Mar 2026 11:09:04 -0800 Subject: [PATCH 1/9] Add hourly upstream sync workflow for feature/wsl-for-apps --- .github/workflows/sync-upstream.yml | 49 +++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/sync-upstream.yml diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 0000000000..fb0c13bfca --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,49 @@ +name: Sync fork with upstream + +on: + schedule: + # Hourly during Pacific business hours (8 AM - 6 PM PT = 16:00 - 02:00 UTC next day) + - cron: '0 16-23 * * 1-5' # Mon-Fri 16:00-23:00 UTC (8 AM - 3 PM PT) + - cron: '0 0-1 * * 2-6' # Tue-Sat 00:00-01:00 UTC (4 PM - 5 PM PT prev day) + workflow_dispatch: # Allow manual trigger + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout fork + uses: actions/checkout@v4 + with: + ref: feature/wsl-for-apps + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Add upstream remote + run: git remote add upstream https://github.com/Microsoft/WSL.git + + - name: Fetch upstream + run: git fetch upstream feature/wsl-for-apps + + - name: Check if update needed + id: check + run: | + LOCAL=$(git rev-parse HEAD) + UPSTREAM=$(git rev-parse upstream/feature/wsl-for-apps) + echo "local=$LOCAL" >> "$GITHUB_OUTPUT" + echo "upstream=$UPSTREAM" >> "$GITHUB_OUTPUT" + if [ "$LOCAL" = "$UPSTREAM" ]; then + echo "needs_update=false" >> "$GITHUB_OUTPUT" + echo "Already up to date." + else + echo "needs_update=true" >> "$GITHUB_OUTPUT" + echo "Update needed: $LOCAL -> $UPSTREAM" + fi + + - name: Fast-forward merge + if: steps.check.outputs.needs_update == 'true' + run: | + git merge --ff-only upstream/feature/wsl-for-apps + + - name: Push changes + if: steps.check.outputs.needs_update == 'true' + run: git push origin feature/wsl-for-apps From 50f9ce0c5c8b5b200b670fd3c707cfd1eb7f3f15 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Thu, 5 Mar 2026 11:19:36 -0800 Subject: [PATCH 2/9] Remove upstream sync workflow --- .github/workflows/sync-upstream.yml | 49 ----------------------------- 1 file changed, 49 deletions(-) delete mode 100644 .github/workflows/sync-upstream.yml diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml deleted file mode 100644 index fb0c13bfca..0000000000 --- a/.github/workflows/sync-upstream.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Sync fork with upstream - -on: - schedule: - # Hourly during Pacific business hours (8 AM - 6 PM PT = 16:00 - 02:00 UTC next day) - - cron: '0 16-23 * * 1-5' # Mon-Fri 16:00-23:00 UTC (8 AM - 3 PM PT) - - cron: '0 0-1 * * 2-6' # Tue-Sat 00:00-01:00 UTC (4 PM - 5 PM PT prev day) - workflow_dispatch: # Allow manual trigger - -jobs: - sync: - runs-on: ubuntu-latest - steps: - - name: Checkout fork - uses: actions/checkout@v4 - with: - ref: feature/wsl-for-apps - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Add upstream remote - run: git remote add upstream https://github.com/Microsoft/WSL.git - - - name: Fetch upstream - run: git fetch upstream feature/wsl-for-apps - - - name: Check if update needed - id: check - run: | - LOCAL=$(git rev-parse HEAD) - UPSTREAM=$(git rev-parse upstream/feature/wsl-for-apps) - echo "local=$LOCAL" >> "$GITHUB_OUTPUT" - echo "upstream=$UPSTREAM" >> "$GITHUB_OUTPUT" - if [ "$LOCAL" = "$UPSTREAM" ]; then - echo "needs_update=false" >> "$GITHUB_OUTPUT" - echo "Already up to date." - else - echo "needs_update=true" >> "$GITHUB_OUTPUT" - echo "Update needed: $LOCAL -> $UPSTREAM" - fi - - - name: Fast-forward merge - if: steps.check.outputs.needs_update == 'true' - run: | - git merge --ff-only upstream/feature/wsl-for-apps - - - name: Push changes - if: steps.check.outputs.needs_update == 'true' - run: git push origin feature/wsl-for-apps From 12395f5ddf7dfd0a66c1f3b616d09151576ad9d8 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 30 Jun 2026 16:43:20 -0700 Subject: [PATCH 3/9] WSLC Events --- msipackage/package.wix.in | 8 + src/windows/WslcSDK/wslcsdk.h | 2 + src/windows/inc/wslc_schema.h | 19 ++ src/windows/service/inc/wslc.idl | 28 +++ src/windows/wslcsession/CMakeLists.txt | 2 + .../wslcsession/DockerEventTracker.cpp | 29 ++- src/windows/wslcsession/DockerEventTracker.h | 9 +- src/windows/wslcsession/EventStore.cpp | 225 ++++++++++++++++++ src/windows/wslcsession/EventStore.h | 84 +++++++ src/windows/wslcsession/WSLCContainer.cpp | 57 +++-- src/windows/wslcsession/WSLCContainer.h | 15 +- .../wslcsession/WSLCProcessControl.cpp | 6 +- src/windows/wslcsession/WSLCProcessControl.h | 2 +- src/windows/wslcsession/WSLCSession.cpp | 33 ++- src/windows/wslcsession/WSLCSession.h | 18 +- test/windows/WSLCTests.cpp | 154 ++++++++++++ 16 files changed, 648 insertions(+), 43 deletions(-) create mode 100644 src/windows/wslcsession/EventStore.cpp create mode 100644 src/windows/wslcsession/EventStore.h diff --git a/msipackage/package.wix.in b/msipackage/package.wix.in index 7fcbb752ff..9e71b25b21 100644 --- a/msipackage/package.wix.in +++ b/msipackage/package.wix.in @@ -306,6 +306,14 @@ + + + + + + + + diff --git a/src/windows/WslcSDK/wslcsdk.h b/src/windows/WslcSDK/wslcsdk.h index 44db9c8c98..049f4e9cd4 100644 --- a/src/windows/WslcSDK/wslcsdk.h +++ b/src/windows/WslcSDK/wslcsdk.h @@ -41,6 +41,8 @@ EXTERN_C_START #define WSLC_E_CONTAINER_DISABLED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 12) /* 0x8004060C */ #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_EVENTS_LOST MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */ +#define WSLC_E_EVENT_STREAM_FINISHED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */ // 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 417efb4731..1459f52d60 100644 --- a/src/windows/inc/wslc_schema.h +++ b/src/windows/inc/wslc_schema.h @@ -200,4 +200,23 @@ 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; + int64_t time{}; + int64_t timeNano{}; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Event, Type, Action, Actor, time, timeNano); +}; + } // namespace wsl::windows::common::wslc_schema diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 10af7310a1..8c87e24804 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -540,6 +540,21 @@ 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, which matches + // docker's `events --format json` shape: {"Type","Action","Actor":{"ID","Attributes":{...}},"time","timeNano"}. + // JSON rather than a struct so future fields (Actor.Attributes, exec/image specifics) need no + // IDL change and the whole event is one allocation the caller frees with a single CoTaskMemFree. + HRESULT GetNext([out, string] LPSTR* EventJson); +} + // Settings for IWSLCSession::Initialize - passed from service to per-user process typedef struct _WSLCSessionInitSettings { @@ -575,6 +590,17 @@ 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. + // SinceTimeNano / UntilTimeNano bound the window by event time. 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] LONGLONG SinceTimeNano, + [in] LONGLONG UntilTimeNano, + [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); @@ -725,3 +751,5 @@ cpp_quote("#define WSLC_E_SDK_UPDATE_NEEDED MAKE_HRESULT(SEVERITY_ERROR, FACILIT cpp_quote("#define WSLC_E_CONTAINER_DISABLED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 12) /* 0x8004060C */") 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_EVENTS_LOST MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */") +cpp_quote("#define WSLC_E_EVENT_STREAM_FINISHED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */") diff --git a/src/windows/wslcsession/CMakeLists.txt b/src/windows/wslcsession/CMakeLists.txt index 6002df79fb..03dda0d1a8 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 f5018d3bca..721042ee0d 100644 --- a/src/windows/wslcsession/DockerEventTracker.cpp +++ b/src/windows/wslcsession/DockerEventTracker.cpp @@ -110,7 +110,16 @@ void DockerEventTracker::OnEvent(const std::string_view& event) auto timeEntry = parsed.find("time"); THROW_HR_IF_MSG( E_INVALIDARG, timeEntry == parsed.end(), "Failed to parse time from event: %.*hs", static_cast(event.size()), event.data()); - std::uint64_t eventTime = timeEntry->get(); + std::uint64_t eventTimeSeconds = timeEntry->get(); + + auto timeNanoEntry = parsed.find("timeNano"); + THROW_HR_IF_MSG( + E_INVALIDARG, + timeNanoEntry == parsed.end(), + "Failed to parse timeNano from event: %.*hs", + static_cast(event.size()), + event.data()); + std::uint64_t eventTimeNano = timeNanoEntry->get(); auto actionStr = action->get(); @@ -120,11 +129,11 @@ void DockerEventTracker::OnEvent(const std::string_view& event) if (typeStr == "container") { - OnContainerEvent(parsed, actionStr, eventTime); + OnContainerEvent(parsed, actionStr, eventTimeSeconds, eventTimeNano); } else if (typeStr == "volume") { - OnVolumeEvent(parsed, actionStr, eventTime); + OnVolumeEvent(parsed, actionStr, eventTimeNano); } // Track object creation for WaitForObjectCreated. @@ -150,10 +159,14 @@ void DockerEventTracker::OnEvent(const std::string_view& event) } } -void DockerEventTracker::OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime) +void DockerEventTracker::OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTimeSeconds, std::uint64_t eventTimeNano) { 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"); @@ -193,12 +206,12 @@ void DockerEventTracker::OnContainerEvent(const nlohmann::json& parsed, const st { if (e.ContainerId == containerId && (!e.ExecId.has_value() || e.ExecId == execId)) { - e.Callback(it->second, exitCode, eventTime); + e.Callback(it->second, exitCode, eventTimeSeconds, eventTimeNano); } } } -void DockerEventTracker::OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime) +void DockerEventTracker::OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTimeNano) { static std::map events{{"create", VolumeEvent::Create}, {"destroy", VolumeEvent::Destroy}}; @@ -220,7 +233,7 @@ void DockerEventTracker::OnVolumeEvent(const nlohmann::json& parsed, const std:: for (const auto& e : m_volumeCallbacks) { - e.Callback(volumeName, it->second, eventTime); + e.Callback(volumeName, it->second, eventTimeNano); } } diff --git a/src/windows/wslcsession/DockerEventTracker.h b/src/windows/wslcsession/DockerEventTracker.h index e040c7592f..a784ae2ab0 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 @@ -61,7 +62,7 @@ class DockerEventTracker DockerEventTracker* m_tracker = nullptr; }; - using ContainerStateChangeCallback = std::function, std::uint64_t)>; + using ContainerStateChangeCallback = std::function, std::uint64_t, std::uint64_t)>; using VolumeEventCallback = std::function; DockerEventTracker(DockerHTTPClient& dockerClient, WSLCSession& session, IORelay& relay); @@ -76,8 +77,8 @@ class DockerEventTracker private: void OnEvent(const std::string_view& event); - void OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime); - void OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime); + void OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTimeSeconds, std::uint64_t eventTimeNano); + void OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTimeNano); struct ContainerCallback { diff --git a/src/windows/wslcsession/EventStore.cpp b/src/windows/wslcsession/EventStore.cpp new file mode 100644 index 0000000000..1f27bb0799 --- /dev/null +++ b/src/windows/wslcsession/EventStore.cpp @@ -0,0 +1,225 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +#include "precomp.h" +#include "EventStore.h" +#include "WSLCSession.h" +#include +#include "wslc_schema.h" + +namespace wsl::windows::service::wslc { + +namespace { + + // Converts a nanoseconds-since-epoch bound from the COM boundary into a timestamp, treating zero as + // "unset". + std::optional ToEventTime(int64_t TimeNano) + { + if (TimeNano == 0) + { + return std::nullopt; + } + + return EventTime{std::chrono::nanoseconds{TimeNano}}; + } + +} // 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, std::optional TimeNano) noexcept +try +{ + wsl::windows::common::wslc_schema::Event event; + event.Type = std::move(Type); + event.Action = std::move(Action); + event.Actor.ID = ActorId; + + const auto now = std::chrono::system_clock::now().time_since_epoch(); + event.time = TimeSeconds.value_or(std::chrono::duration_cast(now).count()); + event.timeNano = TimeNano.value_or(std::chrono::duration_cast(now).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, int64_t SinceTimeNano, int64_t UntilTimeNano, std::map> Filters) +{ + // Start the reader at the oldest event still buffered so it sees all the buffered history. + uint64_t startSequenceNumber; + { + std::lock_guard lock(m_lock); + startSequenceNumber = m_firstSequenceNumber; + } + + Microsoft::WRL::ComPtr stream; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize( + &stream, std::move(Session), this, startSequenceNumber, SinceTimeNano, UntilTimeNano, std::move(Filters))); + + return stream; +} + +std::optional EventStore::GetLockHeld(uint64_t SequenceNumber) +{ + if (SequenceNumber < m_firstSequenceNumber) + { + THROW_HR(WSLC_E_EVENTS_LOST); + } + + const uint64_t index = SequenceNumber - m_firstSequenceNumber; + if (index >= m_events.size()) + { + return std::nullopt; + } + + return m_events[index]; +} + +std::optional EventStore::Get(uint64_t SequenceNumber, std::optional Until) +{ + std::unique_lock lock(m_lock); + + // Checked before parking, so a termination that already latched is seen without sleeping. + const auto ready = [&] { return m_terminating || GetLockHeld(SequenceNumber).has_value(); }; + + if (Until.has_value()) + { + // Round the deadline up to system_clock's coarser tick so the wait never ends early. + const auto deadline = std::chrono::ceil(Until.value()); + if (!m_updated.wait_until(lock, deadline, ready)) + { + return std::nullopt; + } + } + else + { + m_updated.wait(lock, ready); + } + + THROW_HR_IF(E_ABORT, m_terminating); + return GetLockHeld(SequenceNumber); +} + +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 StartSequenceNumber, + int64_t SinceTimeNano, + int64_t UntilTimeNano, + std::map> Filters) +{ + m_session = std::move(Session); + m_store = Store; + m_nextSequenceNumber = StartSequenceNumber; + m_since = ToEventTime(SinceTimeNano); + m_until = ToEventTime(UntilTimeNano); + m_filters = std::move(Filters); + return S_OK; +} + +HRESULT EventStream::GetNext(LPSTR* EventJson) +try +{ + RETURN_HR_IF_NULL(E_POINTER, EventJson); + *EventJson = nullptr; + + // Read forward until an event lands inside the time window and matches the filters. + while (true) + { + const auto event = m_store->Get(m_nextSequenceNumber, m_until); + if (!event.has_value()) + { + return WSLC_E_EVENT_STREAM_FINISHED; + } + + ++m_nextSequenceNumber; + + const EventTime eventTime{std::chrono::nanoseconds{event.value().timeNano}}; + + if (m_until.has_value() && eventTime > m_until.value()) + { + return WSLC_E_EVENT_STREAM_FINISHED; + } + + if (m_since.has_value() && eventTime < m_since.value()) + { + continue; + } + + if (!EventMatchesFilters(event.value(), m_filters)) + { + continue; + } + + *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 0000000000..1bbdf4d78e --- /dev/null +++ b/src/windows/wslcsession/EventStore.h @@ -0,0 +1,84 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "wslc.h" +#include "wslc_schema.h" + +namespace wsl::windows::service::wslc { + +class WSLCSession; + +using EventTime = std::chrono::sys_time; + +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, + std::optional TimeNano = std::nullopt) noexcept; + + Microsoft::WRL::ComPtr CreateStream( + Microsoft::WRL::ComPtr Session, + int64_t SinceTimeNano, + int64_t UntilTimeNano, + std::map> Filters); + + std::optional Get(uint64_t SequenceNumber, std::optional Until); + + void OnSessionTerminating(); + +private: + void Append(wsl::windows::common::wslc_schema::Event Event); + + 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 StartSequenceNumber, + int64_t SinceTimeNano, + int64_t UntilTimeNano, + 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; + + uint64_t m_nextSequenceNumber = 0; +}; + +} // namespace wsl::windows::service::wslc diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index b392a1ddf0..8b77645af6 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -535,6 +535,7 @@ WSLCContainerImpl::WSLCContainerImpl( DockerEventTracker& EventTracker, DockerHTTPClient& DockerClient, IORelay& Relay, + EventStore& eventStore, WSLCContainerState InitialState, std::uint64_t CreatedAt, WSLCProcessFlags InitProcessFlags, @@ -555,8 +556,9 @@ 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_id, std::bind(&WSLCContainerImpl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4))), m_state(InitialState), m_createdAt(CreatedAt), m_initProcessFlags(InitProcessFlags), @@ -866,20 +868,32 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt cleanup.release(); } -void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional exitCode, std::uint64_t eventTime) +void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional exitCode, std::uint64_t eventTimeSeconds, std::uint64_t eventTimeNano) { // We must release m_lock and m_stopLock before the wrapper's destructor calls // Disconnect(), so in-flight COM callers can drain from COMImplClass::m_callers. unique_com_disconnect comWrapper; - if (event == ContainerEvent::Stop) + // Record lifecycle events here, driven by Docker's own events, so the store gets Docker's + // timestamps and the same ordering Docker observed. + if (event == ContainerEvent::Start) { + m_eventStore.Record("container", "start", m_id, static_cast(eventTimeSeconds), static_cast(eventTimeNano)); + } + else if (event == ContainerEvent::Kill) + { + m_eventStore.Record("container", "kill", m_id, static_cast(eventTimeSeconds), static_cast(eventTimeNano)); + } + else if (event == ContainerEvent::Stop) + { + m_eventStore.Record("container", "stop", m_id, static_cast(eventTimeSeconds), static_cast(eventTimeNano)); + THROW_HR_IF(E_UNEXPECTED, !exitCode.has_value()); SetExitCode(exitCode.value()); std::unique_lock stopGuard{m_stopLock, std::try_to_lock}; - m_stopNotification.EventTime.store(eventTime, std::memory_order_release); + m_stopNotification.EventTime.store(eventTimeSeconds, std::memory_order_release); m_stopNotification.Event.SetEvent(); // If Stop() is already in flight, it will wake when the stop event is signaled and take care of cleanup. @@ -889,7 +903,7 @@ void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional exitCod } auto lock = m_lock.lock_exclusive(); - comWrapper = OnStopped(eventTime); + comWrapper = OnStopped(eventTimeSeconds); } else if (event == ContainerEvent::Destroy) { @@ -900,7 +914,7 @@ void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional exitCod if (m_state != WslcContainerStateDeleted) { - Transition(WslcContainerStateDeleted, eventTime); + Transition(WslcContainerStateDeleted, eventTimeSeconds); comWrapper = ReleaseResources(); } @@ -980,13 +994,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)) { @@ -998,7 +1012,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; @@ -1019,7 +1033,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)) { @@ -1386,7 +1400,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,12 +1792,20 @@ std::unique_ptr WSLCContainerImpl::Create( EventTracker, DockerClient, IoRelay, + eventStore, WslcContainerStateCreated, ParseDockerTimestamp(inspectData.Created), containerOptions.InitProcessOptions.Flags, containerOptions.Flags); deleteOnFailure.release(); + + // The create event is not delivered on Docker's event stream the way start/kill/stop are, so + // record it directly now that the container is committed. + const int64_t createTimeNano = + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + eventStore.Record("container", "create", container->ID(), createTimeNano / 1'000'000'000, createTimeNano); + return container; } @@ -1795,7 +1818,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); @@ -1869,6 +1893,7 @@ std::unique_ptr WSLCContainerImpl::Open( EventTracker, DockerClient, ioRelay, + eventStore, DockerStateToWSLCState(dockerContainer.State), static_cast(dockerContainer.Created), metadata.InitProcessFlags, @@ -2149,7 +2174,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); @@ -2161,7 +2186,9 @@ __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))); } WSLCContainer::WSLCContainer(WSLCContainerImpl* impl, WSLCSession& session, std::function&& OnDeleted) : diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index 10fd57fd37..1ed9bb5297 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,20 +154,21 @@ 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); void AllocateBridgedModePorts(); - void OnEvent(ContainerEvent event, std::optional exitCode, std::uint64_t eventTime); + void OnEvent(ContainerEvent event, std::optional exitCode, std::uint64_t eventTimeSeconds, std::uint64_t eventTimeNano); __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect ReleaseResources(); __requires_exclusive_lock_held(m_lock) void ReleaseRuntimeResources(); __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/WSLCProcessControl.cpp b/src/windows/wslcsession/WSLCProcessControl.cpp index 0c0e61eac9..75f9e6a001 100644 --- a/src/windows/wslcsession/WSLCProcessControl.cpp +++ b/src/windows/wslcsession/WSLCProcessControl.cpp @@ -112,7 +112,9 @@ DockerExecProcessControl::DockerExecProcessControl( m_id(Id), m_client(DockerClient), m_eventTrackingReference(EventTracker.RegisterExecStateUpdates( - Container.ID(), Id, std::bind(&DockerExecProcessControl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))) + Container.ID(), + Id, + std::bind(&DockerExecProcessControl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4))) { } @@ -164,7 +166,7 @@ void DockerExecProcessControl::SetExitCode(int ExitCode) } } -void DockerExecProcessControl::OnEvent(ContainerEvent Event, std::optional ExitCode, std::uint64_t /*eventTime*/) +void DockerExecProcessControl::OnEvent(ContainerEvent Event, std::optional ExitCode, std::uint64_t /*eventTimeSeconds*/, std::uint64_t /*eventTimeNano*/) { if (Event == ContainerEvent::ExecDied && !m_exitEvent.is_signaled()) { diff --git a/src/windows/wslcsession/WSLCProcessControl.h b/src/windows/wslcsession/WSLCProcessControl.h index 54cb2aca55..bea4a1ef26 100644 --- a/src/windows/wslcsession/WSLCProcessControl.h +++ b/src/windows/wslcsession/WSLCProcessControl.h @@ -72,7 +72,7 @@ class DockerExecProcessControl : public WSLCProcessControl void SetExitCode(int ExitCode); private: - void OnEvent(ContainerEvent Event, std::optional ExitCode, std::uint64_t eventTime); + void OnEvent(ContainerEvent Event, std::optional ExitCode, std::uint64_t eventTimeSeconds, std::uint64_t eventTimeNano); mutable std::mutex m_lock; std::string m_id; diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 17b2a0758b..d4c5b51420 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; @@ -787,7 +788,8 @@ void WSLCSession::StreamImageOperation(DockerHTTPClient::HTTPRequestContext& req void WSLCSession::OnImageCreated(const std::string& ImageNameOrId) noexcept try { - LOG_IF_FAILED(m_pluginNotifier->OnImageCreated(InspectImageLockHeld(ImageNameOrId).c_str())); + const auto dockerInspect = m_dockerClient->InspectImage(ImageNameOrId); + LOG_IF_FAILED(m_pluginNotifier->OnImageCreated(wsl::shared::ToJson(ConvertInspectImage(dockerInspect)).c_str())); } CATCH_LOG() @@ -1887,7 +1889,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)); @@ -2717,6 +2720,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 @@ -3320,6 +3326,26 @@ try } CATCH_RETURN(); +HRESULT WSLCSession::GetEvents(LONGLONG SinceTimeNano, LONGLONG UntilTimeNano, const WSLCFilter* Filters, ULONG FiltersCount, IWSLCEventStream** Stream) +try +{ + WSLCExecutionContext context(this); + + RETURN_HR_IF_NULL(E_POINTER, Stream); + + *Stream = nullptr; + + // A non-zero until earlier than since describes a backwards, empty window. + RETURN_HR_IF(E_INVALIDARG, UntilTimeNano != 0 && SinceTimeNano > UntilTimeNano); + + auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); + auto stream = m_eventStore.CreateStream(Microsoft::WRL::ComPtr{this}, SinceTimeNano, UntilTimeNano, std::move(filters)); + + *Stream = stream.Detach(); + return S_OK; +} +CATCH_RETURN(); + void WSLCSession::RecoverExistingContainers() { WI_ASSERT(m_dockerClient.has_value()); @@ -3341,7 +3367,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 bd77c87e76..90aa2d9be4 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 @@ -102,6 +103,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_ LONGLONG SinceTimeNano, + _In_ LONGLONG UntilTimeNano, + _In_reads_opt_(FiltersCount) const WSLCFilter* Filters, + _In_ ULONG FiltersCount, + _Outptr_ IWSLCEventStream** Stream) override; + // Image management. IFACEMETHOD(PullImage)( _In_ LPCSTR Image, @@ -241,11 +251,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; @@ -327,6 +332,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 b0cde34fb2..ab09a22eb3 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -24,6 +24,7 @@ Module Name: #include "wslc/e2e/WSLCE2EHelpers.h" #include +using namespace std::chrono; using namespace std::literals::chrono_literals; using namespace wsl::windows::common::registry; using wsl::windows::common::RunningWSLCContainer; @@ -5797,6 +5798,159 @@ 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 a drained event vector carries exactly the expected container events, in order: + // each is a container event for `actorId` whose seconds and nanosecond timestamps agree. + auto verifyEvents = [&](const std::vector& events, + const std::string& actorId, + std::initializer_list expectedActions) { + VERIFY_ARE_EQUAL(events.size(), expectedActions.size()); + + size_t i = 0; + for (const auto action : expectedActions) + { + VERIFY_ARE_EQUAL(std::string("container"), events[i].Type); + VERIFY_ARE_EQUAL(std::string(action), events[i].Action); + VERIFY_ARE_EQUAL(actorId, events[i].Actor.ID); + + // Each event carries Docker's seconds timestamp alongside the nanosecond one; they agree. + VERIFY_ARE_EQUAL(events[i].time, events[i].timeNano / 1'000'000'000); + ++i; + } + }; + + // Run a container through its create/start/kill/stop lifecycle inside a bounded time window. + const LONGLONG since = now(); + std::string id; + LONGLONG until = 0; + { + 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); + + 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"}); + + // The whole lifecycle falls inside the requested window. + VERIFY_IS_TRUE(events[0].timeNano >= since); + VERIFY_IS_TRUE(events[3].timeNano <= until); + + // The start, kill and stop events share Docker's clock, so they are ordered. + VERIFY_IS_TRUE(events[2].timeNano >= events[1].timeNano); + VERIFY_IS_TRUE(events[3].timeNano >= events[2].timeNano); + } + + // 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(until, since, nullptr, 0, &stream)); + } + + // A bounded window that closed before any event was recorded returns immediately with an + // empty stream rather than blocking for events that can never arrive. + { + constexpr LONGLONG oneSecondNano = 1'000'000'000; + WSLCFilter filter{"container", id.c_str()}; + wil::com_ptr stream; + VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since - 2 * oneSecondNano, since - oneSecondNano, &filter, 1, &stream)); + + VERIFY_IS_TRUE(drain(stream.get()).empty()); + } + } + + WSLC_TEST_METHOD(EventStreamSessionTerminationAbortsReader) + { + WSLCFilter filter{"type", "container"}; + const LONGLONG 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) { From 2c3e0ffc5c513a569c3f604357200e9496e65b88 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 1 Jul 2026 11:40:19 -0700 Subject: [PATCH 4/9] Fine-tune --- src/windows/inc/wslc_schema.h | 5 +-- src/windows/service/inc/wslc.idl | 7 ++-- .../wslcsession/DockerEventTracker.cpp | 23 ++++------- src/windows/wslcsession/DockerEventTracker.h | 6 +-- src/windows/wslcsession/EventStore.cpp | 38 +++++++++---------- src/windows/wslcsession/EventStore.h | 25 ++++-------- src/windows/wslcsession/WSLCContainer.cpp | 25 ++++++------ src/windows/wslcsession/WSLCContainer.h | 2 +- .../wslcsession/WSLCProcessControl.cpp | 6 +-- src/windows/wslcsession/WSLCProcessControl.h | 2 +- src/windows/wslcsession/WSLCSession.cpp | 9 ++--- src/windows/wslcsession/WSLCSession.h | 4 +- test/windows/WSLCTests.cpp | 25 ++++++------ 13 files changed, 74 insertions(+), 103 deletions(-) diff --git a/src/windows/inc/wslc_schema.h b/src/windows/inc/wslc_schema.h index 1459f52d60..59f8abc6fd 100644 --- a/src/windows/inc/wslc_schema.h +++ b/src/windows/inc/wslc_schema.h @@ -213,10 +213,9 @@ struct Event std::string Type; std::string Action; EventActor Actor; - int64_t time{}; - int64_t timeNano{}; + uint64_t time{}; - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Event, Type, Action, Actor, time, timeNano); + 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 8c87e24804..ab736fada7 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -592,11 +592,12 @@ interface IWSLCSession : IUnknown // 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. - // SinceTimeNano / UntilTimeNano bound the window by event time. 0 means unbounded on that end. + // 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] LONGLONG SinceTimeNano, - [in] LONGLONG UntilTimeNano, + [in] ULONGLONG SinceTime, + [in] ULONGLONG UntilTime, [in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out] IWSLCEventStream** Stream); diff --git a/src/windows/wslcsession/DockerEventTracker.cpp b/src/windows/wslcsession/DockerEventTracker.cpp index 721042ee0d..385d3b6850 100644 --- a/src/windows/wslcsession/DockerEventTracker.cpp +++ b/src/windows/wslcsession/DockerEventTracker.cpp @@ -110,16 +110,7 @@ void DockerEventTracker::OnEvent(const std::string_view& event) auto timeEntry = parsed.find("time"); THROW_HR_IF_MSG( E_INVALIDARG, timeEntry == parsed.end(), "Failed to parse time from event: %.*hs", static_cast(event.size()), event.data()); - std::uint64_t eventTimeSeconds = timeEntry->get(); - - auto timeNanoEntry = parsed.find("timeNano"); - THROW_HR_IF_MSG( - E_INVALIDARG, - timeNanoEntry == parsed.end(), - "Failed to parse timeNano from event: %.*hs", - static_cast(event.size()), - event.data()); - std::uint64_t eventTimeNano = timeNanoEntry->get(); + std::uint64_t eventTime = timeEntry->get(); auto actionStr = action->get(); @@ -129,11 +120,11 @@ void DockerEventTracker::OnEvent(const std::string_view& event) if (typeStr == "container") { - OnContainerEvent(parsed, actionStr, eventTimeSeconds, eventTimeNano); + OnContainerEvent(parsed, actionStr, eventTime); } else if (typeStr == "volume") { - OnVolumeEvent(parsed, actionStr, eventTimeNano); + OnVolumeEvent(parsed, actionStr, eventTime); } // Track object creation for WaitForObjectCreated. @@ -159,7 +150,7 @@ void DockerEventTracker::OnEvent(const std::string_view& event) } } -void DockerEventTracker::OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTimeSeconds, std::uint64_t eventTimeNano) +void DockerEventTracker::OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime) { static std::map events{ {"start", ContainerEvent::Start}, @@ -206,12 +197,12 @@ void DockerEventTracker::OnContainerEvent(const nlohmann::json& parsed, const st { if (e.ContainerId == containerId && (!e.ExecId.has_value() || e.ExecId == execId)) { - e.Callback(it->second, exitCode, eventTimeSeconds, eventTimeNano); + e.Callback(it->second, exitCode, eventTime); } } } -void DockerEventTracker::OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTimeNano) +void DockerEventTracker::OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime) { static std::map events{{"create", VolumeEvent::Create}, {"destroy", VolumeEvent::Destroy}}; @@ -233,7 +224,7 @@ void DockerEventTracker::OnVolumeEvent(const nlohmann::json& parsed, const std:: for (const auto& e : m_volumeCallbacks) { - e.Callback(volumeName, it->second, eventTimeNano); + e.Callback(volumeName, it->second, eventTime); } } diff --git a/src/windows/wslcsession/DockerEventTracker.h b/src/windows/wslcsession/DockerEventTracker.h index a784ae2ab0..1ec7531ad5 100644 --- a/src/windows/wslcsession/DockerEventTracker.h +++ b/src/windows/wslcsession/DockerEventTracker.h @@ -62,7 +62,7 @@ class DockerEventTracker DockerEventTracker* m_tracker = nullptr; }; - using ContainerStateChangeCallback = std::function, std::uint64_t, std::uint64_t)>; + using ContainerStateChangeCallback = std::function, std::uint64_t)>; using VolumeEventCallback = std::function; DockerEventTracker(DockerHTTPClient& dockerClient, WSLCSession& session, IORelay& relay); @@ -77,8 +77,8 @@ class DockerEventTracker private: void OnEvent(const std::string_view& event); - void OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTimeSeconds, std::uint64_t eventTimeNano); - void OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTimeNano); + void OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime); + void OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime); struct ContainerCallback { diff --git a/src/windows/wslcsession/EventStore.cpp b/src/windows/wslcsession/EventStore.cpp index 1f27bb0799..91d9e02b38 100644 --- a/src/windows/wslcsession/EventStore.cpp +++ b/src/windows/wslcsession/EventStore.cpp @@ -10,16 +10,15 @@ namespace wsl::windows::service::wslc { namespace { - // Converts a nanoseconds-since-epoch bound from the COM boundary into a timestamp, treating zero as - // "unset". - std::optional ToEventTime(int64_t TimeNano) + // Normalizes a seconds-since-epoch window bound from the COM boundary, treating zero as "unset". + std::optional ToTimeBound(uint64_t TimeSeconds) { - if (TimeNano == 0) + if (TimeSeconds == 0) { return std::nullopt; } - return EventTime{std::chrono::nanoseconds{TimeNano}}; + return TimeSeconds; } } // namespace @@ -39,7 +38,7 @@ void EventStore::Append(wsl::windows::common::wslc_schema::Event Event) m_updated.notify_all(); } -void EventStore::Record(std::string Type, std::string Action, const std::string& ActorId, std::optional TimeSeconds, std::optional TimeNano) noexcept +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; @@ -47,9 +46,8 @@ try event.Action = std::move(Action); event.Actor.ID = ActorId; - const auto now = std::chrono::system_clock::now().time_since_epoch(); - event.time = TimeSeconds.value_or(std::chrono::duration_cast(now).count()); - event.timeNano = TimeNano.value_or(std::chrono::duration_cast(now).count()); + event.time = TimeSeconds.value_or(static_cast( + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count())); Append(std::move(event)); } @@ -98,7 +96,7 @@ namespace { } // namespace Microsoft::WRL::ComPtr EventStore::CreateStream( - Microsoft::WRL::ComPtr Session, int64_t SinceTimeNano, int64_t UntilTimeNano, std::map> Filters) + Microsoft::WRL::ComPtr Session, uint64_t SinceTime, uint64_t UntilTime, std::map> Filters) { // Start the reader at the oldest event still buffered so it sees all the buffered history. uint64_t startSequenceNumber; @@ -109,7 +107,7 @@ Microsoft::WRL::ComPtr EventStore::CreateStream( Microsoft::WRL::ComPtr stream; THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize( - &stream, std::move(Session), this, startSequenceNumber, SinceTimeNano, UntilTimeNano, std::move(Filters))); + &stream, std::move(Session), this, startSequenceNumber, SinceTime, UntilTime, std::move(Filters))); return stream; } @@ -130,7 +128,7 @@ std::optional EventStore::GetLockHeld( return m_events[index]; } -std::optional EventStore::Get(uint64_t SequenceNumber, std::optional Until) +std::optional EventStore::Get(uint64_t SequenceNumber, std::optional Until) { std::unique_lock lock(m_lock); @@ -139,8 +137,10 @@ std::optional EventStore::Get(uint64_t if (Until.has_value()) { - // Round the deadline up to system_clock's coarser tick so the wait never ends early. - const auto deadline = std::chrono::ceil(Until.value()); + // The bound is Unix seconds and system_clock shares that epoch, so it doubles as a wall-clock + // deadline. Round it up to system_clock's finer tick so the wait never ends early. + const std::chrono::sys_seconds untilTime{std::chrono::seconds{static_cast(Until.value())}}; + const auto deadline = std::chrono::ceil(untilTime); if (!m_updated.wait_until(lock, deadline, ready)) { return std::nullopt; @@ -169,15 +169,15 @@ HRESULT EventStream::RuntimeClassInitialize( Microsoft::WRL::ComPtr Session, EventStore* Store, uint64_t StartSequenceNumber, - int64_t SinceTimeNano, - int64_t UntilTimeNano, + uint64_t SinceTime, + uint64_t UntilTime, std::map> Filters) { m_session = std::move(Session); m_store = Store; m_nextSequenceNumber = StartSequenceNumber; - m_since = ToEventTime(SinceTimeNano); - m_until = ToEventTime(UntilTimeNano); + m_since = ToTimeBound(SinceTime); + m_until = ToTimeBound(UntilTime); m_filters = std::move(Filters); return S_OK; } @@ -199,7 +199,7 @@ try ++m_nextSequenceNumber; - const EventTime eventTime{std::chrono::nanoseconds{event.value().timeNano}}; + const uint64_t eventTime = event.value().time; if (m_until.has_value() && eventTime > m_until.value()) { diff --git a/src/windows/wslcsession/EventStore.h b/src/windows/wslcsession/EventStore.h index 1bbdf4d78e..50141c4bd3 100644 --- a/src/windows/wslcsession/EventStore.h +++ b/src/windows/wslcsession/EventStore.h @@ -2,7 +2,6 @@ #pragma once -#include #include #include #include @@ -18,27 +17,17 @@ namespace wsl::windows::service::wslc { class WSLCSession; -using EventTime = std::chrono::sys_time; - 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, - std::optional TimeNano = std::nullopt) noexcept; + 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, - int64_t SinceTimeNano, - int64_t UntilTimeNano, - std::map> Filters); + Microsoft::WRL::ComPtr Session, uint64_t SinceTime, uint64_t UntilTime, std::map> Filters); - std::optional Get(uint64_t SequenceNumber, std::optional Until); + std::optional Get(uint64_t SequenceNumber, std::optional Until); void OnSessionTerminating(); @@ -64,8 +53,8 @@ class EventStream Microsoft::WRL::ComPtr Session, EventStore* Store, uint64_t StartSequenceNumber, - int64_t SinceTimeNano, - int64_t UntilTimeNano, + uint64_t SinceTime, + uint64_t UntilTime, std::map> Filters); IFACEMETHOD(GetNext)(_Outptr_result_z_ LPSTR* EventJson) override; @@ -74,8 +63,8 @@ class EventStream Microsoft::WRL::ComPtr m_session; EventStore* m_store = nullptr; - std::optional m_since; - std::optional m_until; + std::optional m_since; + std::optional m_until; std::map> m_filters; uint64_t m_nextSequenceNumber = 0; diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 8b77645af6..7832253778 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -558,7 +558,7 @@ WSLCContainerImpl::WSLCContainerImpl( 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, std::placeholders::_4))), + m_id, std::bind(&WSLCContainerImpl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))), m_state(InitialState), m_createdAt(CreatedAt), m_initProcessFlags(InitProcessFlags), @@ -868,7 +868,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt cleanup.release(); } -void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional exitCode, std::uint64_t eventTimeSeconds, std::uint64_t eventTimeNano) +void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional exitCode, std::uint64_t eventTime) { // We must release m_lock and m_stopLock before the wrapper's destructor calls // Disconnect(), so in-flight COM callers can drain from COMImplClass::m_callers. @@ -878,22 +878,22 @@ void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional exitCod // timestamps and the same ordering Docker observed. if (event == ContainerEvent::Start) { - m_eventStore.Record("container", "start", m_id, static_cast(eventTimeSeconds), static_cast(eventTimeNano)); + m_eventStore.Record("container", "start", m_id, eventTime); } else if (event == ContainerEvent::Kill) { - m_eventStore.Record("container", "kill", m_id, static_cast(eventTimeSeconds), static_cast(eventTimeNano)); + m_eventStore.Record("container", "kill", m_id, eventTime); } else if (event == ContainerEvent::Stop) { - m_eventStore.Record("container", "stop", m_id, static_cast(eventTimeSeconds), static_cast(eventTimeNano)); + m_eventStore.Record("container", "stop", m_id, eventTime); THROW_HR_IF(E_UNEXPECTED, !exitCode.has_value()); SetExitCode(exitCode.value()); std::unique_lock stopGuard{m_stopLock, std::try_to_lock}; - m_stopNotification.EventTime.store(eventTimeSeconds, std::memory_order_release); + m_stopNotification.EventTime.store(eventTime, std::memory_order_release); m_stopNotification.Event.SetEvent(); // If Stop() is already in flight, it will wake when the stop event is signaled and take care of cleanup. @@ -903,7 +903,7 @@ void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional exitCod } auto lock = m_lock.lock_exclusive(); - comWrapper = OnStopped(eventTimeSeconds); + comWrapper = OnStopped(eventTime); } else if (event == ContainerEvent::Destroy) { @@ -914,7 +914,7 @@ void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional exitCod if (m_state != WslcContainerStateDeleted) { - Transition(WslcContainerStateDeleted, eventTimeSeconds); + Transition(WslcContainerStateDeleted, eventTime); comWrapper = ReleaseResources(); } @@ -1775,6 +1775,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, @@ -1800,12 +1803,6 @@ std::unique_ptr WSLCContainerImpl::Create( deleteOnFailure.release(); - // The create event is not delivered on Docker's event stream the way start/kill/stop are, so - // record it directly now that the container is committed. - const int64_t createTimeNano = - std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); - eventStore.Record("container", "create", container->ID(), createTimeNano / 1'000'000'000, createTimeNano); - return container; } diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index 1ed9bb5297..e97e0aa5e1 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -161,7 +161,7 @@ class WSLCContainerImpl __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect DeleteExclusiveLockHeld(WSLCDeleteFlags Flags); void AllocateBridgedModePorts(); - void OnEvent(ContainerEvent event, std::optional exitCode, std::uint64_t eventTimeSeconds, std::uint64_t eventTimeNano); + void OnEvent(ContainerEvent event, std::optional exitCode, std::uint64_t eventTime); __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect ReleaseResources(); __requires_exclusive_lock_held(m_lock) void ReleaseRuntimeResources(); diff --git a/src/windows/wslcsession/WSLCProcessControl.cpp b/src/windows/wslcsession/WSLCProcessControl.cpp index 75f9e6a001..0c0e61eac9 100644 --- a/src/windows/wslcsession/WSLCProcessControl.cpp +++ b/src/windows/wslcsession/WSLCProcessControl.cpp @@ -112,9 +112,7 @@ DockerExecProcessControl::DockerExecProcessControl( m_id(Id), m_client(DockerClient), m_eventTrackingReference(EventTracker.RegisterExecStateUpdates( - Container.ID(), - Id, - std::bind(&DockerExecProcessControl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4))) + Container.ID(), Id, std::bind(&DockerExecProcessControl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))) { } @@ -166,7 +164,7 @@ void DockerExecProcessControl::SetExitCode(int ExitCode) } } -void DockerExecProcessControl::OnEvent(ContainerEvent Event, std::optional ExitCode, std::uint64_t /*eventTimeSeconds*/, std::uint64_t /*eventTimeNano*/) +void DockerExecProcessControl::OnEvent(ContainerEvent Event, std::optional ExitCode, std::uint64_t /*eventTime*/) { if (Event == ContainerEvent::ExecDied && !m_exitEvent.is_signaled()) { diff --git a/src/windows/wslcsession/WSLCProcessControl.h b/src/windows/wslcsession/WSLCProcessControl.h index bea4a1ef26..54cb2aca55 100644 --- a/src/windows/wslcsession/WSLCProcessControl.h +++ b/src/windows/wslcsession/WSLCProcessControl.h @@ -72,7 +72,7 @@ class DockerExecProcessControl : public WSLCProcessControl void SetExitCode(int ExitCode); private: - void OnEvent(ContainerEvent Event, std::optional ExitCode, std::uint64_t eventTimeSeconds, std::uint64_t eventTimeNano); + void OnEvent(ContainerEvent Event, std::optional ExitCode, std::uint64_t eventTime); mutable std::mutex m_lock; std::string m_id; diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index d4c5b51420..8d3b3ad6c5 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -788,8 +788,7 @@ void WSLCSession::StreamImageOperation(DockerHTTPClient::HTTPRequestContext& req void WSLCSession::OnImageCreated(const std::string& ImageNameOrId) noexcept try { - const auto dockerInspect = m_dockerClient->InspectImage(ImageNameOrId); - LOG_IF_FAILED(m_pluginNotifier->OnImageCreated(wsl::shared::ToJson(ConvertInspectImage(dockerInspect)).c_str())); + LOG_IF_FAILED(m_pluginNotifier->OnImageCreated(InspectImageLockHeld(ImageNameOrId).c_str())); } CATCH_LOG() @@ -3326,7 +3325,7 @@ try } CATCH_RETURN(); -HRESULT WSLCSession::GetEvents(LONGLONG SinceTimeNano, LONGLONG UntilTimeNano, const WSLCFilter* Filters, ULONG FiltersCount, IWSLCEventStream** Stream) +HRESULT WSLCSession::GetEvents(ULONGLONG SinceTime, ULONGLONG UntilTime, const WSLCFilter* Filters, ULONG FiltersCount, IWSLCEventStream** Stream) try { WSLCExecutionContext context(this); @@ -3336,10 +3335,10 @@ try *Stream = nullptr; // A non-zero until earlier than since describes a backwards, empty window. - RETURN_HR_IF(E_INVALIDARG, UntilTimeNano != 0 && SinceTimeNano > UntilTimeNano); + RETURN_HR_IF(E_INVALIDARG, UntilTime != 0 && SinceTime > UntilTime); auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); - auto stream = m_eventStore.CreateStream(Microsoft::WRL::ComPtr{this}, SinceTimeNano, UntilTimeNano, std::move(filters)); + auto stream = m_eventStore.CreateStream(Microsoft::WRL::ComPtr{this}, SinceTime, UntilTime, std::move(filters)); *Stream = stream.Detach(); return S_OK; diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index 90aa2d9be4..f293c06505 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -106,8 +106,8 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession // Event streaming. Opens a stream object that yields matching events one at a time via // IWSLCEventStream::GetNext. IFACEMETHOD(GetEvents)( - _In_ LONGLONG SinceTimeNano, - _In_ LONGLONG UntilTimeNano, + _In_ ULONGLONG SinceTime, + _In_ ULONGLONG UntilTime, _In_reads_opt_(FiltersCount) const WSLCFilter* Filters, _In_ ULONG FiltersCount, _Outptr_ IWSLCEventStream** Stream) override; diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index ab09a22eb3..31071ca7d9 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -5800,7 +5800,7 @@ class WSLCTests WSLC_TEST_METHOD(EventStream) { - auto now = [] { return duration_cast(system_clock::now().time_since_epoch()).count(); }; + 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. @@ -5819,7 +5819,7 @@ class WSLCTests }; // Verifies a drained event vector carries exactly the expected container events, in order: - // each is a container event for `actorId` whose seconds and nanosecond timestamps agree. + // each is a container event for `actorId`. auto verifyEvents = [&](const std::vector& events, const std::string& actorId, std::initializer_list expectedActions) { @@ -5831,17 +5831,14 @@ class WSLCTests VERIFY_ARE_EQUAL(std::string("container"), events[i].Type); VERIFY_ARE_EQUAL(std::string(action), events[i].Action); VERIFY_ARE_EQUAL(actorId, events[i].Actor.ID); - - // Each event carries Docker's seconds timestamp alongside the nanosecond one; they agree. - VERIFY_ARE_EQUAL(events[i].time, events[i].timeNano / 1'000'000'000); ++i; } }; // Run a container through its create/start/kill/stop lifecycle inside a bounded time window. - const LONGLONG since = now(); + const ULONGLONG since = now(); std::string id; - LONGLONG until = 0; + ULONGLONG until = 0; { WSLCContainerLauncher launcher("debian:latest", "wslc-test-events", {"sleep", "99999"}); auto container = launcher.Launch(*m_defaultSession); @@ -5867,12 +5864,12 @@ class WSLCTests verifyEvents(events, id, {"create", "start", "kill", "stop"}); // The whole lifecycle falls inside the requested window. - VERIFY_IS_TRUE(events[0].timeNano >= since); - VERIFY_IS_TRUE(events[3].timeNano <= until); + VERIFY_IS_TRUE(events[0].time >= since); + VERIFY_IS_TRUE(events[3].time <= until); // The start, kill and stop events share Docker's clock, so they are ordered. - VERIFY_IS_TRUE(events[2].timeNano >= events[1].timeNano); - VERIFY_IS_TRUE(events[3].timeNano >= events[2].timeNano); + VERIFY_IS_TRUE(events[2].time >= events[1].time); + VERIFY_IS_TRUE(events[3].time >= events[2].time); } // Each lifecycle action is independently selectable: an 'event=' filter, AND'd with @@ -5918,10 +5915,10 @@ class WSLCTests // A bounded window that closed before any event was recorded returns immediately with an // empty stream rather than blocking for events that can never arrive. { - constexpr LONGLONG oneSecondNano = 1'000'000'000; + constexpr ULONGLONG oneSecond = 1; WSLCFilter filter{"container", id.c_str()}; wil::com_ptr stream; - VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since - 2 * oneSecondNano, since - oneSecondNano, &filter, 1, &stream)); + VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since - 2 * oneSecond, since - oneSecond, &filter, 1, &stream)); VERIFY_IS_TRUE(drain(stream.get()).empty()); } @@ -5930,7 +5927,7 @@ class WSLCTests WSLC_TEST_METHOD(EventStreamSessionTerminationAbortsReader) { WSLCFilter filter{"type", "container"}; - const LONGLONG since = duration_cast(system_clock::now().time_since_epoch()).count(); + 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)); From 2f952f81db61d80b0c853189d87fcdc39a7c71ce Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 1 Jul 2026 15:03:29 -0700 Subject: [PATCH 5/9] Fine-tune some more --- localization/strings/en-US/Resources.resw | 4 + src/windows/wslcsession/EventStore.cpp | 141 +++++++++++++--------- src/windows/wslcsession/EventStore.h | 19 ++- src/windows/wslcsession/WSLCContainer.cpp | 2 + src/windows/wslcsession/WSLCSession.cpp | 3 - test/windows/WSLCTests.cpp | 45 +++---- 6 files changed, 125 insertions(+), 89 deletions(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 974bf046c8..ddf1d63dbd 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2282,6 +2282,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="{}"}{Locked="`since`"}{Locked="`until`"}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/src/windows/wslcsession/EventStore.cpp b/src/windows/wslcsession/EventStore.cpp index 91d9e02b38..d42fc206a1 100644 --- a/src/windows/wslcsession/EventStore.cpp +++ b/src/windows/wslcsession/EventStore.cpp @@ -3,9 +3,12 @@ #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 { @@ -98,26 +101,20 @@ namespace { Microsoft::WRL::ComPtr EventStore::CreateStream( Microsoft::WRL::ComPtr Session, uint64_t SinceTime, uint64_t UntilTime, std::map> Filters) { - // Start the reader at the oldest event still buffered so it sees all the buffered history. - uint64_t startSequenceNumber; - { - std::lock_guard lock(m_lock); - startSequenceNumber = m_firstSequenceNumber; - } + // 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, startSequenceNumber, SinceTime, UntilTime, std::move(Filters))); + 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) { - if (SequenceNumber < m_firstSequenceNumber) - { - THROW_HR(WSLC_E_EVENTS_LOST); - } + // 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()) @@ -128,31 +125,88 @@ std::optional EventStore::GetLockHeld( return m_events[index]; } -std::optional EventStore::Get(uint64_t SequenceNumber, std::optional Until) +bool EventStore::WaitForEvent(std::unique_lock& Lock, uint64_t SequenceNumber, std::optional Until) { - std::unique_lock lock(m_lock); - - // Checked before parking, so a termination that already latched is seen without sleeping. - const auto ready = [&] { return m_terminating || GetLockHeld(SequenceNumber).has_value(); }; + // 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 || GetLockHeld(SequenceNumber).has_value(); + }; if (Until.has_value()) { - // The bound is Unix seconds and system_clock shares that epoch, so it doubles as a wall-clock - // deadline. Round it up to system_clock's finer tick so the wait never ends early. - const std::chrono::sys_seconds untilTime{std::chrono::seconds{static_cast(Until.value())}}; - const auto deadline = std::chrono::ceil(untilTime); - if (!m_updated.wait_until(lock, deadline, ready)) + // 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 std::nullopt; + return false; } } else { - m_updated.wait(lock, ready); + m_updated.wait(Lock, ready); } THROW_HR_IF(E_ABORT, m_terminating); - return GetLockHeld(SequenceNumber); + 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() @@ -168,14 +222,12 @@ void EventStore::OnSessionTerminating() HRESULT EventStream::RuntimeClassInitialize( Microsoft::WRL::ComPtr Session, EventStore* Store, - uint64_t StartSequenceNumber, uint64_t SinceTime, uint64_t UntilTime, std::map> Filters) { m_session = std::move(Session); m_store = Store; - m_nextSequenceNumber = StartSequenceNumber; m_since = ToTimeBound(SinceTime); m_until = ToTimeBound(UntilTime); m_filters = std::move(Filters); @@ -188,37 +240,14 @@ try RETURN_HR_IF_NULL(E_POINTER, EventJson); *EventJson = nullptr; - // Read forward until an event lands inside the time window and matches the filters. - while (true) + const auto event = m_store->Get(m_nextSequenceNumber, m_since, m_until, m_filters); + if (!event.has_value()) { - const auto event = m_store->Get(m_nextSequenceNumber, m_until); - if (!event.has_value()) - { - return WSLC_E_EVENT_STREAM_FINISHED; - } - - ++m_nextSequenceNumber; - - const uint64_t eventTime = event.value().time; - - if (m_until.has_value() && eventTime > m_until.value()) - { - return WSLC_E_EVENT_STREAM_FINISHED; - } - - if (m_since.has_value() && eventTime < m_since.value()) - { - continue; - } - - if (!EventMatchesFilters(event.value(), m_filters)) - { - continue; - } - - *EventJson = wil::make_unique_ansistring(wsl::shared::ToJson(event.value()).c_str()).release(); - return S_OK; + return WSLC_E_EVENT_STREAM_FINISHED; } + + *EventJson = wil::make_unique_ansistring(wsl::shared::ToJson(event.value()).c_str()).release(); + return S_OK; } CATCH_RETURN(); diff --git a/src/windows/wslcsession/EventStore.h b/src/windows/wslcsession/EventStore.h index 50141c4bd3..8de506e840 100644 --- a/src/windows/wslcsession/EventStore.h +++ b/src/windows/wslcsession/EventStore.h @@ -27,13 +27,27 @@ class EventStore Microsoft::WRL::ComPtr CreateStream( Microsoft::WRL::ComPtr Session, uint64_t SinceTime, uint64_t UntilTime, std::map> Filters); - std::optional Get(uint64_t SequenceNumber, std::optional Until); + // 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; @@ -52,7 +66,6 @@ class EventStream HRESULT RuntimeClassInitialize( Microsoft::WRL::ComPtr Session, EventStore* Store, - uint64_t StartSequenceNumber, uint64_t SinceTime, uint64_t UntilTime, std::map> Filters); @@ -67,7 +80,7 @@ class EventStream std::optional m_until; std::map> m_filters; - uint64_t m_nextSequenceNumber = 0; + std::optional m_nextSequenceNumber; }; } // namespace wsl::windows::service::wslc diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 7832253778..067018be95 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -907,6 +907,8 @@ void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional exitCod } else if (event == ContainerEvent::Destroy) { + m_eventStore.Record("container", "destroy", m_id, eventTime); + WI_ASSERT(!m_destroyEvent.is_signaled()); m_destroyEvent.SetEvent(); diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 8d3b3ad6c5..00715cdfb5 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -3334,9 +3334,6 @@ try *Stream = nullptr; - // A non-zero until earlier than since describes a backwards, empty window. - RETURN_HR_IF(E_INVALIDARG, UntilTime != 0 && SinceTime > UntilTime); - auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); auto stream = m_eventStore.CreateStream(Microsoft::WRL::ComPtr{this}, SinceTime, UntilTime, std::move(filters)); diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 31071ca7d9..909c4731e5 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -5818,20 +5818,25 @@ class WSLCTests return events; }; - // Verifies a drained event vector carries exactly the expected container events, in order: - // each is a container event for `actorId`. + // Verifies the given events match the expected actions in order for a given actor. auto verifyEvents = [&](const std::vector& events, const std::string& actorId, - std::initializer_list expectedActions) { + const std::vector& expectedActions) { VERIFY_ARE_EQUAL(events.size(), expectedActions.size()); - size_t i = 0; - for (const auto action : expectedActions) + for (size_t i = 0; i < expectedActions.size(); ++i) { - VERIFY_ARE_EQUAL(std::string("container"), events[i].Type); - VERIFY_ARE_EQUAL(std::string(action), events[i].Action); - VERIFY_ARE_EQUAL(actorId, events[i].Actor.ID); - ++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); + } } }; @@ -5861,15 +5866,11 @@ class WSLCTests VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since, until, &filter, 1, &stream)); auto events = drain(stream.get()); - verifyEvents(events, id, {"create", "start", "kill", "stop"}); + 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[3].time <= until); - - // The start, kill and stop events share Docker's clock, so they are ordered. - VERIFY_IS_TRUE(events[2].time >= events[1].time); - VERIFY_IS_TRUE(events[3].time >= events[2].time); + VERIFY_IS_TRUE(events[4].time <= until); } // Each lifecycle action is independently selectable: an 'event=' filter, AND'd with @@ -5909,18 +5910,8 @@ class WSLCTests // 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(until, since, nullptr, 0, &stream)); - } - - // A bounded window that closed before any event was recorded returns immediately with an - // empty stream rather than blocking for events that can never arrive. - { - constexpr ULONGLONG oneSecond = 1; - WSLCFilter filter{"container", id.c_str()}; - wil::com_ptr stream; - VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since - 2 * oneSecond, since - oneSecond, &filter, 1, &stream)); - - VERIFY_IS_TRUE(drain(stream.get()).empty()); + VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->GetEvents(since + 1, since, nullptr, 0, &stream)); + ValidateCOMErrorMessage(wsl::shared::Localization::MessageWslcEventsInvalidTimeWindow(since + 1, since)); } } From ba0606beb2fe4404149ea134dd428652297fd42e Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 1 Jul 2026 16:23:55 -0700 Subject: [PATCH 6/9] Address feedback --- src/windows/wslcsession/EventStore.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/windows/wslcsession/EventStore.cpp b/src/windows/wslcsession/EventStore.cpp index d42fc206a1..5e41b6fcb4 100644 --- a/src/windows/wslcsession/EventStore.cpp +++ b/src/windows/wslcsession/EventStore.cpp @@ -129,9 +129,7 @@ bool EventStore::WaitForEvent(std::unique_lock& Lock, uint64_t Seque { // 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 || GetLockHeld(SequenceNumber).has_value(); - }; + const auto ready = [&] { return m_terminating || SequenceNumber < m_firstSequenceNumber + m_events.size(); }; if (Until.has_value()) { From 960eba48a459f4efda330d303f2d280a478f9fce Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 1 Jul 2026 16:32:11 -0700 Subject: [PATCH 7/9] Address feedback --- localization/strings/en-US/Resources.resw | 2 +- src/windows/service/inc/wslc.idl | 5 +---- test/windows/WSLCTests.cpp | 4 +--- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index dbdae67551..60cdd00ed5 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2293,7 +2293,7 @@ For privacy information about this product please visit https://aka.ms/privacy.< `since` time ({}) cannot be after `until` time ({}) - {FixedPlaceholder="{}"}{Locked="`since`"}{Locked="`until`"}Command line arguments, file names and string inserts should not be translated + {Locked="`since`"}{Locked="`until`"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated Path is not absolute: '{}' diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 52b73346e8..416ded4218 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -569,10 +569,7 @@ typedef struct _WSLCListContainersOptions 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, which matches - // docker's `events --format json` shape: {"Type","Action","Actor":{"ID","Attributes":{...}},"time","timeNano"}. - // JSON rather than a struct so future fields (Actor.Attributes, exec/image specifics) need no - // IDL change and the whole event is one allocation the caller frees with a single CoTaskMemFree. + // then returns the event as a JSON object following the wslc_schema::Event format. HRESULT GetNext([out, string] LPSTR* EventJson); } diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 6b2326cdb5..dcd2aebf22 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -6059,7 +6059,6 @@ class WSLCTests // Run a container through its create/start/kill/stop lifecycle inside a bounded time window. const ULONGLONG since = now(); std::string id; - ULONGLONG until = 0; { WSLCContainerLauncher launcher("debian:latest", "wslc-test-events", {"sleep", "99999"}); auto container = launcher.Launch(*m_defaultSession); @@ -6070,14 +6069,13 @@ class WSLCTests // 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); - - 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()}; + ULONGLONG until = now(); wil::com_ptr stream; VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since, until, &filter, 1, &stream)); From 6ba4dac25c92d3179dc61775981354c38e830a03 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Thu, 2 Jul 2026 11:02:36 -0700 Subject: [PATCH 8/9] Fix syntax --- test/windows/WSLCTests.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index dcd2aebf22..35a6c4c621 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -6071,11 +6071,12 @@ class WSLCTests 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()}; - ULONGLONG until = now(); wil::com_ptr stream; VERIFY_SUCCEEDED(m_defaultSession->GetEvents(since, until, &filter, 1, &stream)); From b374243996b2efb7948a113115c700edb5786205 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 6 Jul 2026 11:21:20 -0700 Subject: [PATCH 9/9] Address feedback --- src/windows/wslcsession/EventStore.cpp | 2 +- src/windows/wslcsession/EventStore.h | 2 +- src/windows/wslcsession/WSLCContainer.cpp | 42 ++++++++++++++++------- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/windows/wslcsession/EventStore.cpp b/src/windows/wslcsession/EventStore.cpp index 5e41b6fcb4..f3954bf7a3 100644 --- a/src/windows/wslcsession/EventStore.cpp +++ b/src/windows/wslcsession/EventStore.cpp @@ -41,7 +41,7 @@ void EventStore::Append(wsl::windows::common::wslc_schema::Event Event) m_updated.notify_all(); } -void EventStore::Record(std::string Type, std::string Action, const std::string& ActorId, std::optional TimeSeconds) noexcept +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; diff --git a/src/windows/wslcsession/EventStore.h b/src/windows/wslcsession/EventStore.h index 8de506e840..9572efc5dc 100644 --- a/src/windows/wslcsession/EventStore.h +++ b/src/windows/wslcsession/EventStore.h @@ -22,7 +22,7 @@ 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; + 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); diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index b0c4c319c9..8694728f94 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". @@ -840,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)) { @@ -872,7 +896,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt portCleanup.release(); volumeCleanup.release(); - Transition(WslcContainerStateRunning); + Transition(WslcContainerStateRunning, startedAtSeconds); cleanup.release(); } @@ -882,20 +906,12 @@ void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional exitCod // Disconnect(), so in-flight COM callers can drain from COMImplClass::m_callers. unique_com_disconnect comWrapper; - // Record lifecycle events here, driven by Docker's own events, so the store gets Docker's - // timestamps and the same ordering Docker observed. - if (event == ContainerEvent::Start) - { - m_eventStore.Record("container", "start", m_id, eventTime); - } - else if (event == ContainerEvent::Kill) + if (event == ContainerEvent::Kill) { m_eventStore.Record("container", "kill", m_id, eventTime); } else if (event == ContainerEvent::Stop) { - m_eventStore.Record("container", "stop", m_id, eventTime); - THROW_HR_IF(E_UNEXPECTED, !exitCode.has_value()); SetExitCode(exitCode.value()); @@ -915,8 +931,6 @@ void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional exitCod } else if (event == ContainerEvent::Destroy) { - m_eventStore.Record("container", "destroy", m_id, eventTime); - WI_ASSERT(!m_destroyEvent.is_signaled()); m_destroyEvent.SetEvent(); @@ -2203,6 +2217,8 @@ __requires_lock_held(m_lock) void WSLCContainerImpl::Transition(WSLCContainerSta // 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) :