diff --git a/src/windows/inc/docker_schema.h b/src/windows/inc/docker_schema.h index 7cb7cd318..78fdd7de8 100644 --- a/src/windows/inc/docker_schema.h +++ b/src/windows/inc/docker_schema.h @@ -273,8 +273,9 @@ struct InspectContainer ContainerInspectState State; ContainerConfig Config; HostConfig HostConfig; + std::vector Mounts; - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectContainer, Id, Name, Created, Image, State, Config, HostConfig); + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(InspectContainer, Id, Name, Created, Image, State, Config, HostConfig, Mounts); }; struct InspectExec diff --git a/src/windows/wslcsession/CMakeLists.txt b/src/windows/wslcsession/CMakeLists.txt index 70dbaf3f9..f277e3052 100644 --- a/src/windows/wslcsession/CMakeLists.txt +++ b/src/windows/wslcsession/CMakeLists.txt @@ -20,16 +20,17 @@ set(SOURCES # Volume management WSLCVhdVolume.cpp WSLCGuestVolume.cpp + WSLCVolumes.cpp # Supporting classes - ContainerEventTracker.cpp + DockerEventTracker.cpp DockerHTTPClient.cpp IORelay.cpp ServiceProcessLauncher.cpp ) set(HEADERS - ContainerEventTracker.h + DockerEventTracker.h DockerHTTPClient.h IORelay.h ServiceProcessLauncher.h @@ -44,6 +45,7 @@ set(HEADERS WSLCVirtualMachine.h WSLCVhdVolume.h WSLCGuestVolume.h + WSLCVolumes.h IWSLCVolume.h WSLCVolumeMetadata.h) diff --git a/src/windows/wslcsession/ContainerEventTracker.cpp b/src/windows/wslcsession/ContainerEventTracker.cpp deleted file mode 100644 index d84ba7b6f..000000000 --- a/src/windows/wslcsession/ContainerEventTracker.cpp +++ /dev/null @@ -1,194 +0,0 @@ -/*++ - -Copyright (c) Microsoft. All rights reserved. - -Module Name: - - ContainerEventTracker.cpp - -Abstract: - - Contains the implementation of ContainerEventTracker. - ---*/ -#include "precomp.h" -#include "ContainerEventTracker.h" -#include "WSLCVirtualMachine.h" -#include - -using wsl::windows::common::relay::MultiHandleWait; -using wsl::windows::service::wslc::ContainerEventTracker; -using wsl::windows::service::wslc::DockerHTTPClient; -using wsl::windows::service::wslc::WSLCVirtualMachine; - -ContainerEventTracker::ContainerTrackingReference::ContainerTrackingReference(ContainerEventTracker* tracker, size_t id) noexcept : - m_tracker(tracker), m_id(id) -{ -} - -ContainerEventTracker::ContainerTrackingReference& ContainerEventTracker::ContainerTrackingReference::operator=( - ContainerEventTracker::ContainerTrackingReference&& other) noexcept -{ - Reset(); - m_id = other.m_id; - m_tracker = other.m_tracker; - - other.m_tracker = nullptr; - other.m_id = {}; - - return *this; -} - -void ContainerEventTracker::ContainerTrackingReference::Reset() noexcept -{ - if (m_tracker != nullptr) - { - m_tracker->UnregisterContainerStateUpdates(m_id); - m_tracker = nullptr; - m_id = {}; - } -} - -ContainerEventTracker::ContainerTrackingReference::ContainerTrackingReference(ContainerTrackingReference&& other) noexcept : - m_id(other.m_id), m_tracker(other.m_tracker) -{ - other.m_tracker = nullptr; - other.m_id = {}; -} - -ContainerEventTracker::ContainerTrackingReference::~ContainerTrackingReference() noexcept -{ - Reset(); -} - -ContainerEventTracker::ContainerEventTracker(DockerHTTPClient& dockerClient, ULONG sessionId, IORelay& relay) : - m_sessionId(sessionId) -{ - auto onChunk = [this](const gsl::span& buffer) { - if (!buffer.empty()) // docker inserts empty lines between events, skip those. - { - try - { - OnEvent(std::string_view(buffer.data(), buffer.size())); - } - catch (...) - { - WSL_LOG( - "DockerEventParseError", - TraceLoggingValue(buffer.data(), "Data"), - TraceLoggingValue(wil::ResultFromCaughtException(), "Error"), - TraceLoggingValue(m_sessionId, "SessionId")); - } - } - }; - - auto socket = dockerClient.MonitorEvents(); - - relay.AddHandle(std::make_unique(std::move(socket), std::move(onChunk))); -} - -ContainerEventTracker::~ContainerEventTracker() -{ - // N.B. No callback should be left when the tracker is destroyed. - WI_ASSERT(m_callbacks.empty()); -} - -void ContainerEventTracker::OnEvent(const std::string_view& event) -{ - WSL_LOG( - "DockerEvent", - TraceLoggingCountedString( - event.data(), static_cast(std::min(event.size(), static_cast(USHRT_MAX))), "Data"), - TraceLoggingValue(m_sessionId, "SessionId")); - - static std::map events{ - {"start", ContainerEvent::Start}, {"die", ContainerEvent::Stop}, {"exec_die", ContainerEvent::ExecDied}}; - - auto parsed = nlohmann::json::parse(event); - - auto action = parsed.find("Action"); - auto actor = parsed.find("Actor"); - - THROW_HR_IF_MSG( - E_INVALIDARG, - action == parsed.end() || actor == parsed.end(), - "Failed to parse json: %.*hs", - static_cast(event.size()), - event.data()); - - auto it = events.find(action->get()); - if (it == events.end()) - { - return; // Event is not tracked, dropped. - } - - auto id = actor->find("ID"); - THROW_HR_IF_MSG(E_INVALIDARG, id == actor->end(), "Failed to parse json: %.*hs", static_cast(event.size()), event.data()); - - auto containerId = id->get(); - - std::optional exitCode; - std::optional execId; - auto attributes = actor->find("Attributes"); - if (attributes != actor->end()) - { - auto exitCodeEntry = attributes->find("exitCode"); - if (exitCodeEntry != attributes->end()) - { - exitCode = std::stoi(exitCodeEntry->get()); - } - - auto execIdEntry = attributes->find("execID"); - if (execIdEntry != attributes->end()) - { - execId = execIdEntry->get(); - } - } - - 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::lock_guard lock{m_lock}; - - for (const auto& e : m_callbacks) - { - if (e.ContainerId == containerId && (!e.ExecId.has_value() || e.ExecId == execId)) - { - e.Callback(it->second, exitCode, eventTime); - } - } -} - -ContainerEventTracker::ContainerTrackingReference ContainerEventTracker::RegisterContainerStateUpdates( - const std::string& ContainerId, ContainerStateChangeCallback&& Callback) noexcept -{ - std::lock_guard lock{m_lock}; - - auto id = m_callbackId++; - m_callbacks.emplace_back(id, ContainerId, std::optional{}, std::move(Callback)); - - return ContainerTrackingReference{this, id}; -} - -ContainerEventTracker::ContainerTrackingReference ContainerEventTracker::RegisterExecStateUpdates( - const std::string& ContainerId, const std::string& ExecId, ContainerStateChangeCallback&& Callback) noexcept -{ - std::lock_guard lock{m_lock}; - - auto id = m_callbackId++; - m_callbacks.emplace_back(id, ContainerId, ExecId, std::move(Callback)); - - return ContainerTrackingReference{this, id}; -} - -void ContainerEventTracker::UnregisterContainerStateUpdates(size_t Id) noexcept -{ - std::lock_guard lock{m_lock}; - - auto remove = std::ranges::remove_if(m_callbacks, [Id](auto& entry) { return entry.CallbackId == Id; }); - WI_ASSERT(remove.size() == 1); - - m_callbacks.erase(remove.begin(), remove.end()); -} \ No newline at end of file diff --git a/src/windows/wslcsession/ContainerEventTracker.h b/src/windows/wslcsession/ContainerEventTracker.h deleted file mode 100644 index b2e05308f..000000000 --- a/src/windows/wslcsession/ContainerEventTracker.h +++ /dev/null @@ -1,86 +0,0 @@ -/*++ - -Copyright (c) Microsoft. All rights reserved. - -Module Name: - - ContainerEventTracker.h - -Abstract: - - Contains the definition for ContainerEventTracker. - ---*/ - -#pragma once - -#include "DockerHTTPClient.h" -#include "IORelay.h" - -namespace wsl::windows::service::wslc { - -class WSLCVirtualMachine; - -enum class ContainerEvent -{ - Create, - Start, - Stop, - Exit, - Destroy, - ExecDied -}; - -class ContainerEventTracker -{ -public: - NON_COPYABLE(ContainerEventTracker); - NON_MOVABLE(ContainerEventTracker); - - struct ContainerTrackingReference - { - NON_COPYABLE(ContainerTrackingReference); - - ContainerTrackingReference() = default; - ContainerTrackingReference(ContainerEventTracker* tracker, size_t id) noexcept; - ContainerTrackingReference(ContainerTrackingReference&& other) noexcept; - ~ContainerTrackingReference() noexcept; - - ContainerTrackingReference& operator=(ContainerTrackingReference&&) noexcept; - - void Reset() noexcept; - - size_t m_id; - ContainerEventTracker* m_tracker = nullptr; - }; - - using ContainerStateChangeCallback = std::function, std::uint64_t)>; - - ContainerEventTracker(DockerHTTPClient& dockerClient, ULONG sessionId, IORelay& relay); - ~ContainerEventTracker(); - - void Stop(); - - ContainerTrackingReference RegisterContainerStateUpdates(const std::string& ContainerId, ContainerStateChangeCallback&& Callback) noexcept; - ContainerTrackingReference RegisterExecStateUpdates(const std::string& ContainerId, const std::string& ExecId, ContainerStateChangeCallback&& Callback) noexcept; - void UnregisterContainerStateUpdates(size_t Id) noexcept; - -private: - void OnEvent(const std::string_view& event); - void Run(wil::unique_socket&& Socket); - - struct Callback - { - size_t CallbackId; - std::string ContainerId; - std::optional ExecId; - ContainerStateChangeCallback Callback; - }; - - std::vector m_callbacks; - - ULONG m_sessionId{}; - std::recursive_mutex m_lock; - std::atomic m_callbackId{0}; -}; -} // namespace wsl::windows::service::wslc \ No newline at end of file diff --git a/src/windows/wslcsession/DockerEventTracker.cpp b/src/windows/wslcsession/DockerEventTracker.cpp new file mode 100644 index 000000000..64d836239 --- /dev/null +++ b/src/windows/wslcsession/DockerEventTracker.cpp @@ -0,0 +1,298 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + DockerEventTracker.cpp + +Abstract: + + Contains the implementation of DockerEventTracker. + +--*/ +#include "precomp.h" +#include "DockerEventTracker.h" +#include "WSLCVirtualMachine.h" +#include + +using wsl::windows::service::wslc::DockerEventTracker; +using wsl::windows::service::wslc::DockerHTTPClient; +using wsl::windows::service::wslc::WSLCVirtualMachine; + +DockerEventTracker::EventTrackingReference::EventTrackingReference(DockerEventTracker* tracker, size_t id) noexcept : + m_tracker(tracker), m_id(id) +{ +} + +DockerEventTracker::EventTrackingReference& DockerEventTracker::EventTrackingReference::operator=(DockerEventTracker::EventTrackingReference&& other) noexcept +{ + Reset(); + m_id = other.m_id; + m_tracker = other.m_tracker; + + other.m_tracker = nullptr; + other.m_id = {}; + + return *this; +} + +void DockerEventTracker::EventTrackingReference::Reset() noexcept +{ + if (m_tracker != nullptr) + { + m_tracker->UnregisterCallback(m_id); + m_tracker = nullptr; + m_id = {}; + } +} + +DockerEventTracker::EventTrackingReference::EventTrackingReference(EventTrackingReference&& other) noexcept : + m_id(other.m_id), m_tracker(other.m_tracker) +{ + other.m_tracker = nullptr; + other.m_id = {}; +} + +DockerEventTracker::EventTrackingReference::~EventTrackingReference() noexcept +{ + Reset(); +} + +DockerEventTracker::DockerEventTracker(DockerHTTPClient& dockerClient, ULONG sessionId, IORelay& relay) : m_sessionId(sessionId) +{ + auto onChunk = [this](const gsl::span& buffer) { + if (!buffer.empty()) // docker inserts empty lines between events, skip those. + { + try + { + OnEvent(std::string_view(buffer.data(), buffer.size())); + } + catch (...) + { + WSL_LOG( + "DockerEventParseError", + TraceLoggingValue(buffer.data(), "Data"), + TraceLoggingValue(wil::ResultFromCaughtException(), "Error"), + TraceLoggingValue(m_sessionId, "SessionId")); + } + } + }; + + auto socket = dockerClient.MonitorEvents(); + + relay.AddHandle(std::make_unique(std::move(socket), std::move(onChunk))); +} + +DockerEventTracker::~DockerEventTracker() +{ + // N.B. No callback should be left when the tracker is destroyed. + WI_ASSERT(m_containerCallbacks.empty()); + WI_ASSERT(m_volumeCallbacks.empty()); +} + +void DockerEventTracker::OnEvent(const std::string_view& event) +{ + WSL_LOG( + "DockerEvent", + TraceLoggingCountedString( + event.data(), static_cast(std::min(event.size(), static_cast(USHRT_MAX))), "Data"), + TraceLoggingValue(m_sessionId, "SessionId")); + + auto parsed = nlohmann::json::parse(event); + + auto action = parsed.find("Action"); + THROW_HR_IF_MSG(E_INVALIDARG, action == parsed.end(), "Failed to parse json: %.*hs", static_cast(event.size()), event.data()); + + 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(); + + auto actionStr = action->get(); + + // Track object lifecycle for WaitForObjectCreated/WaitForObjectDestroyed. + auto actor = parsed.find("Actor"); + if (actor != parsed.end()) + { + auto id = actor->find("ID"); + if (id != actor->end()) + { + auto objectId = id->get(); + if (actionStr == "create") + { + std::lock_guard lock{m_lock}; + m_createdObjects.insert(objectId); + m_objectStateChanged.notify_all(); + } + else if (actionStr == "destroy") + { + std::lock_guard lock{m_lock}; + m_createdObjects.erase(objectId); + m_objectStateChanged.notify_all(); + } + } + } + + // Route events by Type field. Docker uses "container", "volume", "network", etc. + auto type = parsed.find("Type"); + std::string typeStr = (type != parsed.end()) ? type->get() : "container"; + + if (typeStr == "container") + { + OnContainerEvent(parsed, actionStr, eventTime); + } + else if (typeStr == "volume") + { + OnVolumeEvent(parsed, actionStr, eventTime); + } +} + +void DockerEventTracker::OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime) +{ + static std::map events{ + {"start", ContainerEvent::Start}, {"die", ContainerEvent::Stop}, {"exec_die", ContainerEvent::ExecDied}}; + + auto actor = parsed.find("Actor"); + THROW_HR_IF_MSG(E_INVALIDARG, actor == parsed.end(), "Missing Actor in container event"); + + auto id = actor->find("ID"); + THROW_HR_IF_MSG(E_INVALIDARG, id == actor->end(), "Missing Actor.ID in container event"); + + auto containerId = id->get(); + + auto it = events.find(action); + if (it == events.end()) + { + return; // Event is not tracked, dropped. + } + + std::optional exitCode; + std::optional execId; + auto attributes = actor->find("Attributes"); + if (attributes != actor->end()) + { + auto exitCodeEntry = attributes->find("exitCode"); + if (exitCodeEntry != attributes->end()) + { + exitCode = std::stoi(exitCodeEntry->get()); + } + + auto execIdEntry = attributes->find("execID"); + if (execIdEntry != attributes->end()) + { + execId = execIdEntry->get(); + } + } + + std::lock_guard lock{m_lock}; + + for (const auto& e : m_containerCallbacks) + { + if (e.ContainerId == containerId && (!e.ExecId.has_value() || e.ExecId == execId)) + { + e.Callback(it->second, exitCode, eventTime); + } + } +} + +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}}; + + auto it = events.find(action); + if (it == events.end()) + { + return; // Event is not tracked, dropped. + } + + auto actor = parsed.find("Actor"); + THROW_HR_IF_MSG(E_INVALIDARG, actor == parsed.end(), "Missing Actor in volume event"); + + auto id = actor->find("ID"); + THROW_HR_IF_MSG(E_INVALIDARG, id == actor->end(), "Missing Actor.ID in volume event"); + + auto volumeName = id->get(); + + std::lock_guard lock{m_lock}; + + for (const auto& e : m_volumeCallbacks) + { + e.Callback(volumeName, it->second, eventTime); + } +} + +void DockerEventTracker::WaitForObjectCreated(const std::string& ObjectId) +{ + constexpr auto c_timeout = std::chrono::seconds{60}; + + std::unique_lock lock{m_lock}; + THROW_HR_IF_MSG( + HRESULT_FROM_WIN32(ERROR_TIMEOUT), + !m_objectStateChanged.wait_for(lock, c_timeout, [&]() { return m_createdObjects.contains(ObjectId); }), + "Timed out waiting for Docker create event for object '%hs'", + ObjectId.c_str()); +} + +void DockerEventTracker::WaitForObjectDestroyed(const std::string& ObjectId) +{ + constexpr auto c_timeout = std::chrono::seconds{60}; + + std::unique_lock lock{m_lock}; + THROW_HR_IF_MSG( + HRESULT_FROM_WIN32(ERROR_TIMEOUT), + !m_objectStateChanged.wait_for(lock, c_timeout, [&]() { return !m_createdObjects.contains(ObjectId); }), + "Timed out waiting for Docker destroy event for object '%hs'", + ObjectId.c_str()); +} + +DockerEventTracker::EventTrackingReference DockerEventTracker::RegisterContainerStateUpdates( + const std::string& ContainerId, ContainerStateChangeCallback&& Callback) noexcept +{ + std::lock_guard lock{m_lock}; + + auto id = m_callbackId++; + m_containerCallbacks.emplace_back(id, ContainerId, std::optional{}, std::move(Callback)); + + return EventTrackingReference{this, id}; +} + +DockerEventTracker::EventTrackingReference DockerEventTracker::RegisterExecStateUpdates( + const std::string& ContainerId, const std::string& ExecId, ContainerStateChangeCallback&& Callback) noexcept +{ + std::lock_guard lock{m_lock}; + + auto id = m_callbackId++; + m_containerCallbacks.emplace_back(id, ContainerId, ExecId, std::move(Callback)); + + return EventTrackingReference{this, id}; +} + +DockerEventTracker::EventTrackingReference DockerEventTracker::RegisterVolumeUpdates(VolumeEventCallback&& Callback) noexcept +{ + std::lock_guard lock{m_lock}; + + auto id = m_callbackId++; + m_volumeCallbacks.emplace_back(id, std::move(Callback)); + + return EventTrackingReference{this, id}; +} + +void DockerEventTracker::UnregisterCallback(size_t Id) noexcept +{ + std::lock_guard lock{m_lock}; + + // Try container callbacks first. + auto containerRemove = std::ranges::remove_if(m_containerCallbacks, [Id](auto& entry) { return entry.CallbackId == Id; }); + if (!containerRemove.empty()) + { + WI_ASSERT(containerRemove.size() == 1); + m_containerCallbacks.erase(containerRemove.begin(), containerRemove.end()); + return; + } + + // Then volume callbacks. + auto volumeRemove = std::ranges::remove_if(m_volumeCallbacks, [Id](auto& entry) { return entry.CallbackId == Id; }); + WI_ASSERT(volumeRemove.size() == 1); + m_volumeCallbacks.erase(volumeRemove.begin(), volumeRemove.end()); +} \ No newline at end of file diff --git a/src/windows/wslcsession/DockerEventTracker.h b/src/windows/wslcsession/DockerEventTracker.h new file mode 100644 index 000000000..fe0719b8e --- /dev/null +++ b/src/windows/wslcsession/DockerEventTracker.h @@ -0,0 +1,106 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + DockerEventTracker.h + +Abstract: + + Contains the definition for DockerEventTracker. + +--*/ + +#pragma once + +#include "DockerHTTPClient.h" +#include "IORelay.h" + +namespace wsl::windows::service::wslc { + +class WSLCVirtualMachine; + +enum class ContainerEvent +{ + Create, + Start, + Stop, + Exit, + Destroy, + ExecDied +}; + +enum class VolumeEvent +{ + Create, + Destroy +}; + +class DockerEventTracker +{ +public: + NON_COPYABLE(DockerEventTracker); + NON_MOVABLE(DockerEventTracker); + + struct EventTrackingReference + { + NON_COPYABLE(EventTrackingReference); + + EventTrackingReference() = default; + EventTrackingReference(DockerEventTracker* tracker, size_t id) noexcept; + EventTrackingReference(EventTrackingReference&& other) noexcept; + ~EventTrackingReference() noexcept; + + EventTrackingReference& operator=(EventTrackingReference&&) noexcept; + + void Reset() noexcept; + + size_t m_id; + DockerEventTracker* m_tracker = nullptr; + }; + + using ContainerStateChangeCallback = std::function, std::uint64_t)>; + using VolumeEventCallback = std::function; + + DockerEventTracker(DockerHTTPClient& dockerClient, ULONG sessionId, IORelay& relay); + ~DockerEventTracker(); + + EventTrackingReference RegisterContainerStateUpdates(const std::string& ContainerId, ContainerStateChangeCallback&& Callback) noexcept; + EventTrackingReference RegisterExecStateUpdates(const std::string& ContainerId, const std::string& ExecId, ContainerStateChangeCallback&& Callback) noexcept; + EventTrackingReference RegisterVolumeUpdates(VolumeEventCallback&& Callback) noexcept; + void UnregisterCallback(size_t Id) noexcept; + + void WaitForObjectCreated(const std::string& ObjectId); + void WaitForObjectDestroyed(const std::string& ObjectId); + +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); + + struct ContainerCallback + { + size_t CallbackId; + std::string ContainerId; + std::optional ExecId; + ContainerStateChangeCallback Callback; + }; + + struct VolumeCallback + { + size_t CallbackId; + VolumeEventCallback Callback; + }; + + std::vector m_containerCallbacks; + std::vector m_volumeCallbacks; + + std::unordered_set m_createdObjects; + std::condition_variable_any m_objectStateChanged; + + ULONG m_sessionId{}; + std::recursive_mutex m_lock; + std::atomic m_callbackId{0}; +}; +} // namespace wsl::windows::service::wslc \ No newline at end of file diff --git a/src/windows/wslcsession/DockerHTTPClient.cpp b/src/windows/wslcsession/DockerHTTPClient.cpp index 35789c59e..5deda7577 100644 --- a/src/windows/wslcsession/DockerHTTPClient.cpp +++ b/src/windows/wslcsession/DockerHTTPClient.cpp @@ -427,6 +427,11 @@ docker_schema::Volume DockerHTTPClient::CreateVolume(const docker_schema::Create return Transaction(verb::post, URL::Create("/volumes/create"), Request); } +docker_schema::Volume DockerHTTPClient::InspectVolume(const std::string& Name) +{ + return Transaction(verb::get, URL::Create("/volumes/{}", Name)); +} + void DockerHTTPClient::RemoveVolume(const std::string& Name) { Transaction(verb::delete_, URL::Create("/volumes/{}", Name)); diff --git a/src/windows/wslcsession/DockerHTTPClient.h b/src/windows/wslcsession/DockerHTTPClient.h index 72d63d715..e8484fc40 100644 --- a/src/windows/wslcsession/DockerHTTPClient.h +++ b/src/windows/wslcsession/DockerHTTPClient.h @@ -145,6 +145,7 @@ class DockerHTTPClient // Volume management. common::docker_schema::Volume CreateVolume(const common::docker_schema::CreateVolume& Request); + common::docker_schema::Volume InspectVolume(const std::string& Name); void RemoveVolume(const std::string& Name); std::vector ListVolumes(); diff --git a/src/windows/wslcsession/IWSLCVolume.h b/src/windows/wslcsession/IWSLCVolume.h index b49b24ec5..4d8e1629a 100644 --- a/src/windows/wslcsession/IWSLCVolume.h +++ b/src/windows/wslcsession/IWSLCVolume.h @@ -39,6 +39,12 @@ class IWSLCVolume // (e.g. detach/delete the VHD for VHD volumes). Throws on failure. virtual void Delete() = 0; + // Called when Docker has already destroyed the volume (e.g. container delete with -v). + // Releases any host-side resources without contacting Docker. Default is a no-op. + virtual void OnDeleted() + { + } + // Returns a JSON string for the COM-facing InspectVolume result. virtual std::string Inspect() const = 0; diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 65a0306db..c3c1cf7b3 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -21,6 +21,7 @@ Module Name: #include "WSLCContainer.h" #include "WSLCProcess.h" #include "WSLCProcessIO.h" +#include "WSLCVolumes.h" using wsl::windows::common::COMServiceExecutionContext; using wsl::windows::common::docker_schema::ErrorResponse; @@ -31,7 +32,6 @@ using wsl::windows::common::relay::OverlappedIOHandle; using wsl::windows::common::relay::ReadHandle; using wsl::windows::common::relay::RelayHandle; using wsl::windows::service::wslc::ContainerPortMapping; -using wsl::windows::service::wslc::IWSLCVolume; using wsl::windows::service::wslc::RelayedProcessIO; using wsl::windows::service::wslc::TypedHandle; using wsl::windows::service::wslc::VMPortMapping; @@ -329,10 +329,7 @@ std::string SerializeContainerMetadata(const WSLCContainerMetadataV1& metadata) return wsl::shared::ToJson(wrapper); } -void ProcessNamedVolumes( - const WSLCContainerOptions& containerOptions, - const std::unordered_map>& sessionVolumes, - wsl::windows::common::docker_schema::CreateContainer& request) +void ProcessNamedVolumes(const WSLCContainerOptions& containerOptions, wsl::windows::common::docker_schema::CreateContainer& request) { THROW_HR_IF(E_INVALIDARG, containerOptions.NamedVolumesCount > 0 && containerOptions.NamedVolumes == nullptr); @@ -342,13 +339,8 @@ void ProcessNamedVolumes( THROW_HR_IF_NULL_MSG(E_INVALIDARG, nv.Name, "NamedVolume at index %lu has null Name", i); THROW_HR_IF_NULL_MSG(E_INVALIDARG, nv.ContainerPath, "NamedVolume at index %lu has null ContainerPath", i); - std::string volumeName = nv.Name; - - THROW_HR_WITH_USER_ERROR_IF( - WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(nv.Name), !sessionVolumes.contains(volumeName)); - wsl::windows::common::docker_schema::Mount mount{}; - mount.Source = std::move(volumeName); + mount.Source = std::string(nv.Name); mount.Target = std::string(nv.ContainerPath); mount.Type = "volume"; mount.ReadOnly = static_cast(nv.ReadOnly); @@ -357,23 +349,6 @@ void ProcessNamedVolumes( } } -void ValidateNamedVolumes( - const std::vector& mounts, - const std::unordered_map>& sessionVolumes, - const std::unordered_set& anonymousVolumes) -{ - for (const auto& mount : mounts) - { - if (mount.Type == "volume" && !mount.Name.empty()) - { - THROW_HR_WITH_USER_ERROR_IF( - WSLC_E_VOLUME_NOT_FOUND, - Localization::MessageWslcVolumeNotFound(mount.Name), - !sessionVolumes.contains(mount.Name) && !anonymousVolumes.contains(mount.Name)); - } - } -} - } // namespace ContainerPortMapping::ContainerPortMapping(VMPortMapping&& VmMapping, uint16_t ContainerPort) : @@ -431,7 +406,7 @@ WSLCContainerImpl::WSLCContainerImpl( std::vector&& ports, std::map&& labels, std::function&& onDeleted, - ContainerEventTracker& EventTracker, + DockerEventTracker& EventTracker, DockerHTTPClient& DockerClient, IORelay& Relay, WSLCContainerState InitialState, @@ -830,10 +805,21 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill) void WSLCContainerImpl::Delete(WSLCDeleteFlags Flags) { - // Acquire an exclusive lock since this method modifies m_state. - auto lock = m_lock.lock_exclusive(); + { + // Acquire an exclusive lock since this method modifies m_state. + auto lock = m_lock.lock_exclusive(); + + DeleteExclusiveLockHeld(Flags); + } - DeleteExclusiveLockHeld(Flags); + // Wait for the container destroy event on the Docker event stream after releasing m_lock. + // Docker emits volume destroy events before the container destroy event, so once this returns + // we are guaranteed that all anonymous volumes deleted with the container have been removed from tracking. + // N.B. This must be done outside m_lock to avoid an ABBA deadlock with the relay thread. + if (WI_IsFlagSet(Flags, WSLCDeleteFlagsDeleteVolumes)) + { + m_eventTracker.WaitForObjectDestroyed(m_id); + } } __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::DeleteExclusiveLockHeld(WSLCDeleteFlags Flags) @@ -1109,9 +1095,8 @@ std::unique_ptr WSLCContainerImpl::Create( const WSLCContainerOptions& containerOptions, WSLCSession& wslcSession, WSLCVirtualMachine& virtualMachine, - const std::unordered_map>& sessionVolumes, std::function&& OnDeleted, - ContainerEventTracker& EventTracker, + DockerEventTracker& EventTracker, DockerHTTPClient& DockerClient, IORelay& IoRelay) { @@ -1208,7 +1193,7 @@ std::unique_ptr WSLCContainerImpl::Create( THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.Volumes, "Volumes is null with VolumesCount=%lu", containerOptions.VolumesCount); } - // Build volume list from container options. + // Build bind mount list from container options. std::vector volumes; volumes.reserve(containerOptions.VolumesCount); @@ -1285,7 +1270,7 @@ std::unique_ptr WSLCContainerImpl::Create( } } - ProcessNamedVolumes(containerOptions, sessionVolumes, request); + ProcessNamedVolumes(containerOptions, request); // Prepare port mappings from container options. std::vector<_WSLCPortMapping> ports; @@ -1378,6 +1363,12 @@ std::unique_ptr WSLCContainerImpl::Create( // Inspect the container to fetch its generated name (if needed) and Docker's authoritative Created timestamp. auto inspectData = DockerClient.InspectContainer(result.Id); + // Wait for the container create event to be delivered on the Docker event stream. + // Docker emits volume create events before the container create event, so once this returns + // we are guaranteed that all anonymous volumes created by Docker have been processed by the + // event tracker's volume callback and are already tracked in WSLCVolumes. + EventTracker.WaitForObjectCreated(result.Id); + auto container = std::make_unique( wslcSession, virtualMachine, @@ -1405,17 +1396,30 @@ std::unique_ptr WSLCContainerImpl::Open( const common::docker_schema::ContainerInfo& dockerContainer, WSLCSession& wslcSession, WSLCVirtualMachine& virtualMachine, - const std::unordered_map>& sessionVolumes, - const std::unordered_set& anonymousVolumes, + WSLCVolumes& volumes, std::function&& OnDeleted, - ContainerEventTracker& EventTracker, + DockerEventTracker& EventTracker, DockerHTTPClient& DockerClient, IORelay& ioRelay) { // Extract container name from Docker's names list. std::string name = ExtractContainerName(dockerContainer.Names, dockerContainer.Id); - ValidateNamedVolumes(dockerContainer.Mounts, sessionVolumes, anonymousVolumes); + // Validate that all named volumes mounted by the container were successfully recovered + // by the volumes manager. If any are missing (e.g. backing VHD removed while the service was + // down), refuse to open the container so it cannot enter a broken state. + for (const auto& mount : dockerContainer.Mounts) + { + if (mount.Type == "volume" && !mount.Name.empty()) + { + THROW_HR_IF_MSG( + E_INVALIDARG, + !volumes.ContainsVolume(mount.Name), + "Cannot open container %hs: referenced volume '%hs' is not available", + dockerContainer.Id.c_str(), + mount.Name.c_str()); + } + } auto labels(dockerContainer.Labels); auto metadataIt = labels.find(WSLCContainerMetadataLabel); diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index 3a8ac1d0b..d48b072b6 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -16,7 +16,7 @@ Module Name: #include "ServiceProcessLauncher.h" #include "WSLCSession.h" -#include "ContainerEventTracker.h" +#include "DockerEventTracker.h" #include "DockerHTTPClient.h" #include "WSLCProcessControl.h" #include "IORelay.h" @@ -30,6 +30,7 @@ namespace wsl::windows::service::wslc { class WSLCContainer; class WSLCSession; +class WSLCVolumes; struct ContainerPortMapping { @@ -64,7 +65,7 @@ class WSLCContainerImpl std::vector&& ports, std::map&& labels, std::function&& OnDeleted, - ContainerEventTracker& EventTracker, + DockerEventTracker& EventTracker, DockerHTTPClient& DockerClient, IORelay& Relay, WSLCContainerState InitialState, @@ -112,9 +113,8 @@ class WSLCContainerImpl const WSLCContainerOptions& Options, WSLCSession& wslcSession, WSLCVirtualMachine& virtualMachine, - const std::unordered_map>& SessionVolumes, std::function&& OnDeleted, - ContainerEventTracker& EventTracker, + DockerEventTracker& EventTracker, DockerHTTPClient& DockerClient, IORelay& Relay); @@ -122,10 +122,9 @@ class WSLCContainerImpl const common::docker_schema::ContainerInfo& DockerContainer, WSLCSession& wslcSession, WSLCVirtualMachine& virtualMachine, - const std::unordered_map>& sessionVolumes, - const std::unordered_set& anonymousVolumes, + WSLCVolumes& Volumes, std::function&& OnDeleted, - ContainerEventTracker& EventTracker, + DockerEventTracker& EventTracker, DockerHTTPClient& DockerClient, IORelay& Relay); @@ -170,8 +169,8 @@ class WSLCContainerImpl std::vector m_mountedVolumes; std::map m_labels; Microsoft::WRL::ComPtr m_comWrapper; - ContainerEventTracker& m_eventTracker; - ContainerEventTracker::ContainerTrackingReference m_containerEvents; + DockerEventTracker& m_eventTracker; + DockerEventTracker::EventTrackingReference m_containerEvents; IORelay& m_ioRelay; WSLCContainerNetworkType m_networkingMode{}; }; diff --git a/src/windows/wslcsession/WSLCGuestVolume.cpp b/src/windows/wslcsession/WSLCGuestVolume.cpp index 53361c8e8..3ea59acb0 100644 --- a/src/windows/wslcsession/WSLCGuestVolume.cpp +++ b/src/windows/wslcsession/WSLCGuestVolume.cpp @@ -61,10 +61,6 @@ std::unique_ptr WSLCGuestVolumeImpl::Create( { ValidateDriverOpts(DriverOpts); - WSLCVolumeMetadata metadata; - metadata.Driver = WSLCGuestVolumeDriver; - metadata.DriverOpts = DriverOpts; - docker_schema::CreateVolume request{}; if (Name != nullptr && Name[0] != '\0') { @@ -72,7 +68,6 @@ std::unique_ptr WSLCGuestVolumeImpl::Create( } request.Driver = "local"; request.DriverOpts = DriverOpts; - request.Labels = {{WSLCVolumeMetadataLabel, wsl::shared::ToJson(metadata)}}; // Merge user labels into the Docker volume labels. for (const auto& [key, value] : Labels) @@ -92,14 +87,6 @@ std::unique_ptr WSLCGuestVolumeImpl::Create( std::unique_ptr WSLCGuestVolumeImpl::Open(const wsl::windows::common::docker_schema::Volume& Volume, DockerHTTPClient& DockerClient) { - THROW_HR_IF(E_INVALIDARG, !Volume.Labels.has_value()); - - auto metadataIt = Volume.Labels->find(WSLCVolumeMetadataLabel); - THROW_HR_IF(E_INVALIDARG, metadataIt == Volume.Labels->end()); - - auto metadata = wsl::shared::FromJson(metadataIt->second.c_str()); - THROW_HR_IF(E_INVALIDARG, metadata.Driver != WSLCGuestVolumeDriver); - THROW_HR_IF(E_INVALIDARG, Volume.Driver != "local"); if (Volume.Options.has_value()) @@ -107,20 +94,11 @@ std::unique_ptr WSLCGuestVolumeImpl::Open(const wsl::window ValidateDriverOpts(Volume.Options.value()); } - // Extract user labels (all labels except our internal metadata label). - std::map userLabels; - for (const auto& [key, value] : *Volume.Labels) - { - if (key != WSLCVolumeMetadataLabel) - { - userLabels[key] = value; - } - } - - auto volume = std::make_unique( - std::string{Volume.Name}, std::string{Volume.CreatedAt}, std::move(metadata.DriverOpts), std::move(userLabels), DockerClient); + std::map driverOpts = Volume.Options.value_or(std::map{}); + std::map labels = Volume.Labels.value_or(std::map{}); - return volume; + return std::make_unique( + std::string{Volume.Name}, std::string{Volume.CreatedAt}, std::move(driverOpts), std::move(labels), DockerClient); } void WSLCGuestVolumeImpl::Delete() diff --git a/src/windows/wslcsession/WSLCProcessControl.cpp b/src/windows/wslcsession/WSLCProcessControl.cpp index 43eddbda8..8b5af5536 100644 --- a/src/windows/wslcsession/WSLCProcessControl.cpp +++ b/src/windows/wslcsession/WSLCProcessControl.cpp @@ -41,10 +41,10 @@ const wil::unique_event& WSLCProcessControl::GetExitEvent() const return m_exitEvent; } -DockerContainerProcessControl::DockerContainerProcessControl(WSLCContainerImpl& Container, DockerHTTPClient& DockerClient, ContainerEventTracker& EventTracker) : +DockerContainerProcessControl::DockerContainerProcessControl(WSLCContainerImpl& Container, DockerHTTPClient& DockerClient, DockerEventTracker& EventTracker) : m_container(&Container), m_client(DockerClient), - m_trackingReference(EventTracker.RegisterContainerStateUpdates( + m_eventTrackingReference(EventTracker.RegisterContainerStateUpdates( Container.ID(), std::bind(&DockerContainerProcessControl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))) { @@ -103,7 +103,7 @@ void DockerContainerProcessControl::OnContainerReleased() noexcept // N.B. The caller might keep a reference to the process even after the container is released. // If that happens, make sure that the state tracking can't outlive the session. // This is safe to call without the lock because removing the tracking reference is protected by the event tracker lock. - m_trackingReference.Reset(); + m_eventTrackingReference.Reset(); // Signal the exit event to prevent callers from being blocked on it. if (!m_exitEvent.is_signaled()) @@ -114,11 +114,11 @@ void DockerContainerProcessControl::OnContainerReleased() noexcept } DockerExecProcessControl::DockerExecProcessControl( - WSLCContainerImpl& Container, const std::string& Id, DockerHTTPClient& DockerClient, ContainerEventTracker& EventTracker) : + WSLCContainerImpl& Container, const std::string& Id, DockerHTTPClient& DockerClient, DockerEventTracker& EventTracker) : m_container(&Container), m_id(Id), m_client(DockerClient), - m_trackingReference(EventTracker.RegisterExecStateUpdates( + m_eventTrackingReference(EventTracker.RegisterExecStateUpdates( Container.ID(), Id, std::bind(&DockerExecProcessControl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))) { } @@ -197,7 +197,7 @@ void DockerExecProcessControl::OnContainerReleased() noexcept // If that happens, make sure that the state tracking can't outlive the session. // This is safe to call without the lock because removing the tracking reference is protected by the event tracker lock. - m_trackingReference.Reset(); + m_eventTrackingReference.Reset(); // Signal the exit event to prevent callers being blocked on it. if (!m_exitEvent.is_signaled()) diff --git a/src/windows/wslcsession/WSLCProcessControl.h b/src/windows/wslcsession/WSLCProcessControl.h index f33e0703e..1d807075e 100644 --- a/src/windows/wslcsession/WSLCProcessControl.h +++ b/src/windows/wslcsession/WSLCProcessControl.h @@ -15,7 +15,7 @@ Module Name: #pragma once #include "wslc.h" #include "DockerHTTPClient.h" -#include "ContainerEventTracker.h" +#include "DockerEventTracker.h" namespace wsl::windows::service::wslc { @@ -42,7 +42,7 @@ class WSLCProcessControl class DockerContainerProcessControl : public WSLCProcessControl { public: - DockerContainerProcessControl(WSLCContainerImpl& Container, DockerHTTPClient& DockerClient, ContainerEventTracker& EventTracker); + DockerContainerProcessControl(WSLCContainerImpl& Container, DockerHTTPClient& DockerClient, DockerEventTracker& EventTracker); ~DockerContainerProcessControl(); void Signal(int Signal) override; void ResizeTty(ULONG Rows, ULONG Columns) override; @@ -55,13 +55,13 @@ class DockerContainerProcessControl : public WSLCProcessControl std::mutex m_lock; DockerHTTPClient& m_client; WSLCContainerImpl* m_container{}; - ContainerEventTracker::ContainerTrackingReference m_trackingReference; + DockerEventTracker::EventTrackingReference m_eventTrackingReference; }; class DockerExecProcessControl : public WSLCProcessControl { public: - DockerExecProcessControl(WSLCContainerImpl& Container, const std::string& Id, DockerHTTPClient& DockerClient, ContainerEventTracker& EventTracker); + DockerExecProcessControl(WSLCContainerImpl& Container, const std::string& Id, DockerHTTPClient& DockerClient, DockerEventTracker& EventTracker); ~DockerExecProcessControl(); void Signal(int Signal) override; void ResizeTty(ULONG Rows, ULONG Columns) override; @@ -79,7 +79,7 @@ class DockerExecProcessControl : public WSLCProcessControl std::optional m_pid{}; DockerHTTPClient& m_client; WSLCContainerImpl* m_container{}; - ContainerEventTracker::ContainerTrackingReference m_trackingReference; + DockerEventTracker::EventTrackingReference m_eventTrackingReference; }; class VMProcessControl : public WSLCProcessControl diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index a755f846c..17452e413 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -260,9 +260,10 @@ try // Start the event tracker. m_eventTracker.emplace(m_dockerClient.value(), m_id, m_ioRelay); - // Recover any existing containers from storage. + m_volumes.emplace(m_dockerClient.value(), m_virtualMachine.value(), m_eventTracker.value(), m_storageVhdPath.parent_path()); + + // Recover any existing resources from storage. RecoverExistingNetworks(); - RecoverExistingVolumes(); RecoverExistingContainers(); errorCleanup.release(); @@ -1586,13 +1587,12 @@ try try { - std::scoped_lock lock(m_containersLock, m_volumesLock); + std::lock_guard lock(m_containersLock); auto& it = m_containers.emplace_back(WSLCContainerImpl::Create( *containerOptions, *this, m_virtualMachine.value(), - m_volumes, std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), m_eventTracker.value(), m_dockerClient.value(), @@ -1880,54 +1880,18 @@ try RETURN_HR_IF_NULL(E_POINTER, VolumeInfo); ZeroMemory(VolumeInfo, sizeof(*VolumeInfo)); - // Default driver to "guest" if not specified. - std::string driver = (Options->Driver != nullptr && *Options->Driver != '\0') ? Options->Driver : WSLCGuestVolumeDriver; - - THROW_HR_WITH_USER_ERROR_IF( - E_INVALIDARG, Localization::MessageWslcInvalidVolumeType(driver), driver != WSLCVhdVolumeDriver && driver != WSLCGuestVolumeDriver); - auto driverOpts = wslutil::ParseKeyValuePairs(Options->DriverOpts, Options->DriverOptsCount); auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCVolumeMetadataLabel); auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); - - std::lock_guard volumesLock(m_volumesLock); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); if (Options->Name != nullptr && Options->Name[0] != '\0') { ValidateName(Options->Name, WSLC_MAX_VOLUME_NAME_LENGTH); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), m_volumes.contains(Options->Name)); - } - - std::unique_ptr volume; - if (driver == WSLCVhdVolumeDriver) - { - WSL_LOG("VolumeCreatedVhdDriver", TraceLoggingValue(Options->Name ? Options->Name : "", "VolumeName")); - volume = WSLCVhdVolumeImpl::Create( - Options->Name, - std::move(driverOpts), - std::move(labels), - m_storageVhdPath.parent_path(), - m_virtualMachine.value(), - m_dockerClient.value()); - } - else - { - WI_ASSERT(driver == WSLCGuestVolumeDriver); - volume = WSLCGuestVolumeImpl::Create(Options->Name, std::move(driverOpts), std::move(labels), m_dockerClient.value()); } - const auto& name = volume->Name(); - auto info = volume->GetVolumeInformation(); - - auto [it, inserted] = m_volumes.insert({name, std::move(volume)}); - WI_VERIFY(inserted); - - WSL_LOG("VolumeCreated", TraceLoggingValue(name.c_str(), "VolumeName")); - - *VolumeInfo = info; + *VolumeInfo = m_volumes->CreateVolume(Options->Name, Options->Driver, std::move(driverOpts), std::move(labels)); return S_OK; } CATCH_RETURN(); @@ -1938,22 +1902,11 @@ try COMServiceExecutionContext context; RETURN_HR_IF_NULL(E_POINTER, Name); - std::string name = Name; - ValidateName(name.c_str(), WSLC_MAX_VOLUME_NAME_LENGTH); auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); - - std::lock_guard volumesLock(m_volumesLock); - - auto it = m_volumes.find(name); - THROW_HR_WITH_USER_ERROR_IF(WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(name), it == m_volumes.end()); - - it->second->Delete(); - m_volumes.erase(it); - WSL_LOG("VolumeDeleted", TraceLoggingValue(name.c_str(), "VolumeName")); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + m_volumes->DeleteVolume(Name); return S_OK; } CATCH_RETURN(); @@ -1970,25 +1923,23 @@ try *Count = 0; auto lock = m_lock.lock_shared(); - std::lock_guard volumesLock(m_volumesLock); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); - if (m_volumes.empty()) + auto volumeList = m_volumes->ListVolumes(); + + if (volumeList.empty()) { return S_OK; } - auto output = wil::make_unique_cotaskmem(m_volumes.size()); - - ULONG index = 0; - for (const auto& [name, vol] : m_volumes) + auto output = wil::make_unique_cotaskmem(volumeList.size()); + for (size_t i = 0; i < volumeList.size(); i++) { - output[index] = vol->GetVolumeInformation(); - index++; + output[i] = volumeList[i]; } + *Count = static_cast(volumeList.size()); *Volumes = output.release(); - *Count = index; - return S_OK; } CATCH_RETURN(); @@ -2007,14 +1958,9 @@ try ValidateName(name.c_str(), WSLC_MAX_VOLUME_NAME_LENGTH); auto lock = m_lock.lock_shared(); - std::lock_guard volumesLock(m_volumesLock); - - auto it = m_volumes.find(name); - THROW_HR_WITH_USER_ERROR_IF(WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(name), it == m_volumes.end()); - - const auto& volume = it->second; + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); - std::string json = volume->Inspect(); + std::string json = m_volumes->InspectVolume(name); *Output = wil::make_unique_ansistring(json.c_str()).release(); return S_OK; @@ -2308,11 +2254,10 @@ try // Acquire an exclusive lock to ensure that no operation is running. auto lock = m_lock.lock_exclusive(); std::lock_guard containersLock(m_containersLock); - std::lock_guard volumesLock(m_volumesLock); std::lock_guard networksLock(m_networksLock); m_containers.clear(); - m_volumes.clear(); + m_volumes.reset(); m_networks.clear(); // Stop the IO relay. @@ -2598,8 +2543,7 @@ void WSLCSession::RecoverExistingContainers() dockerContainer, *this, m_virtualMachine.value(), - m_volumes, - m_anonymousVolumes, + *m_volumes, std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), m_eventTracker.value(), m_dockerClient.value(), @@ -2668,59 +2612,4 @@ void WSLCSession::RecoverExistingNetworks() TraceLoggingValue(m_networks.size(), "NetworkCount")); } -void WSLCSession::RecoverExistingVolumes() -{ - WI_ASSERT(m_dockerClient.has_value()); - WI_ASSERT(m_virtualMachine.has_value()); - - auto volumes = m_dockerClient->ListVolumes(); - - std::lock_guard volumesLock(m_volumesLock); - - for (const auto& volume : volumes) - { - if (!volume.Labels.has_value() || !volume.Labels->contains(WSLCVolumeMetadataLabel)) - { - m_anonymousVolumes.insert(volume.Name); - continue; - } - - try - { - WI_ASSERT(!m_volumes.contains(volume.Name)); - - // Peek at the driver field to decide which implementation to use. - const auto& metadataJson = volume.Labels->at(WSLCVolumeMetadataLabel); - auto metadata = wsl::shared::FromJson(metadataJson.c_str()); - - std::unique_ptr recovered; - if (metadata.Driver == WSLCVhdVolumeDriver) - { - recovered = WSLCVhdVolumeImpl::Open(volume, m_virtualMachine.value(), m_dockerClient.value()); - } - else if (metadata.Driver == WSLCGuestVolumeDriver) - { - recovered = WSLCGuestVolumeImpl::Open(volume, m_dockerClient.value()); - } - else - { - WSL_LOG( - "VolumeRecoverySkippedUnknownDriver", - TraceLoggingValue(volume.Name.c_str(), "VolumeName"), - TraceLoggingValue(metadata.Driver.c_str(), "Driver")); - continue; - } - - auto [_, inserted] = m_volumes.insert({volume.Name, std::move(recovered)}); - WI_VERIFY(inserted); - } - CATCH_LOG_MSG("Failed to recover volume: %hs", volume.Name.c_str()); - } - - WSL_LOG( - "VolumesRecovered", - TraceLoggingValue(m_displayName.c_str(), "SessionName"), - TraceLoggingValue(m_volumes.size(), "VolumeCount")); -} - } // namespace wsl::windows::service::wslc diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index 935888b6a..95f8f0b7f 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -17,12 +17,9 @@ Module Name: #include "wslc.h" #include "WSLCVirtualMachine.h" #include "WSLCContainer.h" -#include "IWSLCVolume.h" -#include "WSLCVhdVolume.h" -#include "WSLCGuestVolume.h" -#include "WSLCVolumeMetadata.h" +#include "WSLCVolumes.h" #include "WSLCNetworkMetadata.h" -#include "ContainerEventTracker.h" +#include "DockerEventTracker.h" #include "DockerHTTPClient.h" #include "IORelay.h" #include @@ -174,7 +171,6 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession int StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs); void ImportImageImpl(DockerHTTPClient::HTTPRequestContext& Request, const WSLCHandle ImageHandle); void RecoverExistingContainers(); - void RecoverExistingVolumes(); void RecoverExistingNetworks(); void SaveImageImpl(std::pair& RequestCodePair, WSLCHandle OutputHandle, HANDLE CancelEvent); @@ -182,19 +178,18 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession std::optional m_dockerClient; std::optional m_virtualMachine; - std::optional m_eventTracker; + std::optional m_eventTracker; wil::unique_event m_dockerdReadyEvent{wil::EventOptions::ManualReset}; std::wstring m_displayName; std::filesystem::path m_storageVhdPath; - // N.B. m_lock must be acquired before acquiring m_volumesLock, m_containersLock, or m_networksLock. - // These locks protect m_volumes / m_containers without requiring an exclusive m_lock. - // This allows independent operations to proceed while volume/container bookkeeping remains synchronized. + // N.B. m_lock must be acquired before acquiring m_containersLock or m_networksLock. + // These locks protect m_containers without requiring an exclusive m_lock. + // This allows independent operations to proceed while container bookkeeping remains synchronized. + // WSLCVolumes has its own internal srwlock and does not require m_lock. std::mutex m_containersLock; - std::mutex m_volumesLock; std::vector> m_containers; - std::unordered_map> m_volumes; - std::unordered_set m_anonymousVolumes; // TODO: Implement proper anonymous volume support. + std::optional m_volumes; std::mutex m_networksLock; std::unordered_map m_networks; wil::unique_event m_sessionTerminatingEvent{wil::EventOptions::ManualReset}; diff --git a/src/windows/wslcsession/WSLCVhdVolume.h b/src/windows/wslcsession/WSLCVhdVolume.h index e0945224a..df0bfc1cb 100644 --- a/src/windows/wslcsession/WSLCVhdVolume.h +++ b/src/windows/wslcsession/WSLCVhdVolume.h @@ -78,7 +78,7 @@ class WSLCVhdVolumeImpl : public IWSLCVolume return m_virtualMachinePath; } - void OnDeleted(); + void OnDeleted() override; private: void Detach(); diff --git a/src/windows/wslcsession/WSLCVolumes.cpp b/src/windows/wslcsession/WSLCVolumes.cpp new file mode 100644 index 000000000..4fff4dc4a --- /dev/null +++ b/src/windows/wslcsession/WSLCVolumes.cpp @@ -0,0 +1,186 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCVolumes.cpp + +Abstract: + + Contains the implementation of WSLCVolumes. + +--*/ + +#include "precomp.h" +#include "WSLCVolumes.h" +#include "WSLCVhdVolume.h" +#include "WSLCGuestVolume.h" +#include "WSLCVirtualMachine.h" +#include "docker_schema.h" + +using wsl::shared::Localization; + +namespace wsl::windows::service::wslc { + +WSLCVolumes::WSLCVolumes( + DockerHTTPClient& dockerClient, WSLCVirtualMachine& virtualMachine, DockerEventTracker& eventTracker, const std::filesystem::path& storagePath) : + m_dockerClient(dockerClient), m_virtualMachine(virtualMachine), m_storagePath(storagePath) +{ + // Hold m_lock exclusively across both callback registration and the recovery loop. + // This ensures any volume events that arrive while recovering are queued behind us in OnVolumeEvent, + // and dedup naturally against entries inserted by recovery (insert is a no-op for existing keys). + auto lock = m_lock.lock_exclusive(); + + m_volumeEventTracking = eventTracker.RegisterVolumeUpdates( + std::bind(&WSLCVolumes::OnVolumeEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + + for (const auto& volume : dockerClient.ListVolumes()) + { + try + { + OpenVolumeExclusiveLockHeld(volume); + } + CATCH_LOG_MSG("Failed to recover volume: %hs", volume.Name.c_str()); + } +} + +__requires_lock_held(m_lock) void WSLCVolumes::OpenVolumeExclusiveLockHeld(const wsl::windows::common::docker_schema::Volume& vol) +{ + THROW_HR_IF_MSG(E_UNEXPECTED, vol.Driver != "local", "Unrecognized volume driver: %hs", vol.Driver.c_str()); + + if (vol.Labels.has_value() && vol.Labels->contains(WSLCVolumeMetadataLabel)) + { + auto metadata = wsl::shared::FromJson(vol.Labels->at(WSLCVolumeMetadataLabel).c_str()); + + if (metadata.Driver == WSLCVhdVolumeDriver) + { + m_volumes.insert({vol.Name, WSLCVhdVolumeImpl::Open(vol, m_virtualMachine, m_dockerClient)}); + return; + } + } + + m_volumes.insert({vol.Name, WSLCGuestVolumeImpl::Open(vol, m_dockerClient)}); +} + +void WSLCVolumes::OnVolumeEvent(const std::string& volumeName, VolumeEvent event, std::uint64_t) +{ + if (event == VolumeEvent::Create) + { + OpenVolume(volumeName); + } + else if (event == VolumeEvent::Destroy) + { + OnVolumeDeleted(volumeName); + } +} + +WSLCVolumeInformation WSLCVolumes::CreateVolume( + LPCSTR Name, LPCSTR Driver, std::map&& DriverOpts, std::map&& Labels) +{ + auto lock = m_lock.lock_exclusive(); + + if (Name != nullptr && Name[0] != '\0') + { + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), m_volumes.contains(Name)); + } + + std::string driver = (Driver != nullptr && Driver[0] != '\0') ? Driver : WSLCGuestVolumeDriver; + std::unique_ptr volume; + + if (driver == WSLCVhdVolumeDriver) + { + volume = WSLCVhdVolumeImpl::Create(Name, std::move(DriverOpts), std::move(Labels), m_storagePath, m_virtualMachine, m_dockerClient); + } + else if (driver == WSLCGuestVolumeDriver) + { + volume = WSLCGuestVolumeImpl::Create(Name, std::move(DriverOpts), std::move(Labels), m_dockerClient); + } + else + { + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcInvalidVolumeType(driver)); + } + + const auto& name = volume->Name(); + auto info = volume->GetVolumeInformation(); + + auto [it, inserted] = m_volumes.insert({name, std::move(volume)}); + WI_VERIFY(inserted); + + return info; +} + +void WSLCVolumes::DeleteVolume(LPCSTR Name) +{ + THROW_HR_IF(E_POINTER, Name == nullptr); + + auto lock = m_lock.lock_exclusive(); + + auto it = m_volumes.find(Name); + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(Name), it == m_volumes.end()); + + it->second->Delete(); + m_volumes.erase(it); +} + +std::vector WSLCVolumes::ListVolumes() const +{ + auto lock = m_lock.lock_shared(); + + std::vector result; + result.reserve(m_volumes.size()); + + for (const auto& [name, vol] : m_volumes) + { + result.push_back(vol->GetVolumeInformation()); + } + + return result; +} + +std::string WSLCVolumes::InspectVolume(const std::string& Name) const +{ + auto lock = m_lock.lock_shared(); + + auto it = m_volumes.find(Name); + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(Name), it == m_volumes.end()); + + return it->second->Inspect(); +} + +bool WSLCVolumes::ContainsVolume(const std::string& Name) const +{ + auto lock = m_lock.lock_shared(); + return m_volumes.contains(Name); +} + +void WSLCVolumes::OpenVolume(const std::string& VolumeName) +{ + auto lock = m_lock.lock_exclusive(); + + if (VolumeName.empty() || m_volumes.contains(VolumeName)) + { + return; + } + + try + { + auto vol = m_dockerClient.InspectVolume(VolumeName); + OpenVolumeExclusiveLockHeld(vol); + } + CATCH_LOG_MSG("Failed to open volume: %hs", VolumeName.c_str()); +} + +void WSLCVolumes::OnVolumeDeleted(const std::string& VolumeName) +{ + auto lock = m_lock.lock_exclusive(); + + auto it = m_volumes.find(VolumeName); + if (it != m_volumes.end()) + { + it->second->OnDeleted(); + m_volumes.erase(it); + } +} + +} // namespace wsl::windows::service::wslc diff --git a/src/windows/wslcsession/WSLCVolumes.h b/src/windows/wslcsession/WSLCVolumes.h new file mode 100644 index 000000000..2f2996aab --- /dev/null +++ b/src/windows/wslcsession/WSLCVolumes.h @@ -0,0 +1,65 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCVolumes.h + +Abstract: + + Contains the definition for WSLCVolumes. + +--*/ + +#pragma once + +#include "IWSLCVolume.h" +#include "WSLCVolumeMetadata.h" +#include "DockerHTTPClient.h" +#include "DockerEventTracker.h" +#include + +namespace wsl::windows::service::wslc { + +class WSLCVirtualMachine; + +class WSLCVolumes +{ +public: + NON_COPYABLE(WSLCVolumes); + NON_MOVABLE(WSLCVolumes); + + WSLCVolumes(DockerHTTPClient& dockerClient, WSLCVirtualMachine& virtualMachine, DockerEventTracker& eventTracker, const std::filesystem::path& storagePath); + ~WSLCVolumes() = default; + + WSLCVolumeInformation CreateVolume( + _In_opt_ LPCSTR Name, + _In_opt_ LPCSTR Driver, + _In_ std::map&& DriverOpts, + _In_ std::map&& Labels); + + void DeleteVolume(_In_ LPCSTR Name); + + std::vector ListVolumes() const; + std::string InspectVolume(_In_ const std::string& Name) const; + + bool ContainsVolume(_In_ const std::string& Name) const; + +private: + void OpenVolume(_In_ const std::string& VolumeName); + __requires_lock_held(m_lock) void OpenVolumeExclusiveLockHeld(const wsl::windows::common::docker_schema::Volume& vol); + + void OnVolumeEvent(const std::string& volumeName, VolumeEvent event, std::uint64_t eventTime); + void OnVolumeDeleted(_In_ const std::string& VolumeName); + + mutable wil::srwlock m_lock; + _Guarded_by_(m_lock) std::unordered_map> m_volumes; + + DockerHTTPClient& m_dockerClient; + WSLCVirtualMachine& m_virtualMachine; + std::filesystem::path m_storagePath; + DockerEventTracker::EventTrackingReference m_volumeEventTracking; +}; + +} // namespace wsl::windows::service::wslc diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 7bde747f4..c1ab4dd5a 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -1925,21 +1925,21 @@ class WSLCTests VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build:latest")); ExpectImagePresent(*m_defaultSession, "wslc-test-build:latest"); - // Lists anonymous docker volume names via the VM's docker CLI. - // TODO: Add proper support so we can list via session's API instead. auto listAnonymousVolumes = [&]() { - auto result = ExpectCommandResult( - m_defaultSession.get(), {"/usr/bin/docker", "volume", "ls", "-q", "-f", "label=com.docker.volume.anonymous"}, 0); + wil::unique_cotaskmem_array_ptr volumes; + VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address())); + std::vector names; - std::stringstream ss(result.Output[1]); - std::string line; - while (std::getline(ss, line)) + + // TODO: Replace with filter for anonymous volumes in ListVolumes API. + for (const auto& vol : volumes) { - if (!line.empty()) + if (std::string(vol.Driver) == "guest") { - names.push_back(line); + names.push_back(vol.Name); } } + return names; }; @@ -1983,16 +1983,21 @@ class WSLCTests container.GetInitProcess().Wait(); container.SetDeleteOnClose(false); + // Clean up any leaked anonymous volumes when this block exits. + auto volumeCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { + auto volumes = listAnonymousVolumes(); + for (const auto& name : volumes) + { + LOG_IF_FAILED(m_defaultSession->DeleteVolume(name.c_str())); + } + }); + VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 1u); VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone)); // Anonymous volume was NOT deleted by Docker. - auto leaked = listAnonymousVolumes(); - VERIFY_ARE_EQUAL(leaked.size(), 1u); - - RunCommand(m_defaultSession.get(), {"/usr/bin/docker", "volume", "prune", "-f"}); - VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 0u); + VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 1u); } // Delete container with WSLCDeleteFlagsDeleteVolumes -> anonymous volume is cleaned up. @@ -2018,6 +2023,12 @@ class WSLCTests VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 1u); VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0)); + // Wait for the volume destroy event to be processed by the event tracker. + wsl::shared::retry::RetryWithTimeout( + [&]() { THROW_WIN32_IF(ERROR_RETRY, listAnonymousVolumes().size() != 0u); }, + std::chrono::milliseconds{100}, + std::chrono::seconds{10}); + VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 0u); } } diff --git a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp index 506c2e81b..cb86f5fed 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp @@ -632,13 +632,17 @@ class WSLCE2EContainerRunTests result.Verify({.Stdout = L"WSLC Named Volume Test", .Stderr = L"", .ExitCode = 0}); } - WSLC_TEST_METHOD(WSLCE2E_Container_Run_Volume_NamedVolume_NotFound_Fail) + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Volume_NamedVolume_AutoCreate) { auto result = RunWslc(std::format( L"container run --rm --volume {}:/data {} sh -c \"echo -n 'WSLC Named Volume Test' > /data/test.txt\"", WslcVolumeName, DebianImage.NameAndTag())); - result.Verify({.Stderr = std::format(L"Volume not found: '{}'\r\nError code: WSLC_E_VOLUME_NOT_FOUND\r\n", WslcVolumeName), .ExitCode = 1}); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + // Verify the volume was auto-created by removing it (fails if it doesn't exist). + result = RunWslc(std::format(L"volume rm {}", WslcVolumeName)); + result.Verify({.Stderr = L"", .ExitCode = 0}); } private: