diff --git a/src/windows/common/WSLCSessionDefaults.h b/src/windows/common/WSLCSessionDefaults.h index e288d4179f..9978a4a8cc 100644 --- a/src/windows/common/WSLCSessionDefaults.h +++ b/src/windows/common/WSLCSessionDefaults.h @@ -22,5 +22,6 @@ inline constexpr const wchar_t DefaultAdminSessionName[] = L"wslc-cli-admin"; inline constexpr const wchar_t DefaultStorageSubPath[] = L"wslc\\sessions"; inline constexpr const wchar_t DefaultStorageVhdName[] = L"storage.vhdx"; inline constexpr uint32_t DefaultBootTimeoutMs = 30000; +inline constexpr const char ContainerdStorageMountPoint[] = "/var/lib/docker"; } // namespace wsl::windows::wslc diff --git a/src/windows/common/WSLCUserSettings.cpp b/src/windows/common/WSLCUserSettings.cpp index b0c3e1e861..0a8581c284 100644 --- a/src/windows/common/WSLCUserSettings.cpp +++ b/src/windows/common/WSLCUserSettings.cpp @@ -57,6 +57,9 @@ static constexpr std::string_view s_DefaultSettingsTemplate = " # used without an explicit address (default: 127.0.0.1)\n" " # defaultBindingAddress: default\n" "\n" + " # Seconds an idle session VM stays running before it is torn down (default: 30)\n" + " # idleTimeout: default\n" + "\n" "# Credential storage backend: \"wincred\" or \"file\" (default: wincred)\n" "# credentialStore: wincred\n"; @@ -163,6 +166,11 @@ namespace details { return value; } + WSLC_VALIDATE_SETTING(SessionIdleTimeout) + { + return value > 0 ? std::optional{value} : std::nullopt; + } + WSLC_VALIDATE_SETTING(CredentialStore) { if (value == "wincred") diff --git a/src/windows/common/WSLCUserSettings.h b/src/windows/common/WSLCUserSettings.h index 30e3f3f9a6..7219fa7ade 100644 --- a/src/windows/common/WSLCUserSettings.h +++ b/src/windows/common/WSLCUserSettings.h @@ -45,6 +45,7 @@ enum class Setting : size_t SessionPortRelay, SessionDefaultBindingAddress, SessionStoragePath, + SessionIdleTimeout, Max }; @@ -101,6 +102,7 @@ namespace details { DEFINE_SETTING_MAPPING(SessionPortRelay, std::string, PortRelayType, PortRelayType::VirtioNet, "experimental.portRelay") DEFINE_SETTING_MAPPING(SessionDefaultBindingAddress, std::string, std::string, std::string{}, "session.defaultBindingAddress") DEFINE_SETTING_MAPPING(SessionStoragePath, std::string, std::string, std::string{}, "session.storagePath") + DEFINE_SETTING_MAPPING(SessionIdleTimeout, uint32_t, uint32_t, 30, "session.idleTimeout") #undef DEFINE_SETTING_MAPPING // clang-format on diff --git a/src/windows/inc/WslPluginApi.h b/src/windows/inc/WslPluginApi.h index bcdcdb8f4f..2b60a20be8 100644 --- a/src/windows/inc/WslPluginApi.h +++ b/src/windows/inc/WslPluginApi.h @@ -141,6 +141,19 @@ typedef HRESULT (*WSLPluginAPI_ImageCreated)(const struct WSLCSessionInformation // Called when an image is deleted. 'ImageId' is the deleted image identifier. Errors are ignored. typedef HRESULT (*WSLPluginAPI_ImageDeleted)(const struct WSLCSessionInformation* Session, LPCSTR ImageId); +// Called when the VM backing a WSLC session has started. Unlike OnSessionCreated (which fires once +// per session), this fires every time a VM is created for the session: on the first operation that +// needs a VM, and again each time the VM is recreated after being idle-terminated. Errors are logged +// but ignored (they do not abort VM startup or the triggering operation). +typedef HRESULT (*WSLPluginAPI_OnWslcVmStarted)(const struct WSLCSessionInformation* Session); + +// Called when the VM backing a WSLC session is about to stop (idle teardown, explicit termination, +// or unexpected exit). Fires every time a VM is torn down, paired with a prior OnWslcVmStarted. +// Errors are logged but ignored. On an idle teardown the session remains alive, so a callback that +// needs the VM (e.g. WSLCCreateProcess) transparently restarts it; during a permanent session +// termination the same call fails cleanly because the session is being torn down. +typedef HRESULT (*WSLPluginAPI_OnWslcVmStopping)(const struct WSLCSessionInformation* Session); + // // WSLC plugin API calls. // @@ -220,6 +233,8 @@ struct WSLPluginHooksV1 WSLPluginAPI_ContainerStopping ContainerStopping; WSLPluginAPI_ImageCreated ImageCreated; WSLPluginAPI_ImageDeleted ImageDeleted; + WSLPluginAPI_OnWslcVmStarted WslcVmStarted; // Introduced in 2.9.5 + WSLPluginAPI_OnWslcVmStopping WslcVmStopping; // Introduced in 2.9.5 }; struct WSLPluginAPIV1 diff --git a/src/windows/service/exe/HcsVirtualMachine.cpp b/src/windows/service/exe/HcsVirtualMachine.cpp index c11a1f78e1..7609643923 100644 --- a/src/windows/service/exe/HcsVirtualMachine.cpp +++ b/src/windows/service/exe/HcsVirtualMachine.cpp @@ -350,7 +350,9 @@ HcsVirtualMachine::HcsVirtualMachine(_In_ const WSLCSessionSettings* Settings) HcsVirtualMachine::~HcsVirtualMachine() { - std::lock_guard lock(m_lock); + // Do not hold m_lock: waiting on m_vmExitEvent and closing the compute system below both block + // on in-flight HCS exit/crash callbacks, which may themselves need m_lock. OnExit() is lock-free, + // and closing the compute system drains all callbacks, so the rest of teardown needs no lock. // Wait up to 5 seconds for the VM to terminate gracefully. bool forceTerminate = false; diff --git a/src/windows/service/exe/PluginManager.cpp b/src/windows/service/exe/PluginManager.cpp index 0d6b6ccbb7..1173dd6746 100644 --- a/src/windows/service/exe/PluginManager.cpp +++ b/src/windows/service/exe/PluginManager.cpp @@ -691,3 +691,41 @@ void PluginManager::OnWslcImageDeleted(const WSLCSessionInformation* Session, LP } } } + +void PluginManager::OnWslcVmStarted(const WSLCSessionInformation* Session) const +{ + ExecutionContext context(Context::Plugin); + + for (const auto& e : m_plugins) + { + if (e.hooks.WslcVmStarted != nullptr) + { + const auto result = e.hooks.WslcVmStarted(Session); + WSL_LOG( + "PluginOnWslcVmStartedCall", + TraceLoggingValue(e.name.c_str(), "Plugin"), + TraceLoggingValue(Session->SessionId, "SessionId"), + TraceLoggingValue(result, "Result")); + LOG_IF_FAILED_MSG(result, "Error thrown from plugin: '%ls'", e.name.c_str()); + } + } +} + +void PluginManager::OnWslcVmStopping(const WSLCSessionInformation* Session) const +{ + ExecutionContext context(Context::Plugin); + + for (const auto& e : m_plugins) + { + if (e.hooks.WslcVmStopping != nullptr) + { + const auto result = e.hooks.WslcVmStopping(Session); + WSL_LOG( + "PluginOnWslcVmStoppingCall", + TraceLoggingValue(e.name.c_str(), "Plugin"), + TraceLoggingValue(Session->SessionId, "SessionId"), + TraceLoggingValue(result, "Result")); + LOG_IF_FAILED_MSG(result, "Error thrown from plugin: '%ls'", e.name.c_str()); + } + } +} diff --git a/src/windows/service/exe/PluginManager.h b/src/windows/service/exe/PluginManager.h index a99a3e332d..f5e4779565 100644 --- a/src/windows/service/exe/PluginManager.h +++ b/src/windows/service/exe/PluginManager.h @@ -52,6 +52,8 @@ class PluginManager void OnWslcContainerStopping(const WSLCSessionInformation* Session, LPCSTR ContainerId) const; void OnWslcImageCreated(const WSLCSessionInformation* Session, LPCSTR InspectJson) const; void OnWslcImageDeleted(const WSLCSessionInformation* Session, LPCSTR ImageId) const; + void OnWslcVmStarted(const WSLCSessionInformation* Session) const; + void OnWslcVmStopping(const WSLCSessionInformation* Session) const; void ThrowIfFatalPluginError() const; diff --git a/src/windows/service/exe/WSLCPluginNotifier.cpp b/src/windows/service/exe/WSLCPluginNotifier.cpp index 7daab35bbf..2b2a88357a 100644 --- a/src/windows/service/exe/WSLCPluginNotifier.cpp +++ b/src/windows/service/exe/WSLCPluginNotifier.cpp @@ -64,3 +64,23 @@ try return S_OK; } CATCH_RETURN(); + +HRESULT WSLCPluginNotifier::OnVmStarted() +try +{ + COMServiceExecutionContext context; + + m_plugins.OnWslcVmStarted(&m_sessionInfo); + return S_OK; +} +CATCH_RETURN(); + +HRESULT WSLCPluginNotifier::OnVmStopping() +try +{ + COMServiceExecutionContext context; + + m_plugins.OnWslcVmStopping(&m_sessionInfo); + return S_OK; +} +CATCH_RETURN(); diff --git a/src/windows/service/exe/WSLCPluginNotifier.h b/src/windows/service/exe/WSLCPluginNotifier.h index b8ba4e11ec..de5f197a33 100644 --- a/src/windows/service/exe/WSLCPluginNotifier.h +++ b/src/windows/service/exe/WSLCPluginNotifier.h @@ -34,6 +34,8 @@ class DECLSPEC_UUID("E29B0F1A-4E18-4F09-83A2-2D6B1B9F8C4D") WSLCPluginNotifier IFACEMETHOD(OnContainerStopping)(_In_ LPCSTR ContainerId) override; IFACEMETHOD(OnImageCreated)(_In_ LPCSTR InspectJson) override; IFACEMETHOD(OnImageDeleted)(_In_ LPCSTR ImageId) override; + IFACEMETHOD(OnVmStarted)() override; + IFACEMETHOD(OnVmStopping)() override; private: wsl::windows::service::PluginManager& m_plugins; diff --git a/src/windows/service/exe/WSLCSessionManager.cpp b/src/windows/service/exe/WSLCSessionManager.cpp index 7ccf4724d6..f6beab093a 100644 --- a/src/windows/service/exe/WSLCSessionManager.cpp +++ b/src/windows/service/exe/WSLCSessionManager.cpp @@ -121,6 +121,7 @@ struct SessionSettings Settings.MemoryMb = memoryMb > 0 ? memoryMb : SessionSettings::DefaultMemoryMb(); Settings.MaximumStorageSizeMb = userSettings.Get(); Settings.BootTimeoutMs = wsl::windows::wslc::DefaultBootTimeoutMs; + Settings.IdleTimeoutSec = userSettings.Get(); Settings.NetworkingMode = userSettings.Get(); // TODO: Add a config setting to opt-out of GPU support. @@ -180,6 +181,12 @@ try } CATCH_LOG() +bool WSLCSessionManagerImpl::InPluginNotificationContext() noexcept +{ + const auto* context = wsl::windows::common::ExecutionContext::Current(); + return context != nullptr && WI_IsFlagSet(context->CurrentContext(), wsl::windows::common::Context::Plugin); +} + void WSLCSessionManagerImpl::CreateSession( _In_ const WSLCSessionSettings* Settings, _In_ WSLCSessionFlags Flags, _In_opt_ IWarningCallback* WarningCallback, _Out_ IWSLCSession** WslcSession) { @@ -285,8 +292,7 @@ void WSLCSessionManagerImpl::CreateSession( g_pluginManager, sessionId, creatorPid, std::wstring(resolvedDisplayName), wil::shared_handle(sharedToken), std::vector(storedSid)); // Create the VM factory in the SYSTEM service (privileged). The per-user session - // uses it to create the VM. Funneling VM creation through a factory lets the session - // own when VMs are created, rather than having one handed to it up front. + // uses it to create VMs on demand and recreate them after idle-termination. auto vmFactory = Microsoft::WRL::Make(Settings); // Launch per-user COM server factory and add it to a fresh per-session job object for crash cleanup. @@ -473,6 +479,7 @@ WSLCSessionInitSettings WSLCSessionManagerImpl::CreateSessionSettings( sessionSettings.RootVhdTypeOverride = Settings->RootVhdTypeOverride; sessionSettings.StorageFlags = Settings->StorageFlags; sessionSettings.SwapSizeMb = Settings->MemoryMb; + sessionSettings.IdleTimeoutSec = Settings->IdleTimeoutSec; return sessionSettings; } diff --git a/src/windows/service/exe/WSLCSessionManager.h b/src/windows/service/exe/WSLCSessionManager.h index 315cc22c7e..e8188d7a7f 100644 --- a/src/windows/service/exe/WSLCSessionManager.h +++ b/src/windows/service/exe/WSLCSessionManager.h @@ -129,6 +129,16 @@ class WSLCSessionManagerImpl wil::com_ptr lockedSession; if (FAILED_LOG(entry.Ref->OpenSession(&lockedSession))) { + // Session is gone. Notifying plugins fires OnWslcSessionStopping, which is a plugin + // callback and must not nest inside another plugin callback (ExecutionContext forbids + // it). If a plugin called back into WSLC from within a notification (e.g. creating a + // process from OnWslcVmStopping), defer this lazy notify + prune to a later, non- + // reentrant pass rather than firing OnWslcSessionStopping on top of the current one. + if (InPluginNotificationContext()) + { + return false; // Keep in tracking; clean up on a later pass. + } + // Session is gone: notify plugins (if not already), then drop persistent reference if any. NotifySessionStoppingLockHeld(entry); @@ -174,6 +184,9 @@ class WSLCSessionManagerImpl void NotifySessionStoppingLockHeld(SessionEntry& entry) noexcept; + // Returns true if the calling thread is currently running inside a plugin notification callback. + static bool InPluginNotificationContext() noexcept; + std::atomic m_nextSessionId{1}; std::recursive_mutex m_wslcSessionsLock; diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 35e272a71f..e3b08a8d7a 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -125,6 +125,13 @@ interface IWSLCPluginNotifier : IUnknown // Called when an image is deleted. 'ImageId' is the image identifier. Errors are logged but ignored. HRESULT OnImageDeleted([in] LPCSTR ImageId); + + // Called when the VM backing the session has started (first start or recreation after idle + // teardown). Errors are logged but ignored. + HRESULT OnVmStarted(); + + // Called when the VM backing the session is about to stop. Errors are logged but ignored. + HRESULT OnVmStopping(); }; typedef struct _WSLCImageInformation @@ -467,6 +474,7 @@ typedef struct _WSLCSessionSettings { WSLCFeatureFlags FeatureFlags; WSLCHandle DmesgOutput; WSLCSessionStorageFlags StorageFlags; + ULONG IdleTimeoutSec; // Below options are used for debugging purposes only. [unique] LPCWSTR RootVhdOverride; @@ -603,6 +611,7 @@ typedef struct _WSLCSessionInitSettings WSLCNetworkingMode NetworkingMode; WSLCFeatureFlags FeatureFlags; [unique] LPCSTR RootVhdTypeOverride; + ULONG IdleTimeoutSec; } WSLCSessionInitSettings; [ @@ -690,6 +699,21 @@ interface IWSLCSession : IUnknown HRESULT PruneNetworks([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out, size_is(, *NetworksCount)] WSLCNetworkName** Networks, [out] ULONG* NetworksCount); HRESULT RegisterCrashDumpCallback([in] ICrashDumpCallback* Callback, [out] IUnknown** Subscription); + + // Used only for testing. Synchronously runs the idle-termination teardown path, ignoring the + // activity-count guard so a test can force the VM down even while a container holds an activity + // reference. The idle-termination-enabled (persistent-storage) guard is still honored: a + // tmpfs-backed session is never torn down, since that would lose unrecoverable state. + // WasAlreadyIdle is TRUE when the VM was not running. + HRESULT TriggerIdleTermination([out] BOOL* WasAlreadyIdle); + + // Keeps the VM alive for the duration of a client-side container operation. The CLI performs + // each mutation as two round-trips (OpenContainer followed by the operation) and may stream + // output afterwards. With on-demand VM idle-termination the VM could otherwise tear down + // between those calls, disconnecting the container wrapper and failing the second call with + // RPC_E_DISCONNECTED. The client holds the returned token for the whole operation; releasing + // it (or the client exiting) lets the VM idle-terminate again. + HRESULT BeginContainerOperation([out] IUnknown** Operation); } // diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index 3f7a029e2f..340e49d702 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -326,6 +326,7 @@ std::wstring ContainerService::FormatRelativeTime(ULONGLONG timestamp) int ContainerService::Attach(Session& session, const std::string& id) { + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); @@ -487,6 +488,7 @@ CreateContainerResult ContainerService::Create(Session& session, const std::stri int ContainerService::Start(Session& session, const std::string& id, bool attach) { + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); WSLCContainerStartFlags flags = attach ? WSLCContainerStartFlagsAttach : WSLCContainerStartFlagsNone; @@ -517,6 +519,7 @@ int ContainerService::Start(Session& session, const std::string& id, bool attach void ContainerService::Stop(Session& session, const std::string& id, StopContainerOptions options) { + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); THROW_IF_FAILED_EXCEPT(container->Stop(options.Signal, options.Timeout), WSLC_E_CONTAINER_NOT_RUNNING); @@ -524,6 +527,7 @@ void ContainerService::Stop(Session& session, const std::string& id, StopContain void ContainerService::Kill(Session& session, const std::string& id, WSLCSignal signal) { + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); THROW_IF_FAILED(container->Kill(signal)); @@ -531,6 +535,7 @@ void ContainerService::Kill(Session& session, const std::string& id, WSLCSignal void ContainerService::Delete(Session& session, const std::string& id, bool force) { + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); THROW_IF_FAILED(container->Delete(force ? WSLCDeleteFlagsForce : WSLCDeleteFlagsNone)); @@ -585,6 +590,7 @@ std::vector ContainerService::List( int ContainerService::Exec(Session& session, const std::string& id, ContainerOptions options) { + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); @@ -616,6 +622,7 @@ int ContainerService::Exec(Session& session, const std::string& id, ContainerOpt InspectContainer ContainerService::Inspect(Session& session, const std::string& id) { + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); wil::unique_cotaskmem_ansistring output; @@ -634,6 +641,8 @@ void ContainerService::Export(Session& session, const std::string& id, const std void ContainerService::Export(Session& session, const std::string& id, HANDLE outputHandle) { + [[maybe_unused]] auto operation = session.BeginContainerOperation(); + wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); @@ -661,6 +670,7 @@ void ContainerService::CopyFromContainer(Session& session, const std::string& id void ContainerService::Logs(Session& session, const std::string& id, bool follow, bool timestamps, ULONGLONG since, ULONGLONG until, ULONGLONG tail) { + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); @@ -688,6 +698,7 @@ void ContainerService::Logs(Session& session, const std::string& id, bool follow wsl::windows::common::docker_schema::ContainerStats ContainerService::Stats(Session& session, const std::string& id) { + [[maybe_unused]] auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); wil::unique_cotaskmem_ansistring output; diff --git a/src/windows/wslc/services/SessionModel.h b/src/windows/wslc/services/SessionModel.h index ab6b5824ed..946e8c3bf4 100644 --- a/src/windows/wslc/services/SessionModel.h +++ b/src/windows/wslc/services/SessionModel.h @@ -31,6 +31,16 @@ struct Session return m_session.get(); } + // Acquires an activity token that keeps the VM alive for the duration of a client-side + // container operation (resolve + operate, plus any streamed output). Hold the returned + // pointer for the whole operation; releasing it lets the VM idle-terminate again. + [[nodiscard]] wil::com_ptr BeginContainerOperation() const + { + wil::com_ptr operation; + THROW_IF_FAILED(m_session->BeginContainerOperation(&operation)); + return operation; + } + private: wil::com_ptr m_session; }; diff --git a/src/windows/wslc/services/SessionService.cpp b/src/windows/wslc/services/SessionService.cpp index 86ebcebbe7..8b59509d73 100644 --- a/src/windows/wslc/services/SessionService.cpp +++ b/src/windows/wslc/services/SessionService.cpp @@ -56,11 +56,13 @@ Session SessionService::OpenOrCreateDefaultSession() { auto manager = CreateSessionManager(); - // Null Settings = default session with server-determined name and settings. + // Null Settings = default session with server-determined name and settings. The warning callback + // is consumed during CreateSession (session initialization); it is not retained afterwards. wil::com_ptr session; auto warningCallback = Microsoft::WRL::Make(); THROW_IF_FAILED(manager->CreateSession(nullptr, WSLCSessionFlagsNone, warningCallback.Get(), &session)); wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); + return Session(std::move(session)); } diff --git a/src/windows/wslcsession/CMakeLists.txt b/src/windows/wslcsession/CMakeLists.txt index 6002df79fb..29e0b02992 100644 --- a/src/windows/wslcsession/CMakeLists.txt +++ b/src/windows/wslcsession/CMakeLists.txt @@ -9,6 +9,7 @@ set(SOURCES # Session and container implementation WSLCSession.cpp + WSLCSessionRuntime.cpp WSLCContainer.cpp WSLCVirtualMachine.cpp @@ -44,6 +45,8 @@ set(HEADERS WSLCProcessControl.h WSLCProcessIO.h WSLCSession.h + WSLCSessionRuntime.h + WSLCIdleState.h WSLCSessionFactory.h WSLCSessionReference.h WSLCVirtualMachine.h diff --git a/src/windows/wslcsession/DockerEventTracker.cpp b/src/windows/wslcsession/DockerEventTracker.cpp index f5018d3bca..95c06b0e7b 100644 --- a/src/windows/wslcsession/DockerEventTracker.cpp +++ b/src/windows/wslcsession/DockerEventTracker.cpp @@ -61,7 +61,11 @@ DockerEventTracker::EventTrackingReference::~EventTrackingReference() noexcept Reset(); } -DockerEventTracker::DockerEventTracker(DockerHTTPClient& dockerClient, WSLCSession& session, IORelay& relay) : m_session(session) +DockerEventTracker::DockerEventTracker(WSLCSession& session) : m_session(session) +{ +} + +void DockerEventTracker::Connect(DockerHTTPClient& dockerClient, IORelay& relay) { auto onChunk = [this](const gsl::span& buffer) { if (!buffer.empty()) // docker inserts empty lines between events, skip those. diff --git a/src/windows/wslcsession/DockerEventTracker.h b/src/windows/wslcsession/DockerEventTracker.h index e040c7592f..68b8195970 100644 --- a/src/windows/wslcsession/DockerEventTracker.h +++ b/src/windows/wslcsession/DockerEventTracker.h @@ -64,9 +64,14 @@ class DockerEventTracker using ContainerStateChangeCallback = std::function, std::uint64_t)>; using VolumeEventCallback = std::function; - DockerEventTracker(DockerHTTPClient& dockerClient, WSLCSession& session, IORelay& relay); + explicit DockerEventTracker(WSLCSession& session); ~DockerEventTracker(); + // Binds the tracker to a VM's docker client and IO relay. Called on every VM start. Existing + // container/volume registrations are preserved across (re)connects so callers do not re-register + // when the VM is idle-terminated and later restarted. + void Connect(DockerHTTPClient& dockerClient, IORelay& relay); + 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; diff --git a/src/windows/wslcsession/IORelay.cpp b/src/windows/wslcsession/IORelay.cpp index 6677bca87a..09f5e05cd4 100644 --- a/src/windows/wslcsession/IORelay.cpp +++ b/src/windows/wslcsession/IORelay.cpp @@ -68,11 +68,20 @@ void IORelay::Stop() } } +bool IORelay::IsRelayThread() const noexcept +{ + return m_thread.get_id() == std::this_thread::get_id(); +} + void IORelay::Run() try { common::wslutil::SetThreadDescription(L"IORelay"); + // Handle callbacks dispatched from this thread (e.g. unexpected VM exit) can tear the VM down, + // releasing cross-process COM proxies, so join the process MTA to avoid RPC_E_WRONG_THREAD. + const auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED); + windows::common::io::MultiHandleWait io; // N.B. All the IO must happen on the thread. diff --git a/src/windows/wslcsession/IORelay.h b/src/windows/wslcsession/IORelay.h index 879d3fee13..844c16dee6 100644 --- a/src/windows/wslcsession/IORelay.h +++ b/src/windows/wslcsession/IORelay.h @@ -30,6 +30,12 @@ class IORelay void Stop(); + // Returns true if the calling thread is the IORelay's own worker thread (i.e. the call + // is being made from a handle callback). Destroying the IORelay from this thread would + // join the thread with itself and call std::terminate(), so callers that may run on the + // relay thread must check this before destroying the object. + bool IsRelayThread() const noexcept; + private: void Start(); void Run(); diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 24c7399932..602a3be1d4 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -36,8 +36,10 @@ using wsl::windows::common::io::OverlappedIOHandle; using wsl::windows::common::io::ReadHandle; using wsl::windows::common::io::RelayHandle; using wsl::windows::service::wslc::ContainerPortMapping; +using wsl::windows::service::wslc::DockerEventTracker; using wsl::windows::service::wslc::DockerHTTPClient; using wsl::windows::service::wslc::DockerHTTPException; +using wsl::windows::service::wslc::IORelay; using wsl::windows::service::wslc::IWSLCVolume; using wsl::windows::service::wslc::NetworkEntry; using wsl::windows::service::wslc::RelayedProcessIO; @@ -53,6 +55,7 @@ using wsl::windows::service::wslc::WSLCPortMapping; using wsl::windows::service::wslc::WSLCSession; using wsl::windows::service::wslc::WSLCVirtualMachine; using wsl::windows::service::wslc::WSLCVolumeMount; +using wsl::windows::service::wslc::WSLCVolumes; using namespace wsl::windows::common::io; using namespace wsl::windows::common::docker_schema; @@ -528,7 +531,7 @@ WSLCPortMapping ContainerPortMapping::Serialize() const WSLCContainerImpl::WSLCContainerImpl( WSLCSession& wslcSession, - WSLCVirtualMachine& virtualMachine, + WSLCSessionRuntime& runtime, IWSLCPluginNotifier* pluginNotifier, std::string&& Id, std::string&& Name, @@ -536,40 +539,39 @@ WSLCContainerImpl::WSLCContainerImpl( std::string NetworkMode, std::vector&& volumes, std::vector&& namedVolumes, - WSLCVolumes& Volumes, std::vector&& ports, std::map&& labels, std::function&& onDeleted, - DockerEventTracker& EventTracker, - DockerHTTPClient& DockerClient, - IORelay& Relay, WSLCContainerState InitialState, std::uint64_t CreatedAt, WSLCProcessFlags InitProcessFlags, WSLCContainerFlags ContainerFlags) : m_wslcSession(wslcSession), m_pluginNotifier(pluginNotifier), - m_virtualMachine(virtualMachine), + m_runtime(runtime), m_name(std::move(Name)), m_image(std::move(Image)), m_networkMode(std::move(NetworkMode)), m_id(std::move(Id)), m_mountedVolumes(std::move(volumes)), m_namedVolumes(std::move(namedVolumes)), - m_volumes(Volumes), m_mappedPorts(std::move(ports)), m_labels(std::move(labels)), m_comWrapper(wil::MakeOrThrow(wslcSession, std::move(onDeleted))), - m_dockerClient(DockerClient), - m_eventTracker(EventTracker), - m_ioRelay(Relay), - m_containerEvents(EventTracker.RegisterContainerStateUpdates( + m_containerEvents(runtime.Events().RegisterContainerStateUpdates( 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), m_containerFlags(ContainerFlags) { + // Acquire the activity hold up front for a container recovered in the running state, so it keeps + // the VM alive even before any client opens its wrapper. A merely-created (never-started) + // container does not pin the VM: its metadata survives teardown and the VM restarts on next use. + if (m_state == WslcContainerStateRunning) + { + m_activityHold = ActivityRef(m_wslcSession.Runtime().IdleStateShared()); + } } WSLCContainerImpl::~WSLCContainerImpl() @@ -623,6 +625,31 @@ void WSLCContainerImpl::Initialize() m_comWrapper->Initialize(weak_from_this()); } +WSLCVirtualMachine& WSLCContainerImpl::Vm() const +{ + return m_runtime.Vm(); +} + +DockerHTTPClient& WSLCContainerImpl::Docker() const +{ + return m_runtime.Docker(); +} + +WSLCVolumes& WSLCContainerImpl::Volumes() const +{ + return m_runtime.Volumes(); +} + +DockerEventTracker& WSLCContainerImpl::Events() const +{ + return m_runtime.Events(); +} + +IORelay& WSLCContainerImpl::Relay() const +{ + return *m_runtime.Relay(); +} + void WSLCContainerImpl::SetExitCode(int ExitCode) noexcept { std::lock_guard processesLock{m_processesLock}; @@ -699,7 +726,7 @@ void WSLCContainerImpl::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* try { - ioHandle = m_dockerClient.AttachContainer(m_id, DetachKeys == nullptr ? std::nullopt : std::optional(DetachKeys)); + ioHandle = Docker().AttachContainer(m_id, DetachKeys == nullptr ? std::nullopt : std::optional(DetachKeys)); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to attach to container '%hs'", m_id.c_str()); @@ -730,7 +757,7 @@ void WSLCContainerImpl::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* handles.emplace_back(std::make_unique( std::move(ioHandle), std::move(stdoutWrite), std::move(stderrWrite), DockerIORelayHandle::Format::Raw)); - m_ioRelay.AddHandles(std::move(handles)); + Relay().AddHandles(std::move(handles)); *Stdin = common::wslutil::ToCOMOutputHandle(reinterpret_cast(stdinWrite.get()), GENERIC_WRITE | SYNCHRONIZE, WSLCHandleTypePipe); @@ -777,11 +804,11 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt if (WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty)) { io = std::make_unique(TypedHandle{ - wil::unique_handle{(HANDLE)m_dockerClient.AttachContainer(m_id, detachKeys).release()}, WSLCHandleTypeSocket}); + wil::unique_handle{(HANDLE)Docker().AttachContainer(m_id, detachKeys).release()}, WSLCHandleTypeSocket}); } else { - wil::unique_handle stream{reinterpret_cast(m_dockerClient.AttachContainer(m_id, detachKeys).release())}; + wil::unique_handle stream{reinterpret_cast(Docker().AttachContainer(m_id, detachKeys).release())}; io = CreateRelayedProcessIO(std::move(stream), m_initProcessFlags); } } @@ -792,7 +819,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt THROW_DOCKER_USER_ERROR_MSG(e, "Failed to attach to container '%hs' during start", m_id.c_str()); } - auto control = std::make_unique(*this, m_dockerClient); + auto control = std::make_unique(*this, Docker()); std::lock_guard processesLock{m_processesLock}; m_initProcessControl = control.get(); @@ -808,7 +835,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt std::vector unavailableVolumes; for (const auto& volumeName : m_namedVolumes) { - const auto [code, message] = m_volumes.GetVolumeStatus(volumeName); + const auto [code, message] = Volumes().GetVolumeStatus(volumeName); if (FAILED(code)) { EMIT_USER_WARNING(Localization::MessageWslcVolumeNotAvailableReason(volumeName, message)); @@ -821,7 +848,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt Localization::MessageWslcVolumeNotAvailable(wsl::shared::string::Join(unavailableVolumes, ',')), !unavailableVolumes.empty()); - auto volumeCleanup = MountVolumes(m_mountedVolumes, m_virtualMachine); + auto volumeCleanup = MountVolumes(m_mountedVolumes, Vm()); auto portCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { UnmapPorts(); }); MapPorts(); @@ -831,7 +858,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt try { - m_dockerClient.StartContainer(m_id, detachKeys); + Docker().StartContainer(m_id, detachKeys); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to start container '%hs'", m_id.c_str()); @@ -839,7 +866,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt { try { - m_dockerClient.ResizeContainerTty(m_id, StartOptions->TtyRows, StartOptions->TtyColumns); + Docker().ResizeContainerTty(m_id, StartOptions->TtyRows, StartOptions->TtyColumns); } CATCH_LOG(); } @@ -854,7 +881,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt LOG_HR_MSG(pluginResult, "Plugin rejected start of container '%hs' (0x%x)", m_id.c_str(), pluginResult); try { - m_dockerClient.StopContainer(m_id.c_str(), {}, {}); + Docker().StopContainer(m_id.c_str(), {}, {}); } catch (...) { @@ -968,7 +995,7 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill) { if (Kill) { - m_dockerClient.SignalContainer(m_id, SignalArg); + Docker().SignalContainer(m_id, SignalArg); if (!waitForStop) { @@ -983,7 +1010,7 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill) TimeoutArg = TimeoutSeconds; } - m_dockerClient.StopContainer(m_id, SignalArg, TimeoutArg); + Docker().StopContainer(m_id, SignalArg, TimeoutArg); } } catch (const DockerHTTPException& e) @@ -1053,6 +1080,86 @@ __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl:: return comWrapper; } +void WSLCContainerImpl::OnVmTornDown() noexcept +try +{ + auto lock = m_lock.lock_exclusive(); + + // Only running containers have an init process and VM activity hold to reconcile. Port reservations + // self-neuter when the VM's table is destroyed; RecoverPorts re-reserves them on restart. + if (m_state != WslcContainerStateRunning) + { + return; + } + + // VM torn down while running (idle-termination or crash). Mirror the stop path: record a synthetic + // SIGKILL exit (else a later die/Destroy event asserts on a missing code), release VM-scoped + // resources, drop to Exited (releasing the activity hold). Wrapper stays connected so COM refs stay + // valid; RecoverState reattaches it next start. + SetExitCode(128 + WSLCSignalSIGKILL); + ReleaseProcesses(); + ReleaseRuntimeResources(); + + Transition(WslcContainerStateExited); + SignalInitProcessExit(); +} +CATCH_LOG() + +void WSLCContainerImpl::RecoverPorts(const common::docker_schema::ContainerInfo& dockerContainer) +{ + auto lock = m_lock.lock_exclusive(); + + // Re-register VM-scoped port reservations (self-neutered when the old VM's table died) against the + // restarted VM using the numbers recorded at create time, restoring bridge-mode forwarding. + const bool allocateVmPorts = NetworkModeAllocatesVmPorts(m_networkMode); + if (!allocateVmPorts) + { + return; + } + + auto metadataIt = dockerContainer.Labels.find(WSLCContainerMetadataLabel); + if (metadataIt == dockerContainer.Labels.end()) + { + return; + } + + auto metadata = ParseContainerMetadata(metadataIt->second.c_str()); + + std::vector ports; + ports.reserve(metadata.Ports.size()); + for (const auto& e : metadata.Ports) + { + auto& inserted = ports.emplace_back(ContainerPortMapping{VMPortMapping::FromContainerMetaData(e), e.ContainerPort}); + + auto allocation = Vm().TryAllocatePort(e.VmPort, e.Family, e.Protocol); + + THROW_HR_IF_MSG( + HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), !allocation, "Port %hu is in use, cannot recover container %hs", e.VmPort, m_id.c_str()); + + inserted.VmMapping.AssignVmPort(allocation); + } + + m_mappedPorts = std::move(ports); +} + +unique_com_disconnect WSLCContainerImpl::RemoveExitedAutoRemoveSurvivor(bool& Removed) +{ + Removed = false; + + auto lock = m_lock.lock_exclusive(); + + // Only an Exited --rm survivor needs cleanup: OnVmTornDown forced a running --rm container to Exited + // without the auto-remove delete. One that exited normally was already deleted by OnStopped. + if (m_state != WslcContainerStateExited || WI_IsFlagClear(m_containerFlags, WSLCContainerFlagsRm)) + { + return {}; + } + + auto wrapper = DeleteExclusiveLockHeld(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes); + Removed = true; + return wrapper; +} + void WSLCContainerImpl::Delete(WSLCDeleteFlags Flags) { // N.B. wrapper must be destroyed after m_lock is released, since its destructor calls Disconnect(). @@ -1085,7 +1192,7 @@ __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl:: try { - m_dockerClient.DeleteContainer(m_id, WI_IsFlagSet(Flags, WSLCDeleteFlagsForce), WI_IsFlagSet(Flags, WSLCDeleteFlagsDeleteVolumes)); + Docker().DeleteContainer(m_id, WI_IsFlagSet(Flags, WSLCDeleteFlagsForce), WI_IsFlagSet(Flags, WSLCDeleteFlagsDeleteVolumes)); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to delete container '%hs'", m_id.c_str()); @@ -1101,7 +1208,7 @@ void WSLCContainerImpl::Export(WSLCHandle OutHandle) const THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_IS_RUNNING, Localization::MessageWslcContainerIsRunning(m_id), m_state == WslcContainerStateRunning); std::pair SocketCodePair; - SocketCodePair = m_dockerClient.ExportContainer(m_id); + SocketCodePair = Docker().ExportContainer(m_id); auto userHandle = m_wslcSession.OpenUserHandle(OutHandle); @@ -1149,7 +1256,7 @@ void WSLCContainerImpl::UploadArchive(WSLCHandle TarHandle, LPCSTR DestPath, ULO contentLength = ContentSize; } - auto requestContext = m_dockerClient.PutArchive(m_id, DestPath, contentLength); + auto requestContext = Docker().PutArchive(m_id, DestPath, contentLength); auto userHandle = m_wslcSession.OpenUserHandle(TarHandle); @@ -1204,7 +1311,7 @@ void WSLCContainerImpl::DownloadArchive(LPCSTR SrcPath, WSLCHandle OutHandle) co { auto lock = m_lock.lock_shared(); - auto [statusCode, socket, isChunked] = m_dockerClient.GetArchive(m_id, SrcPath); + auto [statusCode, socket, isChunked] = Docker().GetArchive(m_id, SrcPath); auto userHandle = m_wslcSession.OpenUserHandle(OutHandle); @@ -1325,12 +1432,12 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, const WSLCProces try { - auto result = m_dockerClient.CreateExec(m_id, request); + auto result = Docker().CreateExec(m_id, request); // N.B. There's no way to delete a created exec instance, it is removed when the container is deleted. wil::unique_handle stream{ - (HANDLE)m_dockerClient + (HANDLE)Docker() .StartExec(result.Id, common::docker_schema::StartExec{.Tty = request.Tty, .ConsoleSize = request.ConsoleSize}) .release()}; @@ -1344,7 +1451,7 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, const WSLCProces io = CreateRelayedProcessIO(std::move(stream), Options->Flags); } - auto control = std::make_shared(*this, result.Id, m_dockerClient, m_eventTracker); + auto control = std::make_shared(*this, result.Id, Docker(), Events()); { std::lock_guard processesLock{m_processesLock}; @@ -1367,7 +1474,7 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, const WSLCProces do { - auto state = m_dockerClient.InspectExec(result.Id); + auto state = Docker().InspectExec(result.Id); if (state.Running && state.Pid > 0) { control->SetPid(state.Pid); @@ -1390,6 +1497,12 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, const WSLCProces } while (!control->GetExitEvent().wait(100)); auto process = wil::MakeOrThrow(std::move(control), std::move(io), Options->Flags); + + // The exec'd process wrapper is handed to the client and is not retained internally, so its + // lifetime tracks the client's proxy. Bind a keep-alive token to it so the idle worker does + // not tear the VM down (killing the process) while the client still holds the proxy. + process->SetKeepAliveToken(m_wslcSession.CreateActivityToken()); + THROW_IF_FAILED(process.CopyTo(__uuidof(IWSLCProcess), (void**)Process)); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to exec process in container %hs", m_id.c_str()); @@ -1533,15 +1646,15 @@ std::shared_ptr WSLCContainerImpl::Create( const WSLCContainerOptions& containerOptions, const std::string& containerName, WSLCSession& wslcSession, - WSLCVirtualMachine& virtualMachine, + WSLCSessionRuntime& runtime, IWSLCPluginNotifier* pluginNotifier, const std::unordered_map& sessionNetworks, - WSLCVolumes& volumesManager, - std::function&& OnDeleted, - DockerEventTracker& EventTracker, - DockerHTTPClient& DockerClient, - IORelay& IoRelay) + std::function&& OnDeleted) { + auto& virtualMachine = runtime.Vm(); + auto& DockerClient = runtime.Docker(); + auto& EventTracker = runtime.Events(); + common::docker_schema::CreateContainer request; request.Image = containerOptions.Image; @@ -1966,7 +2079,7 @@ std::shared_ptr WSLCContainerImpl::Create( auto container = std::make_shared( wslcSession, - virtualMachine, + runtime, pluginNotifier, std::move(result.Id), CleanContainerName(inspectData.Name), @@ -1974,13 +2087,9 @@ std::shared_ptr WSLCContainerImpl::Create( std::move(networkMode), std::move(volumes), std::move(namedVolumes), - volumesManager, std::move(mappedPorts), std::move(labels), std::move(OnDeleted), - EventTracker, - DockerClient, - IoRelay, WslcContainerStateCreated, ParseDockerTimestamp(inspectData.Created), containerOptions.InitProcessOptions.Flags, @@ -1995,14 +2104,13 @@ std::shared_ptr WSLCContainerImpl::Create( std::shared_ptr WSLCContainerImpl::Open( const common::docker_schema::ContainerInfo& dockerContainer, WSLCSession& wslcSession, - WSLCVirtualMachine& virtualMachine, + WSLCSessionRuntime& runtime, IWSLCPluginNotifier* pluginNotifier, - WSLCVolumes& volumes, - std::function&& OnDeleted, - DockerEventTracker& EventTracker, - DockerHTTPClient& DockerClient, - IORelay& ioRelay) + std::function&& OnDeleted) { + auto& virtualMachine = runtime.Vm(); + auto& DockerClient = runtime.Docker(); + // Extract container name from Docker's names list. std::string name = ExtractContainerName(dockerContainer.Names, dockerContainer.Id); @@ -2060,7 +2168,7 @@ std::shared_ptr WSLCContainerImpl::Open( auto container = std::make_shared( wslcSession, - virtualMachine, + runtime, pluginNotifier, std::string(dockerContainer.Id), std::move(name), @@ -2068,13 +2176,9 @@ std::shared_ptr WSLCContainerImpl::Open( std::move(networkMode), std::move(metadata.Volumes), std::move(namedVolumes), - volumes, std::move(ports), std::move(labels), std::move(OnDeleted), - EventTracker, - DockerClient, - ioRelay, DockerStateToWSLCState(dockerContainer.State), static_cast(dockerContainer.Created), metadata.InitProcessFlags, @@ -2087,11 +2191,21 @@ std::shared_ptr WSLCContainerImpl::Open( { auto inspectData = DockerClient.InspectContainer(dockerContainer.Id); auto state = DockerStateToWSLCState(dockerContainer.State); - const auto& timestamp = (state == WslcContainerStateRunning) ? inspectData.State.StartedAt : inspectData.State.FinishedAt; - if (!timestamp.empty()) + if (state == WslcContainerStateCreated) { - container->m_stateChangedAt = ParseDockerTimestamp(timestamp); + // A created-but-never-started container has no StartedAt/FinishedAt; its state last + // changed when it was created. + container->m_stateChangedAt = static_cast(dockerContainer.Created); + } + else + { + const auto& timestamp = (state == WslcContainerStateRunning) ? inspectData.State.StartedAt : inspectData.State.FinishedAt; + + if (!timestamp.empty()) + { + container->m_stateChangedAt = ParseDockerTimestamp(timestamp); + } } } catch (...) @@ -2123,7 +2237,7 @@ void WSLCContainerImpl::Inspect(LPSTR* Output) const std::string WSLCContainerImpl::InspectLockHeld() const { // Get Docker inspect data - auto dockerInspect = m_dockerClient.InspectContainer(m_id); + auto dockerInspect = Docker().InspectContainer(m_id); // Convert to WSLC schema auto wslcInspect = BuildInspectContainer(dockerInspect); @@ -2139,7 +2253,7 @@ void WSLCContainerImpl::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle wil::unique_socket socket; try { - socket = m_dockerClient.ContainerLogs(m_id, Flags, Since, Until, Tail); + socket = Docker().ContainerLogs(m_id, Flags, Since, Until, Tail); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to get logs from '%hs'", m_id.c_str()); @@ -2149,7 +2263,7 @@ void WSLCContainerImpl::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle auto [ttyRead, ttyWrite] = common::wslutil::OpenAnonymousPipe(0, true, true); auto handle = std::make_unique>(std::move(socket), std::move(ttyWrite)); - m_ioRelay.AddHandle(std::move(handle)); + Relay().AddHandle(std::move(handle)); *Stdout = common::wslutil::ToCOMOutputHandle(ttyRead.get(), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe); } @@ -2162,7 +2276,7 @@ void WSLCContainerImpl::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle auto handle = std::make_unique( std::move(socket), std::move(stdoutWrite), std::move(stderrWrite), DockerIORelayHandle::Format::HttpChunked); - m_ioRelay.AddHandle(std::move(handle)); + Relay().AddHandle(std::move(handle)); *Stdout = common::wslutil::ToCOMOutputHandle(stdoutRead.get(), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe); *Stderr = common::wslutil::ToCOMOutputHandle(stderrRead.get(), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe); @@ -2175,7 +2289,7 @@ void WSLCContainerImpl::Stats(LPSTR* Output) const try { - auto stats = m_dockerClient.ContainerStats(m_id); + auto stats = Docker().ContainerStats(m_id); // Always inject the authoritative id and name from this instance. // The response may omit them or use inconsistent casing. @@ -2222,7 +2336,7 @@ std::unique_ptr WSLCContainerImpl::CreateRelayedProcessIO(wil: ioHandles.emplace_back(std::make_unique( std::move(stream), std::move(stdoutWrite), std::move(stderrWrite), common::io::DockerIORelayHandle::Format::Raw)); - m_ioRelay.AddHandles(std::move(ioHandles)); + Relay().AddHandles(std::move(ioHandles)); return std::make_unique(std::move(fds)); } @@ -2246,8 +2360,7 @@ void WSLCContainerImpl::MapPorts() } else { - auto allocatedPort = - m_virtualMachine.TryAllocatePort(e.ContainerPort, e.VmMapping.BindAddress.si_family, e.VmMapping.Protocol); + auto allocatedPort = Vm().TryAllocatePort(e.ContainerPort, e.VmMapping.BindAddress.si_family, e.VmMapping.Protocol); THROW_HR_WITH_USER_ERROR_IF( HRESULT_FROM_WIN32(WSAEADDRINUSE), wsl::shared::Localization::MessageWslcPortInUse(FormatPortEndpoint(e), m_id), !allocatedPort); @@ -2260,7 +2373,7 @@ void WSLCContainerImpl::MapPorts() try { - m_virtualMachine.MapPort(e.VmMapping); + Vm().MapPort(e.VmMapping); } catch (...) { @@ -2322,7 +2435,21 @@ __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::ReleaseRuntimeRes // Release runtime resources (port relays, volume mounts) that were set up at Start(). UnmapPorts(); - UnmountVolumes(m_mountedVolumes, m_virtualMachine); + + // A VM that already exited (crash / external kill) has dropped every guest mount, so calling + // UnmountWindowsFolder would only block on the RPC timeout and emit spurious unmount-failed + // warnings. Mark the mounts inactive without touching the dead VM. + if (m_runtime.VmExited()) + { + for (auto& volume : m_mountedVolumes) + { + volume.Mounted = false; + } + } + else + { + UnmountVolumes(m_mountedVolumes, Vm()); + } } __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::ReleaseResources() @@ -2368,6 +2495,25 @@ __requires_lock_held(m_lock) void WSLCContainerImpl::Transition(WSLCContainerSta m_state = State; m_stateChangedAt = stateChangedAt.value_or(static_cast(std::time(nullptr))); + + // Keep the VM alive while this container is Running and release the hold once it leaves that + // state, even when no client holds the wrapper (e.g. a detached `run -d` container). Dropping + // the hold on the transition out of Running is what lets an otherwise-idle VM be torn down; a + // Created or Exited container does not pin the VM, since its metadata survives teardown. + UpdateActivityHoldLockHeld(); +} + +__requires_lock_held(m_lock) void WSLCContainerImpl::UpdateActivityHoldLockHeld() noexcept +{ + const bool active = (m_state == WslcContainerStateRunning); + if (active && !m_activityHold) + { + m_activityHold = ActivityRef(m_wslcSession.Runtime().IdleStateShared()); + } + else if (!active && m_activityHold) + { + m_activityHold.reset(); + } } WSLCContainer::WSLCContainer(WSLCSession& session, std::function&& OnDeleted) : @@ -2376,6 +2522,7 @@ WSLCContainer::WSLCContainer(WSLCSession& session, std::functionFlags, ~WSLCProcessFlagsValid), "Invalid flags: 0x%x", Options->Flags); *Process = nullptr; + + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Exec, Options, StartOptions, Process); } +CATCH_RETURN(); HRESULT WSLCContainer::Stop(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds) +try { WSLCExecutionContext context(&m_session); + // Hold a VM lease for the whole operation: --rm containers self-delete during Stop, which + // disconnects the wrapper and drops activity. Without the lease, the idle worker can fire + // during the post-stop destroy wait (up to 60s) and tear the VM down mid-call. + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Stop, Signal, TimeoutSeconds, false); } +CATCH_RETURN(); HRESULT WSLCContainer::Kill(_In_ WSLCSignal Signal) +try { WSLCExecutionContext context(&m_session); + // Hold a VM lease for the same reason as Stop(): --rm can self-delete and drop activity. + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Stop, Signal, {}, true); } +CATCH_RETURN(); HRESULT WSLCContainer::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, IWarningCallback* WarningCallback) try @@ -2478,11 +2641,13 @@ try THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCContainerStartFlagsValid), "Invalid flags: 0x%x", Flags); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Start, Flags, StartOptions); } CATCH_RETURN(); HRESULT WSLCContainer::Inspect(LPSTR* Output) +try { WSLCExecutionContext context(&m_session); @@ -2490,8 +2655,10 @@ HRESULT WSLCContainer::Inspect(LPSTR* Output) *Output = nullptr; + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Inspect, Output); } +CATCH_RETURN(); HRESULT WSLCContainer::Stats(LPSTR* Output) try @@ -2501,6 +2668,8 @@ try RETURN_HR_IF(E_POINTER, Output == nullptr); *Output = nullptr; + + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Stats, Output); } CATCH_RETURN(); @@ -2513,6 +2682,11 @@ try THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCDeleteFlagsValid), "Invalid flags: 0x%x", Flags); // Special case for Delete(): If deletion is successful, notify the WSLCSession that the container has been deleted. + // Hold a VM lease across the whole operation: deleting a container makes it inactive and + // can trigger an idle teardown. Without the lease the idle worker could take the session + // lock exclusively and clear m_containers (destroying this container) concurrently, racing + // the delete and inverting the container->session lock order. + auto vmLease = m_session.Runtime().AcquireVmLease(); auto [lock, impl] = LockImpl(); impl->Delete(Flags); @@ -2538,29 +2712,40 @@ try CATCH_LOG(); HRESULT WSLCContainer::Export(WSLCHandle TarHandle) +try { WSLCExecutionContext context(&m_session); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Export, TarHandle); } +CATCH_RETURN(); HRESULT WSLCContainer::UploadArchive(WSLCHandle TarHandle, LPCSTR DestPath, ULONGLONG ContentSize) +try { WSLCExecutionContext context(&m_session); RETURN_HR_IF(E_POINTER, DestPath == nullptr); RETURN_HR_IF(E_INVALIDARG, DestPath[0] == '\0'); + + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::UploadArchive, TarHandle, DestPath, ContentSize); } +CATCH_RETURN(); HRESULT WSLCContainer::DownloadArchive(LPCSTR SrcPath, WSLCHandle OutHandle) +try { WSLCExecutionContext context(&m_session); RETURN_HR_IF(E_POINTER, SrcPath == nullptr); RETURN_HR_IF(E_INVALIDARG, SrcPath[0] == '\0'); + + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::DownloadArchive, SrcPath, OutHandle); } +CATCH_RETURN(); HRESULT WSLCContainer::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) try @@ -2573,6 +2758,7 @@ try *Stdout = {}; *Stderr = {}; + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Logs, Flags, Stdout, Stderr, Since, Until, Tail); } CATCH_RETURN(); @@ -2687,7 +2873,7 @@ void WSLCContainerImpl::ConnectToNetwork(const WSLCNetworkConnectionOptions* Opt try { - m_dockerClient.ConnectContainerToNetwork(Options->NetworkName, request); + Docker().ConnectContainerToNetwork(Options->NetworkName, request); } catch (const DockerHTTPException& e) { @@ -2719,7 +2905,7 @@ void WSLCContainerImpl::DisconnectFromNetwork(LPCSTR NetworkName) try { - m_dockerClient.DisconnectContainerFromNetwork(NetworkName, request); + Docker().DisconnectContainerFromNetwork(NetworkName, request); } catch (const DockerHTTPException& e) { @@ -2750,6 +2936,8 @@ HRESULT WSLCContainer::ConnectToNetwork(const WSLCNetworkConnectionOptions* Opti try { COMServiceExecutionContext context; + + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::ConnectToNetwork, Options); } CATCH_RETURN(); @@ -2758,6 +2946,8 @@ HRESULT WSLCContainer::DisconnectFromNetwork(LPCSTR NetworkName) try { COMServiceExecutionContext context; + + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::DisconnectFromNetwork, NetworkName); } CATCH_RETURN(); diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index 23cb60e786..33e4af6ac8 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -16,6 +16,7 @@ Module Name: #include "ServiceProcessLauncher.h" #include "WSLCSession.h" +#include "WSLCIdleState.h" #include "DockerEventTracker.h" #include "DockerHTTPClient.h" #include "WSLCProcessControl.h" @@ -32,6 +33,7 @@ namespace wsl::windows::service::wslc { class WSLCContainer; class WSLCSession; +class WSLCSessionRuntime; class WSLCVolumes; class unique_com_disconnect @@ -72,7 +74,7 @@ class WSLCContainerImpl : public std::enable_shared_from_this WSLCContainerImpl( WSLCSession& wslcSession, - WSLCVirtualMachine& virtualMachine, + WSLCSessionRuntime& runtime, IWSLCPluginNotifier* pluginNotifier, std::string&& Id, std::string&& Name, @@ -80,13 +82,9 @@ class WSLCContainerImpl : public std::enable_shared_from_this std::string NetworkMode, std::vector&& volumes, std::vector&& namedVolumes, - WSLCVolumes& Volumes, std::vector&& ports, std::map&& labels, std::function&& OnDeleted, - DockerEventTracker& EventTracker, - DockerHTTPClient& DockerClient, - IORelay& Relay, WSLCContainerState InitialState, std::uint64_t CreatedAt, WSLCProcessFlags InitProcessFlags, @@ -122,6 +120,21 @@ class WSLCContainerImpl : public std::enable_shared_from_this WSLCContainerState State() const noexcept; std::vector GetPorts() const; + // Reconciles a surviving wrapper after its VM was torn down (idle-termination or crash) while the + // container was running: records a synthetic init-process exit, releases VM-scoped resources and + // drops to Exited (releasing the VM activity hold). Keeps the wrapper connected so client COM + // references stay valid across the VM restart. + void OnVmTornDown() noexcept; + + // Re-registers a survivor's VM-scoped port allocations against the restarted VM (see OnVmTornDown). + void RecoverPorts(const common::docker_schema::ContainerInfo& dockerContainer); + + // Honors --rm for a survivor that was running when the VM was torn down: OnVmTornDown forced it to + // Exited but deferred the auto-remove delete while dockerd was down. Removes it now that the VM is + // back, mirroring OnStopped's Running->Exited delete. Sets Removed and returns the disconnect + // wrapper (destroy after dropping the container from tracking) when it deletes; otherwise a no-op. + [[nodiscard]] unique_com_disconnect RemoveExitedAutoRemoveSurvivor(bool& Removed); + __requires_lock_held(m_lock) void Transition(WSLCContainerState State, std::optional stateChangedAt = std::nullopt) noexcept; const std::string& ID() const noexcept; @@ -137,25 +150,17 @@ class WSLCContainerImpl : public std::enable_shared_from_this const WSLCContainerOptions& Options, const std::string& Name, WSLCSession& wslcSession, - WSLCVirtualMachine& virtualMachine, + WSLCSessionRuntime& runtime, IWSLCPluginNotifier* pluginNotifier, const std::unordered_map& SessionNetworks, - WSLCVolumes& Volumes, - std::function&& OnDeleted, - DockerEventTracker& EventTracker, - DockerHTTPClient& DockerClient, - IORelay& Relay); + std::function&& OnDeleted); static std::shared_ptr Open( const common::docker_schema::ContainerInfo& DockerContainer, WSLCSession& wslcSession, - WSLCVirtualMachine& virtualMachine, + WSLCSessionRuntime& runtime, IWSLCPluginNotifier* pluginNotifier, - WSLCVolumes& Volumes, - std::function&& OnDeleted, - DockerEventTracker& EventTracker, - DockerHTTPClient& DockerClient, - IORelay& Relay); + std::function&& OnDeleted); private: __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect DeleteExclusiveLockHeld(WSLCDeleteFlags Flags); @@ -180,8 +185,22 @@ class WSLCContainerImpl : public std::enable_shared_from_this void MapPorts(); void UnmapPorts(); + // Acquires or releases the activity hold so it is held exactly while the container is Running, + // keeping the session's VM alive across idle teardown. + __requires_lock_held(m_lock) void UpdateActivityHoldLockHeld() noexcept; + __requires_shared_lock_held(m_lock) std::string InspectLockHeld() const; + // Accessors for the session's VM-scoped resources. The container outlives any single VM: it + // survives idle-termination and is reused when the VM restarts. These fetch the current VM's + // objects from the (stable) runtime rather than caching references that would dangle across a + // restart. They are only valid while a VM lease is held (i.e. the VM is running). + WSLCVirtualMachine& Vm() const; + DockerHTTPClient& Docker() const; + WSLCVolumes& Volumes() const; + DockerEventTracker& Events() const; + IORelay& Relay() const; + mutable wil::srwlock m_lock; std::string m_name; std::string m_image; @@ -205,25 +224,26 @@ class WSLCContainerImpl : public std::enable_shared_from_this // Must be acquired before m_lock when both are needed. std::mutex m_stopLock; - DockerHTTPClient& m_dockerClient; + WSLCSessionRuntime& m_runtime; std::uint64_t m_stateChangedAt{static_cast(std::time(nullptr))}; std::uint64_t m_createdAt{}; WSLCContainerState m_state = WslcContainerStateInvalid; WSLCSession& m_wslcSession; IWSLCPluginNotifier* m_pluginNotifier; - WSLCVirtualMachine& m_virtualMachine; std::vector m_mappedPorts; std::vector m_mountedVolumes; std::vector m_namedVolumes; - WSLCVolumes& m_volumes; std::map m_labels; Microsoft::WRL::ComPtr m_comWrapper; - DockerEventTracker& m_eventTracker; DockerEventTracker::EventTrackingReference m_containerEvents; - IORelay& m_ioRelay; std::string m_networkMode; + + // Held (non-empty) exactly while the container is Running so the session's VM stays alive even + // when no client holds the wrapper (e.g. a detached `run -d` container). Maintained by + // UpdateActivityHoldLockHeld(); released automatically when the container is destroyed. + ActivityRef m_activityHold; }; class DECLSPEC_UUID("B1F1C4E3-C225-4CAE-AD8A-34C004DE1AE4") WSLCContainer diff --git a/src/windows/wslcsession/WSLCExecutionContext.h b/src/windows/wslcsession/WSLCExecutionContext.h index 5d75bfed14..62b21ed8c6 100644 --- a/src/windows/wslcsession/WSLCExecutionContext.h +++ b/src/windows/wslcsession/WSLCExecutionContext.h @@ -27,7 +27,8 @@ class WSLCExecutionContext : public wsl::windows::common::COMServiceExecutionCon protected: bool CollectUserWarning(const std::wstring& warning) override { - if (m_warningCallback != nullptr) + IWarningCallback* callback = m_warningCallback; + if (callback != nullptr) { std::unique_ptr comCallback; if (m_session != nullptr) @@ -35,7 +36,7 @@ class WSLCExecutionContext : public wsl::windows::common::COMServiceExecutionCon comCallback = std::make_unique(m_session->RegisterUserCOMCallback()); } - auto hr = m_warningCallback->OnWarning(warning.c_str()); + auto hr = callback->OnWarning(warning.c_str()); if (SUCCEEDED(hr) || hr == RPC_E_CALL_CANCELED || hr == HRESULT_FROM_WIN32(ERROR_CANCELLED)) { return true; diff --git a/src/windows/wslcsession/WSLCIdleState.h b/src/windows/wslcsession/WSLCIdleState.h new file mode 100644 index 0000000000..712022eecb --- /dev/null +++ b/src/windows/wslcsession/WSLCIdleState.h @@ -0,0 +1,226 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCIdleState.h + +Abstract: + + Shared idle-termination state for WSLC session VM lifecycle. + +--*/ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace wsl::windows::service::wslc { + +// Shared idle-termination state for a WSLC session. +// +// A single activity refcount is the only source of truth for "the VM is needed". Everything that +// requires the VM holds a reference for as long as it needs it: +// * in-flight operations (WSLCSession::VmLease), +// * running/created containers themselves (WSLCContainerImpl's ActivityRef), +// * client-held process wrappers (WSLCProcess keep-alive token), +// * multi-round-trip CLI operations (WSLCSession::BeginContainerOperation). +// +// When the count drops to zero a threadpool timer is armed for the idle grace period; if it +// elapses without new activity the session-supplied OnIdle callback tears the VM down. Any new +// activity before it fires cancels the timer. +// +// Held via shared_ptr so activity holders (container/process wrappers, operation tokens) can +// outlive the owning session and release activity without dereferencing it. The session clears the +// callback and drains the timer in Disarm() during teardown, after which a late release simply +// decrements the count and never re-enters the destroyed session. +class IdleState +{ +public: + IdleState() = default; + + IdleState(const IdleState&) = delete; + IdleState& operator=(const IdleState&) = delete; + + // Installs the idle-teardown callback and grace period and creates the timer. Called once by + // the owning session after construction. OnIdle runs on a threadpool thread. + void Initialize(std::chrono::milliseconds GracePeriod, std::function OnIdle) + { + auto lock = m_lock.lock_exclusive(); + m_gracePeriod = GracePeriod; + m_onIdle = std::move(OnIdle); + m_timer.reset(CreateThreadpoolTimer(&IdleState::TimerCallback, this, nullptr)); + THROW_LAST_ERROR_IF(!m_timer); + } + + // Permanently disables idle teardown: clears the callback so no further arm has any effect, and + // drains any pending/running timer callback. Must be called by the session (with its own lock + // released) during teardown, before the session object is destroyed, so no callback can + // reference it afterwards. + void Disarm() noexcept + { + PTP_TIMER timer = nullptr; + { + auto lock = m_lock.lock_exclusive(); + m_onIdle = nullptr; + timer = m_timer.get(); + if (timer != nullptr) + { + SetThreadpoolTimer(timer, nullptr, 0, 0); + } + } + + // Drain any in-flight callback outside the lock; it may take the session lock. + if (timer != nullptr) + { + WaitForThreadpoolTimerCallbacks(timer, TRUE); + } + } + + // Records the start of an activity; cancels any pending idle teardown on the 0->1 transition. + void AddActivity() noexcept + { + auto lock = m_lock.lock_exclusive(); + if (m_activityCount.fetch_add(1) == 0) + { + CancelLockHeld(); + } + } + + // Records the end of an activity; arms the idle timer on the 1->0 transition. + void ReleaseActivity() noexcept + { + auto lock = m_lock.lock_exclusive(); + const int previous = m_activityCount.fetch_sub(1); + FAIL_FAST_IF(previous <= 0); // Underflow is a fatal bug, not a recoverable condition. + if (previous == 1) + { + ArmLockHeld(); + } + } + + int ActivityCount() const noexcept + { + return m_activityCount.load(); + } + +private: + static void CALLBACK TimerCallback(PTP_CALLBACK_INSTANCE, PVOID Context, PTP_TIMER) noexcept + try + { + auto* self = static_cast(Context); + + std::function onIdle; + { + auto lock = self->m_lock.lock_exclusive(); + + // Activity resumed (count != 0) or teardown raced us (callback cleared): nothing to do. + if (self->m_activityCount.load() != 0 || !self->m_onIdle) + { + return; + } + + // Copy and invoke outside the lock: OnIdle takes the session lock, and holding this + // lock across that would invert the session-lock -> idle-lock ordering. + onIdle = self->m_onIdle; + } + + onIdle(); + } + CATCH_LOG() + + void ArmLockHeld() noexcept + { + if (!m_timer || !m_onIdle) + { + return; + } + + // Relative due time is expressed as a negative count of 100ns intervals. + const int64_t relative = -static_cast(m_gracePeriod.count()) * 10000; + FILETIME due{}; + due.dwLowDateTime = static_cast(relative & 0xFFFFFFFF); + due.dwHighDateTime = static_cast((relative >> 32) & 0xFFFFFFFF); + SetThreadpoolTimer(m_timer.get(), &due, 0, 0); + } + + void CancelLockHeld() noexcept + { + if (m_timer) + { + SetThreadpoolTimer(m_timer.get(), nullptr, 0, 0); + } + } + + std::atomic m_activityCount{0}; + wil::srwlock m_lock; + + _Guarded_by_(m_lock) std::function m_onIdle; + _Guarded_by_(m_lock) std::chrono::milliseconds m_gracePeriod { 0 }; + _Guarded_by_(m_lock) wil::unique_threadpool_timer m_timer; +}; + +// RAII activity hold on an IdleState: increments on construction and decrements on destruction or +// reset(). Movable, non-copyable. Used by running/created containers to keep the VM alive without +// a client reference. Holds the IdleState via shared_ptr so it is safe even if it outlives the +// owning session. +class ActivityRef +{ +public: + ActivityRef() = default; + + explicit ActivityRef(std::shared_ptr State) noexcept : m_state(std::move(State)) + { + if (m_state) + { + m_state->AddActivity(); + } + } + + ActivityRef(ActivityRef&& Other) noexcept : m_state(std::exchange(Other.m_state, nullptr)) + { + } + + ActivityRef& operator=(ActivityRef&& Other) noexcept + { + if (this != &Other) + { + reset(); + m_state = std::exchange(Other.m_state, nullptr); + } + + return *this; + } + + ActivityRef(const ActivityRef&) = delete; + ActivityRef& operator=(const ActivityRef&) = delete; + + ~ActivityRef() + { + reset(); + } + + void reset() noexcept + { + if (m_state) + { + m_state->ReleaseActivity(); + m_state.reset(); + } + } + + explicit operator bool() const noexcept + { + return m_state != nullptr; + } + +private: + std::shared_ptr m_state; +}; + +} // namespace wsl::windows::service::wslc diff --git a/src/windows/wslcsession/WSLCProcess.h b/src/windows/wslcsession/WSLCProcess.h index 1a79525dc1..9ce606a378 100644 --- a/src/windows/wslcsession/WSLCProcess.h +++ b/src/windows/wslcsession/WSLCProcess.h @@ -45,9 +45,19 @@ class DECLSPEC_UUID("AFBEA6D6-D8A4-4F81-8FED-F947EB74B33B") WSLCProcess HANDLE GetExitEvent(); int GetPid() const; + // Attaches an opaque keep-alive token whose lifetime is bound to this process object. A + // root-namespace process is not tracked as a container, so it relies on this token to hold an + // activity reference on the owning session for as long as the client keeps the process alive, + // preventing the idle worker from tearing the VM down (and killing the process) underneath it. + void SetKeepAliveToken(Microsoft::WRL::ComPtr&& Token) noexcept + { + m_keepAliveToken = std::move(Token); + } + private: WSLCProcessFlags m_flags; std::shared_ptr m_control; std::unique_ptr m_io; + Microsoft::WRL::ComPtr m_keepAliveToken; }; } // namespace wsl::windows::service::wslc \ No newline at end of file diff --git a/src/windows/wslcsession/WSLCProcessControl.cpp b/src/windows/wslcsession/WSLCProcessControl.cpp index 0c0e61eac9..8c8da1837d 100644 --- a/src/windows/wslcsession/WSLCProcessControl.cpp +++ b/src/windows/wslcsession/WSLCProcessControl.cpp @@ -101,7 +101,15 @@ void DockerContainerProcessControl::OnContainerReleased() noexcept // Signal the exit event to prevent callers from being blocked on it. if (!m_exitEvent.is_signaled()) { - m_exitedCode = 128 + WSLCSignalSIGKILL; + // If the container already produced a real exit code (recorded by SetExitCode but not yet + // signaled — e.g. an --rm container whose init-exit signal is deferred to the Destroy + // event), preserve it. Only synthesize SIGKILL when the container is released without ever + // having produced an exit code (an abrupt teardown of a still-running container). + if (!m_exitedCode.has_value()) + { + m_exitedCode = 128 + WSLCSignalSIGKILL; + } + m_exitEvent.SetEvent(); } } diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 6c82d61a48..f1b0e1d877 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -36,15 +36,49 @@ using wsl::windows::service::wslc::WSLCExecutionContext; using wsl::windows::service::wslc::WSLCSession; using wsl::windows::service::wslc::WSLCVirtualMachine; -constexpr auto c_containerdStorage = "/var/lib/docker"; +constexpr auto c_containerdStorage = wsl::windows::wslc::ContainerdStorageMountPoint; constexpr auto c_containerdSocket = "/run/containerd/containerd.sock"; constexpr auto c_dockerdReadyLogLine = "API listen on /var/run/docker.sock"; constexpr auto c_storageVhdFilename = wsl::windows::wslc::DefaultStorageVhdName; constexpr DWORD c_processTerminateTimeoutMs = 30 * 1000; constexpr DWORD c_processKillTimeoutMs = 10 * 1000; +// Default grace period to keep an otherwise-idle VM running before tearing it down (used when the +// session's IdleTimeoutSec setting is 0/unset). This avoids thrashing the VM (repeated +// teardown/recreate) when containers are created and destroyed, or operations issued, in quick +// succession. The clock restarts whenever the VM is observed to be non-idle, so a full grace period +// of continuous idleness is required before teardown. +constexpr auto c_vmIdleGracePeriod = std::chrono::seconds(30); + namespace { +// Validates the target path for a NEW session (one with no existing storage VHD): if the path +// already exists it must be an empty directory, so session storage is never mixed with unrelated +// user files. A non-existent path is fine (it will be created). Enforced eagerly at session +// creation and again when the storage VHD is lazily created. +void ValidateNewSessionStorageDirectory(const std::filesystem::path& StoragePath) +{ + // status's error_code distinguishes "doesn't exist yet" (OK, we'll create it) from other I/O errors. + std::error_code ec; + const auto status = std::filesystem::status(StoragePath, ec); + if (ec && ec.value() != ERROR_FILE_NOT_FOUND && ec.value() != ERROR_PATH_NOT_FOUND) + { + THROW_IF_WIN32_ERROR_MSG(ec.value(), "status failed for %ls", StoragePath.c_str()); + } + + if (!std::filesystem::exists(status)) + { + return; + } + + THROW_HR_WITH_USER_ERROR_IF( + E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeDirectory(StoragePath.c_str()), !std::filesystem::is_directory(status)); + + const bool empty = std::filesystem::is_empty(StoragePath, ec); + THROW_IF_WIN32_ERROR_MSG(ec.value(), "is_empty failed for %ls", StoragePath.c_str()); + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeEmpty(StoragePath.c_str()), !empty); +} + // Group policy: WSLContainerRegistryAllowlist restricts which container-image // registries can be pulled from or pushed to. The check is enforced here at the // service boundary so it covers ALL callers (wslc.exe CLI, the WslcSDK C API, and @@ -332,7 +366,7 @@ HRESULT WSLCSession::Initialize( try { RETURN_HR_IF(E_POINTER, Settings == nullptr || VmFactory == nullptr); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_INITIALIZED), m_virtualMachine.has_value()); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_INITIALIZED), m_vmFactoryGitCookie != 0); THROW_HR_IF_MSG( E_INVALIDARG, WI_IsAnyFlagSet(Settings->FeatureFlags, ~WSLCFeatureFlagsValid), "Invalid feature flags: 0x%x", Settings->FeatureFlags); @@ -343,9 +377,35 @@ try Settings->StorageFlags); // Set up a warning context for the duration of initialization so that non-fatal - // failures (e.g., container/volume/network recovery) are streamed to the CLI. + // failures are streamed to the CLI. WSLCExecutionContext warningContext(this, WarningCallback); + // The VM (and storage VHD) is created lazily on the first operation. Validate the storage + // configuration eagerly here so misconfiguration is reported at session creation rather than + // surfacing later on the first VM-starting operation. + if (Settings->StoragePath != nullptr) + { + const std::filesystem::path storagePath{Settings->StoragePath}; + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Settings->StoragePath), !storagePath.is_absolute()); + + const auto vhdPath = storagePath / c_storageVhdFilename; + std::error_code existsError; + const bool vhdExists = std::filesystem::exists(vhdPath, existsError); + THROW_IF_WIN32_ERROR_MSG(existsError.value(), "exists failed for %ls", vhdPath.c_str()); + + if (WI_IsFlagSet(Settings->StorageFlags, WSLCSessionStorageFlagsNoCreate)) + { + // The storage VHD must already exist (ConfigureStorage will not create it). + THROW_HR_WITH_USER_ERROR_IF( + HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), Localization::MessageWslcSessionStorageNotFound(Settings->StoragePath), !vhdExists); + } + else if (!vhdExists) + { + // New session: the target path (if it exists) must be an empty directory. + ValidateNewSessionStorageDirectory(storagePath); + } + } + // N.B. No locking is required because Initialize() is always called before the session is returned to the caller. m_id = Settings->SessionId; m_displayName = Settings->DisplayName ? Settings->DisplayName : L""; @@ -353,8 +413,16 @@ try m_featureFlags = Settings->FeatureFlags; m_pluginNotifier = PluginNotifier; - // Get user token for the current process + // Park the VM factory in the Global Interface Table. It is supplied here (on the call that + // creates the session) but used on demand from other threads/apartments; storing the raw + // proxy and calling it later would raise RPC_E_WRONG_THREAD. + m_git = wil::CoCreateInstance(CLSID_StdGlobalInterfaceTable, CLSCTX_INPROC_SERVER); + THROW_IF_FAILED(m_git->RegisterInterfaceInGlobal(VmFactory, __uuidof(IWSLCVirtualMachineFactory), &m_vmFactoryGitCookie)); + + // Persist a deep copy of the settings (and the creating user's SID) required to + // (re)create the VM on demand. const auto tokenInfo = wil::get_token_information(GetCurrentProcessToken()); + PersistSettings(*Settings, tokenInfo->User.Sid); WSL_LOG( "SessionInitialized", @@ -362,62 +430,139 @@ try TraceLoggingValue(m_displayName.c_str(), "DisplayName"), TraceLoggingValue(m_creatorProcessName.c_str(), "CreatorProcess")); - // Create the VM through the factory. The VM produces crash events; the session multiplexes - // them out to any registered ICrashDumpCallback subscribers via OnCrashDumpWritten. - wil::com_ptr vm; - THROW_IF_FAILED(VmFactory->CreateVirtualMachine(&vm)); + const auto idleGracePeriod = m_settings.IdleTimeoutSec > 0 ? std::chrono::seconds(m_settings.IdleTimeoutSec) : c_vmIdleGracePeriod; - m_virtualMachine.emplace( - vm.get(), - Settings, - m_sessionTerminatingEvent.get(), - std::bind(&WSLCSession::OnCrashDumpWritten, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); + WSLCSessionRuntime::RuntimeHooks hooks; + hooks.BringUp = [this]() { + // Configure storage. + ConfigureStorage(m_settings, m_userSid.empty() ? nullptr : reinterpret_cast(m_userSid.data())); - // Make sure that everything is destroyed correctly if an exception is thrown. - auto errorCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_FAILED(Terminate()); }); + // Mirror the host's trusted root CAs into the VM before dockerd starts. + InstallTrustedRootCertificates(); - m_virtualMachine->Initialize(); + // Launch containerd first, then dockerd with the external containerd socket. + StartContainerd(); - // Get an event from the service that is signaled when the VM exits. - THROW_IF_FAILED(vm->GetTerminationEvent(&m_vmExitedEvent)); + // Reset the readiness event before (re)starting dockerd so a stale signal from a prior + // VM instance is not observed. + m_runtime.ResetDockerdReady(); + StartDockerd(); - // Configure storage. - ConfigureStorage(*Settings, tokenInfo->User.Sid); + m_runtime.InitializeDockerRuntime(m_storageVhdPath.parent_path()); + }; - // Mirror the host's trusted root CAs into the VM before dockerd starts. - InstallTrustedRootCertificates(); + hooks.RecoverState = [this]() { + RecoverExistingNetworks(); + RecoverExistingContainers(); + }; - // Launch containerd first - StartContainerd(); + hooks.TearDownSessionState = [this](bool permanent) { + std::lock_guard containersLock(m_containersLock); + std::lock_guard networksLock(m_networksLock); - // Launch dockerd with external containerd socket - StartDockerd(); + // Network metadata is rebuilt from dockerd on every VM start, so it is always dropped. + m_networks.clear(); - // Wait for dockerd to be ready before starting the event tracker. - THROW_WIN32_IF_MSG( - ERROR_TIMEOUT, !m_dockerdReadyEvent.wait(Settings->BootTimeoutMs), "Timed out waiting for dockerd to start"); + // Container wrappers are kept alive across idle teardown (only cleared on permanent shutdown) + // so client COM references stay valid; RecoverState reattaches them to the restarted VM. + if (permanent) + { + m_containers.clear(); + } + else + { + // Session lives on but the VM is going down: reconcile each surviving wrapper to a stopped + // state (releasing its VM activity hold) before teardown. + for (auto& [id, container] : m_containers) + { + container->OnVmTornDown(); + } + } + }; - auto [_, __, channel] = m_virtualMachine->Fork(WSLC_FORK::Thread); + hooks.OnSpontaneousExit = [this]() { LOG_IF_FAILED(Terminate()); }; - m_dockerClient.emplace(std::move(channel), m_virtualMachine->TerminatingEvent(), m_virtualMachine->VmId(), 10 * 1000); + // Forward VM start/stop to plugins. Both are best-effort: errors are logged and ignored so a + // misbehaving plugin cannot abort VM startup or the operation that triggered it. + hooks.OnVmStarted = [this]() { + if (m_pluginNotifier) + { + LOG_IF_FAILED(m_pluginNotifier->OnVmStarted()); + } + }; - // Start the event tracker. - m_eventTracker.emplace(m_dockerClient.value(), *this, m_ioRelay); + hooks.OnVmStopping = [this]() { + if (m_pluginNotifier) + { + LOG_IF_FAILED(m_pluginNotifier->OnVmStopping()); + } + }; - m_volumes.emplace(m_dockerClient.value(), m_virtualMachine.value(), m_eventTracker.value(), m_storageVhdPath.parent_path()); + hooks.OnCrashDump = std::bind( + &WSLCSession::OnCrashDumpWritten, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5); - // Monitor for unexpected VM exit. - m_ioRelay.AddHandle(std::make_unique(m_vmExitedEvent.get(), std::bind(&WSLCSession::OnVmExited, this))); + WSLCSessionRuntime::SessionContext sessionContext; + sessionContext.Id = m_id; + sessionContext.DisplayName = m_displayName; + sessionContext.Terminating = &m_terminating; + sessionContext.SessionTerminatingEvent = m_sessionTerminatingEvent; + sessionContext.SessionTerminatedEvent = m_sessionTerminatedEvent; - // Recover any existing resources from storage. - RecoverExistingNetworks(); - RecoverExistingContainers(); + m_runtime.Initialize(m_vmFactoryGitCookie, m_git, &m_settings, idleGracePeriod, std::move(sessionContext), std::move(hooks)); - errorCleanup.release(); return S_OK; } CATCH_RETURN() +void WSLCSession::PersistSettings(const WSLCSessionInitSettings& Settings, PSID UserSid) +{ + m_settings = Settings; + + // Repoint the string fields at storage owned by the session so they outlive the caller's buffers. + m_settings.DisplayName = m_displayName.c_str(); + + if (Settings.CreatorProcessName != nullptr) + { + m_settingsCreatorProcessName = Settings.CreatorProcessName; + m_settings.CreatorProcessName = m_settingsCreatorProcessName->c_str(); + } + else + { + m_settings.CreatorProcessName = nullptr; + } + + if (Settings.StoragePath != nullptr) + { + m_settingsStoragePath = Settings.StoragePath; + m_settings.StoragePath = m_settingsStoragePath->c_str(); + } + else + { + m_settings.StoragePath = nullptr; + } + + if (Settings.RootVhdTypeOverride != nullptr) + { + m_settingsRootVhdTypeOverride = Settings.RootVhdTypeOverride; + m_settings.RootVhdTypeOverride = m_settingsRootVhdTypeOverride->c_str(); + } + else + { + m_settings.RootVhdTypeOverride = nullptr; + } + + THROW_HR_IF(E_UNEXPECTED, UserSid == nullptr); + + const auto length = GetLengthSid(UserSid); + const auto* bytes = reinterpret_cast(UserSid); + m_userSid.assign(bytes, bytes + length); +} + +WSLCSession::VmLease WSLCSession::AcquireVmLease() +{ + return m_runtime.AcquireVmLease(); +} + WSLCSession::~WSLCSession() { WSL_LOG("SessionTerminated", TraceLoggingValue(m_id, "SessionId"), TraceLoggingValue(m_displayName.c_str(), "DisplayName")); @@ -440,8 +585,8 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID if (Settings.StoragePath == nullptr) { // If no storage path is specified, use a tmpfs for convenience. - m_virtualMachine->Mount("", c_containerdStorage, "tmpfs", "", 0); - m_storageMounted = true; + m_runtime.Vm().Mount("", c_containerdStorage, "tmpfs", "", 0); + m_runtime.SetStorageMounted(true); return; } @@ -459,7 +604,7 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID { if (diskLun.has_value()) { - m_virtualMachine->DetachDisk(diskLun.value()); + m_runtime.Vm().DetachDisk(diskLun.value()); } LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_storageVhdPath.c_str())); @@ -467,7 +612,7 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID }); auto result = - wil::ResultFromException([&]() { diskDevice = m_virtualMachine->AttachDisk(m_storageVhdPath.c_str(), false).second; }); + wil::ResultFromException([&]() { diskDevice = m_runtime.Vm().AttachDisk(m_storageVhdPath.c_str(), false).second; }); if (FAILED(result)) { @@ -484,23 +629,7 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID WI_IsFlagSet(Settings.StorageFlags, WSLCSessionStorageFlagsNoCreate)); // Reject any non-empty existing path so we don't mix user files with session storage. - // status's error_code distinguishes "doesn't exist yet" (OK, we'll create it) from other I/O errors. - std::error_code ec; - const auto status = std::filesystem::status(storagePath, ec); - if (ec && ec.value() != ERROR_FILE_NOT_FOUND && ec.value() != ERROR_PATH_NOT_FOUND) - { - THROW_IF_WIN32_ERROR_MSG(ec.value(), "status failed for %ls", storagePath.c_str()); - } - - if (std::filesystem::exists(status)) - { - THROW_HR_WITH_USER_ERROR_IF( - E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeDirectory(storagePath.c_str()), !std::filesystem::is_directory(status)); - - const bool empty = std::filesystem::is_empty(storagePath, ec); - THROW_IF_WIN32_ERROR_MSG(ec.value(), "is_empty failed for %ls", storagePath.c_str()); - THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeEmpty(storagePath.c_str()), !empty); - } + ValidateNewSessionStorageDirectory(storagePath); // If the VHD wasn't found, create it. WSL_LOG("CreateStorageVhd", TraceLoggingValue(m_storageVhdPath.c_str(), "StorageVhdPath")); @@ -515,31 +644,32 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID vhdCreated = true; // Then attach the new disk. - std::tie(diskLun, diskDevice) = m_virtualMachine->AttachDisk(m_storageVhdPath.c_str(), false); + std::tie(diskLun, diskDevice) = m_runtime.Vm().AttachDisk(m_storageVhdPath.c_str(), false); // Then format it. - m_virtualMachine->Ext4Format(diskDevice); + m_runtime.Vm().Ext4Format(diskDevice); } // Mount the device to /root. - m_virtualMachine->Mount(diskDevice.c_str(), c_containerdStorage, "ext4", "", 0); - m_storageMounted = true; + m_runtime.Vm().Mount(diskDevice.c_str(), c_containerdStorage, "ext4", "", 0); + m_runtime.SetStorageMounted(true); // Configure swap on a separate ephemeral VHD. if (Settings.SwapSizeMb > 0) { try { - m_swapVhdPath = storagePath / "swap.vhdx"; - DeleteFileW(m_swapVhdPath.c_str()); // Remove stale swap from prior run - wsl::core::filesystem::CreateVhd(m_swapVhdPath.c_str(), static_cast(Settings.SwapSizeMb) * _1MB, UserSid, false, false); + std::filesystem::path swapVhdPath = storagePath / "swap.vhdx"; + m_runtime.SetSwapVhdPath(swapVhdPath); + DeleteFileW(swapVhdPath.c_str()); // Remove stale swap from prior run + wsl::core::filesystem::CreateVhd(swapVhdPath.c_str(), static_cast(Settings.SwapSizeMb) * _1MB, UserSid, false, false); - auto [_, swapDevice] = m_virtualMachine->AttachDisk(m_swapVhdPath.c_str(), false); + auto [_, swapDevice] = m_runtime.Vm().AttachDisk(swapVhdPath.c_str(), false); // Fire-and-forget: mkswap + swapon runs asynchronously since swap is best-effort. auto cmd = std::format("/usr/sbin/mkswap {0} && /usr/sbin/swapon {0}", swapDevice); ServiceProcessLauncher launcher("/bin/sh", {"/bin/sh", "-c", cmd}); - launcher.Launch(*m_virtualMachine); + launcher.Launch(m_runtime.Vm()); } catch (...) { @@ -573,7 +703,7 @@ CATCH_RETURN(); void WSLCSession::OnDockerdExited() { - if (!m_sessionTerminatingEvent.is_signaled()) + if (!m_sessionTerminatingEvent.is_signaled() && m_runtime.ExitDisposition() != WSLCSessionRuntime::VmExitDisposition::StopRequested) { WSL_LOG("UnexpectedDockerdExit", TraceLoggingValue(m_displayName.c_str(), "Name")); } @@ -581,24 +711,12 @@ void WSLCSession::OnDockerdExited() void WSLCSession::OnContainerdExited() { - if (!m_sessionTerminatingEvent.is_signaled()) + if (!m_sessionTerminatingEvent.is_signaled() && m_runtime.ExitDisposition() != WSLCSessionRuntime::VmExitDisposition::StopRequested) { WSL_LOG("UnexpectedContainerdExit", TraceLoggingValue(m_displayName.c_str(), "Name")); } } -void WSLCSession::OnVmExited() -{ - WSL_LOG( - "VmExited", - TraceLoggingLevel(WINEVENT_LEVEL_WARNING), - TraceLoggingValue(m_id, "SessionId"), - TraceLoggingValue(m_displayName.c_str(), "Name"), - TraceLoggingValue(!m_sessionTerminatingEvent.is_signaled(), "Unexpected")); - - LOG_IF_FAILED(Terminate()); -} - void WSLCSession::OnProcessLog(const gsl::span& Buffer, PCSTR Source) try { @@ -614,11 +732,11 @@ try TraceLoggingValue(entry.c_str(), "Content"), TraceLoggingValue(m_displayName.c_str(), "Name")); - if (!m_dockerdReadyEvent.is_signaled()) + if (!m_runtime.IsDockerdReady()) { if (entry.find(c_dockerdReadyLogLine) != std::string::npos) { - m_dockerdReadyEvent.SetEvent(); + m_runtime.SignalDockerdReady(); } } } @@ -629,15 +747,15 @@ ServiceRunningProcess WSLCSession::StartProcess( { ServiceProcessLauncher launcher{Executable, Args, {{"PATH=/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin"}}}; - auto process = launcher.Launch(*m_virtualMachine); + auto process = launcher.Launch(m_runtime.Vm()); - m_ioRelay.AddHandle(std::make_unique( + m_runtime.Relay()->AddHandle(std::make_unique( process.GetStdHandle(1), [this, LogSource](const auto& data) { OnProcessLog(data, LogSource); }, false)); - m_ioRelay.AddHandle(std::make_unique( + m_runtime.Relay()->AddHandle(std::make_unique( process.GetStdHandle(2), [this, LogSource](const auto& data) { OnProcessLog(data, LogSource); }, false)); - m_ioRelay.AddHandle(std::make_unique(process.GetExitEvent(), std::move(ExitCallback))); + m_runtime.Relay()->AddHandle(std::make_unique(process.GetExitEvent(), std::move(ExitCallback))); return process; } @@ -655,7 +773,7 @@ void WSLCSession::StartContainerd() args.emplace_back("debug"); } - m_containerdProcess = StartProcess("/usr/bin/containerd", args, "containerd", std::bind(&WSLCSession::OnContainerdExited, this)); + m_runtime.SetContainerdProcess(StartProcess("/usr/bin/containerd", args, "containerd", std::bind(&WSLCSession::OnContainerdExited, this))); WSL_LOG("ContainerdStarted"); } @@ -668,7 +786,7 @@ void WSLCSession::StartDockerd() args.emplace_back("--debug"); } - m_dockerdProcess = StartProcess("/usr/bin/dockerd", args, "dockerd", std::bind(&WSLCSession::OnDockerdExited, this)); + m_runtime.SetDockerdProcess(StartProcess("/usr/bin/dockerd", args, "dockerd", std::bind(&WSLCSession::OnDockerdExited, this))); WSL_LOG("DockerdStarted"); } @@ -688,7 +806,7 @@ try const auto script = std::format("cat > '{}'", c_certPath); ServiceProcessLauncher launcher("/bin/sh", {"/bin/sh", "--norc", "-c", script}, {}, WSLCProcessFlagsStdin); - auto process = launcher.Launch(*m_virtualMachine); + auto process = launcher.Launch(m_runtime.Vm()); std::unique_ptr writeStdin( new WriteHandle(process.GetStdHandle(WSLCFDStdin), std::vector{pem.begin(), pem.end()})); @@ -845,8 +963,8 @@ try auto [repo, tagOrDigest] = wslutil::ParseImage(Image); EnforceRegistryAllowlist(repo); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + auto runtime = m_runtime.Acquire(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); if (!tagOrDigest.has_value()) { @@ -860,7 +978,7 @@ try registryAuth = std::string(RegistryAuthenticationInformation); } - auto requestContext = m_dockerClient->PullImage(repo, tagOrDigest, registryAuth); + auto requestContext = runtime.Docker().PullImage(repo, tagOrDigest, registryAuth); StreamImageOperation(*requestContext, Image, "Pull", ProgressCallback); OnImageCreated(Image); @@ -903,16 +1021,15 @@ try comCall = RegisterUserCOMCallback(); } - auto lock = m_lock.lock_shared(); + auto runtime = m_runtime.Acquire(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); GUID volumeId{}; THROW_IF_FAILED(CoCreateGuid(&volumeId)); auto mountPath = std::format("/mnt/{}", wsl::shared::string::GuidToString(volumeId)); - THROW_IF_FAILED(m_virtualMachine->MountWindowsFolder(Options->ContextPath, mountPath.c_str(), TRUE)); - auto unmountFolder = - wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { m_virtualMachine->UnmountWindowsFolder(mountPath.c_str()); }); + THROW_IF_FAILED(runtime.Vm().MountWindowsFolder(Options->ContextPath, mountPath.c_str(), TRUE)); + auto unmountFolder = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { runtime.Vm().UnmountWindowsFolder(mountPath.c_str()); }); std::vector buildArgs{"/usr/bin/docker", "build", "--progress=rawjson"}; if (WI_IsFlagSet(Options->Flags, WSLCBuildImageFlagsNoCache)) @@ -957,7 +1074,7 @@ try WSL_LOG("BuildImageStart", TraceLoggingValue(wsl::shared::string::Join(buildArgs, ' ').c_str(), "Command")); ServiceProcessLauncher buildLauncher(buildArgs[0], buildArgs, {}, WSLCProcessFlagsStdin); - auto buildProcess = buildLauncher.Launch(*m_virtualMachine); + auto buildProcess = buildLauncher.Launch(runtime.Vm()); auto io = CreateIOContext(); @@ -1232,11 +1349,11 @@ try { WSLCExecutionContext context(this, WarningCallback); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); - auto requestContext = m_dockerClient->LoadImage(ContentSize); + auto requestContext = m_runtime.Docker().LoadImage(ContentSize); std::ignore = ImportImageImpl(*requestContext, ImageHandle, LoadCallback); @@ -1265,11 +1382,11 @@ try tag = tagOrDigest.value(); } - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); - auto requestContext = m_dockerClient->ImportImage(repo, tag, ContentSize); + auto requestContext = m_runtime.Docker().ImportImage(repo, tag, ContentSize); auto imageId = ImportImageImpl(*requestContext, ImageHandle); THROW_HR_IF_MSG(E_UNEXPECTED, !imageId.has_value(), "Docker import succeeded but did not return an image ID"); @@ -1299,7 +1416,7 @@ std::optional WSLCSession::ImportImageImpl(DockerHTTPClient::HTTPRe comCall = RegisterUserCOMCallback(); } - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); auto io = CreateIOContext(); @@ -1436,11 +1553,11 @@ try RETURN_HR_IF_NULL(E_POINTER, ImageNameOrID); RETURN_HR_IF(E_INVALIDARG, strlen(ImageNameOrID) > WSLC_MAX_IMAGE_NAME_LENGTH); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); - auto retVal = m_dockerClient->SaveImage(ImageNameOrID); + auto retVal = m_runtime.Docker().SaveImage(ImageNameOrID); SaveImageImpl(retVal, OutHandle, CancelEvent); return S_OK; } @@ -1469,11 +1586,11 @@ try names.emplace_back(ImageNames->Values[i]); } - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); - auto retVal = m_dockerClient->SaveImages(names); + auto retVal = m_runtime.Docker().SaveImages(names); SaveImageImpl(retVal, OutHandle, CancelEvent); return S_OK; } @@ -1483,7 +1600,7 @@ void WSLCSession::SaveImageImpl(std::pair& SocketC { auto userHandle = OpenUserHandle(OutputHandle); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); auto io = CreateIOContext(CancelEvent); @@ -1544,14 +1661,14 @@ try filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Options->Filters, Options->FiltersCount); } - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); std::vector images; try { - images = m_dockerClient->ListImages(all, digests, filters); + images = m_runtime.Docker().ListImages(all, digests, filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list images"); @@ -1653,14 +1770,14 @@ try *DeletedImages = nullptr; *Count = 0; - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); std::vector deletedImages; try { - deletedImages = m_dockerClient->DeleteImage( + deletedImages = m_runtime.Docker().DeleteImage( Options->Image, WI_IsFlagSet(Options->Flags, WSLCDeleteImageFlagsForce), WI_IsFlagSet(Options->Flags, WSLCDeleteImageFlagsNoPrune)); } catch (const DockerHTTPException& e) @@ -1727,13 +1844,13 @@ try RETURN_HR_IF_NULL(E_POINTER, Options->Tag); RETURN_HR_IF(E_INVALIDARG, strlen(Options->Repo) + strlen(Options->Tag) + 1 > WSLC_MAX_IMAGE_NAME_LENGTH); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); try { - m_dockerClient->TagImage(Options->Image, Options->Repo, Options->Tag); + m_runtime.Docker().TagImage(Options->Image, Options->Repo, Options->Tag); } catch (const DockerHTTPException& e) { @@ -1764,10 +1881,10 @@ try auto [repo, tagOrDigest] = wslutil::ParseImage(Image); EnforceRegistryAllowlist(repo); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); - auto requestContext = m_dockerClient->PushImage(repo, tagOrDigest, RegistryAuthenticationInformation); + auto requestContext = m_runtime.Docker().PushImage(repo, tagOrDigest, RegistryAuthenticationInformation); StreamImageOperation(*requestContext, Image, "Push", ProgressCallback); return S_OK; @@ -1785,8 +1902,8 @@ try *Output = nullptr; - auto lock = m_lock.lock_shared(); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + auto lock = AcquireVmLease(); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); *Output = wil::make_unique_ansistring(InspectImageLockHeld(ImageNameOrId).c_str()).release(); @@ -1799,7 +1916,7 @@ std::string WSLCSession::InspectImageLockHeld(const std::string& NameOrId) docker_schema::InspectImage dockerInspect; try { - dockerInspect = m_dockerClient->InspectImage(NameOrId); + dockerInspect = m_runtime.Docker().InspectImage(NameOrId); } catch (const DockerHTTPException& e) { @@ -1833,14 +1950,14 @@ try *IdentityToken = nullptr; - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); wil::unique_cotaskmem_ansistring token; try { - auto response = m_dockerClient->Authenticate(ServerAddress, Username, Password); + auto response = m_runtime.Docker().Authenticate(ServerAddress, Username, Password); token = wil::make_unique_ansistring(response.c_str()); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to authenticate with registry: %hs", ServerAddress); @@ -1865,13 +1982,13 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); - auto lock = m_lock.lock_shared(); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + auto lock = AcquireVmLease(); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); docker_schema::PruneImageResult pruneResult; try { - pruneResult = m_dockerClient->PruneImages(filters); + pruneResult = m_runtime.Docker().PruneImages(filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune images"); @@ -1926,7 +2043,7 @@ try "Invalid process flags: 0x%x", containerOptions->InitProcessOptions.Flags); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); auto result = wil::ResultFromException([&]() { CreateContainerImpl(containerOptions, Container); }); @@ -1947,10 +2064,10 @@ CATCH_RETURN(); void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptions, IWSLCContainer** Container) { - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_eventTracker); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasEvents()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes()); // Validate that name & images are valid. if (containerOptions->Name != nullptr && containerOptions->Name[0] != '\0') @@ -1997,14 +2114,10 @@ void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptio *containerOptions, containerName, *this, - m_virtualMachine.value(), + m_runtime, m_pluginNotifier.get(), m_networks, - m_volumes.value(), - std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), - m_eventTracker.value(), - m_dockerClient.value(), - m_ioRelay); + std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1)); // 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)); @@ -2037,7 +2150,7 @@ try ValidateName(Id, WSLC_MAX_CONTAINER_NAME_LENGTH); // Look for an exact ID match first. - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); std::lock_guard containersLock{m_containersLock}; // Purge containers that were auto-deleted via OnEvent (--rm). @@ -2052,7 +2165,7 @@ try try { - inspectResult = m_dockerClient->InspectContainer(Id); + inspectResult = m_runtime.Docker().InspectContainer(Id); } catch (DockerHTTPException& e) { @@ -2076,6 +2189,77 @@ try } CATCH_RETURN(); +namespace { + + // Activity token holds an activity reference to prevent idle VM teardown while client holds it. + // Implements IFastRundown so crashed clients reclaim stub promptly instead of slow default rundown. + class ContainerOperation + : public Microsoft::WRL::RuntimeClass, IUnknown, IFastRundown> + { + public: + // Adopts an activity reference from CreateActivityToken; callback releases it. + void Initialize(std::function&& onRelease) noexcept + { + m_onRelease = std::move(onRelease); + } + + ~ContainerOperation() override + { + if (m_onRelease) + { + m_onRelease(); + } + } + + private: + std::function m_onRelease; + }; + +} // namespace + +Microsoft::WRL::ComPtr WSLCSession::CreateActivityToken() +{ + // Record the in-flight activity up front so the VM cannot idle-terminate before the caller + // takes ownership of the returned token. + m_runtime.Idle().AddActivity(); + auto countCleanup = wil::scope_exit([this]() { m_runtime.Idle().ReleaseActivity(); }); + + auto operation = Microsoft::WRL::Make(); + THROW_IF_NULL_ALLOC(operation.Get()); + + // Capture shared idle state so token can outlive session and release activity without keeping session alive. + std::shared_ptr idleState = m_runtime.IdleStateShared(); + operation->Initialize([idleState = std::move(idleState)]() { idleState->ReleaseActivity(); }); + + // The token now owns the activity-count reference and will release it on destruction. + countCleanup.release(); + + Microsoft::WRL::ComPtr token; + THROW_IF_FAILED(operation.As(&token)); + return token; +} + +HRESULT WSLCSession::BeginContainerOperation(IUnknown** Operation) +try +{ + WSLCExecutionContext context(this); + + RETURN_HR_IF_NULL(E_POINTER, Operation); + *Operation = nullptr; + + // Do not start a new operation (which would hold the VM alive) once the session is terminating + // or has terminated. Mirrors the gate in EnsureVmRunning(). + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating.load() || m_sessionTerminatedEvent.is_signaled()); + + // Record the in-flight operation up front so the VM cannot idle-terminate before the client + // resolves the container and issues the operation (and streams any output). + auto token = CreateActivityToken(); + + RETURN_IF_FAILED(token.CopyTo(Operation)); + return S_OK; +} +CATCH_RETURN(); + HRESULT WSLCSession::ListContainers( const WSLCListContainersOptions* Options, WSLCContainerEntry** Containers, ULONG* Count, WSLCContainerPortMapping** Ports, ULONG* PortsCount) try @@ -2110,13 +2294,13 @@ try filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Options->Filters, Options->FiltersCount); } - auto lock = m_lock.lock_shared(); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + auto lock = AcquireVmLease(); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); std::vector dockerContainers; try { - dockerContainers = m_dockerClient->ListContainers(all, limit, filters); + dockerContainers = m_runtime.Docker().ListContainers(all, limit, filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list containers"); @@ -2189,8 +2373,8 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); - auto lock = m_lock.lock_shared(); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + auto lock = AcquireVmLease(); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); std::lock_guard containersLock{m_containersLock}; @@ -2198,7 +2382,7 @@ try try { - pruneResult = m_dockerClient->PruneContainers(filters); + pruneResult = m_runtime.Docker().PruneContainers(filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune containers"); @@ -2260,10 +2444,18 @@ try *Errno = -1; // Make sure not to return 0 if something fails. } - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + auto runtime = m_runtime.Acquire(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); + + auto process = runtime.Vm().CreateLinuxProcess(Executable, *Options, TtyRows, TtyColumns, Errno); + + // The VmLease above is released when this call returns, but the process keeps running in the + // VM and the client holds the returned proxy. A root-namespace process is not tracked as a + // container, so attach an activity token bound to the process's lifetime; this keeps the VM + // alive for as long as the client holds the process, preventing the idle worker from tearing + // the VM down and killing the process out from under the client. + process->SetKeepAliveToken(CreateActivityToken()); - auto process = m_virtualMachine->CreateLinuxProcess(Executable, *Options, TtyRows, TtyColumns, Errno); THROW_IF_FAILED(process.CopyTo(Process)); return S_OK; @@ -2274,7 +2466,7 @@ void WSLCSession::Ext4Format(const std::string& Device) { constexpr auto mkfsPath = "/usr/sbin/mkfs.ext4"; ServiceProcessLauncher launcher(mkfsPath, {mkfsPath, Device}); - auto result = launcher.Launch(*m_virtualMachine).WaitAndCaptureOutput(); + auto result = launcher.Launch(m_runtime.Vm()).WaitAndCaptureOutput(); THROW_HR_IF_MSG(E_FAIL, result.Code != 0, "%hs", launcher.FormatResult(result).c_str()); } @@ -2286,17 +2478,17 @@ try THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Path), !std::filesystem::path(Path).is_absolute()); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); // Attach the disk to the VM (AttachDisk() performs the access check for the VHD file). - auto [lun, device] = m_virtualMachine->AttachDisk(Path, false); + auto [lun, device] = m_runtime.Vm().AttachDisk(Path, false); // N.B. DetachDisk calls sync() before detaching. - auto detachDisk = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, lun]() { m_virtualMachine->DetachDisk(lun); }); + auto detachDisk = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, lun]() { m_runtime.Vm().DetachDisk(lun); }); // Format it to ext4. - m_virtualMachine->Ext4Format(device); + m_runtime.Vm().Ext4Format(device); return S_OK; } @@ -2314,15 +2506,15 @@ try 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_volumes); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes()); if (Options->Name != nullptr && Options->Name[0] != '\0') { ValidateName(Options->Name, WSLC_MAX_VOLUME_NAME_LENGTH); } - *VolumeInfo = m_volumes->CreateVolume(Options->Name, Options->Driver, std::move(driverOpts), std::move(labels)); + *VolumeInfo = m_runtime.Volumes().CreateVolume(Options->Name, Options->Driver, std::move(driverOpts), std::move(labels)); return S_OK; } CATCH_RETURN(); @@ -2334,10 +2526,10 @@ try RETURN_HR_IF_NULL(E_POINTER, Name); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes()); - m_volumes->DeleteVolume(Name); + m_runtime.Volumes().DeleteVolume(Name); return S_OK; } CATCH_RETURN(); @@ -2355,10 +2547,10 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes()); - auto volumeList = m_volumes->ListVolumes(std::move(filters)); + auto volumeList = m_runtime.Volumes().ListVolumes(std::move(filters)); if (volumeList.empty()) { @@ -2387,10 +2579,10 @@ try 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_volumes); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes()); - std::string json = m_volumes->InspectVolume(name); + std::string json = m_runtime.Volumes().InspectVolume(name); *Output = wil::make_unique_ansistring(json.c_str()).release(); return S_OK; @@ -2412,13 +2604,13 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes()); WSLCVolumes::PruneVolumesResult pruneResult; try { - pruneResult = m_volumes->PruneVolumes(filters); + pruneResult = m_runtime.Volumes().PruneVolumes(filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune volumes"); @@ -2444,32 +2636,6 @@ try } CATCH_RETURN(); -int WSLCSession::StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs) -{ - auto signalResult = Process.Get().Signal(WSLCSignalSIGTERM); - if (FAILED(signalResult)) - { - LOG_HR_MSG(signalResult, "Failed to terminate process %i", Process.Get().GetPid()); - return -1; - } - - try - { - return Process.Wait(TerminateTimeoutMs); - } - catch (...) - { - LOG_CAUGHT_EXCEPTION(); - try - { - LOG_IF_FAILED(Process.Get().Signal(WSLCSignalSIGKILL)); - return Process.Wait(KillTimeoutMs); - } - CATCH_LOG(); - } - - return -1; -} // Network management. HRESULT WSLCSession::CreateNetwork(const WSLCNetworkOptions* Options, IWarningCallback* WarningCallback) @@ -2491,9 +2657,9 @@ try auto driverOpts = wslutil::ParseKeyValuePairs(Options->DriverOpts, Options->DriverOptsCount); auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCNetworkManagedLabel); - 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); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); std::lock_guard networksLock(m_networksLock); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), m_networks.contains(name)); @@ -2532,7 +2698,7 @@ try docker_schema::CreateNetworkResponse createResult; try { - createResult = m_dockerClient->CreateNetwork(request); + createResult = m_runtime.Docker().CreateNetwork(request); } catch (const DockerHTTPException& e) { @@ -2546,14 +2712,15 @@ try EMIT_USER_WARNING(wsl::shared::string::MultiByteToWide(createResult.Warning)); } - auto removeNetworkCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, &name]() { m_dockerClient->RemoveNetwork(name); }); + auto removeNetworkCleanup = + wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, &name]() { m_runtime.Docker().RemoveNetwork(name); }); // Inspect the newly created network to cache full properties (IPAM, Scope, etc.) // since CreateNetworkResponse only returns {Id, Warning}. docker_schema::Network full; try { - full = m_dockerClient->InspectNetwork(name); + full = m_runtime.Docker().InspectNetwork(name); } catch (const DockerHTTPException& e) { @@ -2600,9 +2767,9 @@ try std::string name = Name; ValidateName(name.c_str(), WSLC_MAX_NETWORK_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); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); std::lock_guard networksLock(m_networksLock); @@ -2611,7 +2778,7 @@ try try { - m_dockerClient->RemoveNetwork(name); + m_runtime.Docker().RemoveNetwork(name); } catch (const DockerHTTPException& e) { @@ -2640,7 +2807,7 @@ try *Networks = nullptr; *Count = 0; - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); std::lock_guard networksLock(m_networksLock); if (m_networks.empty()) @@ -2679,7 +2846,7 @@ try std::string name = Name; ValidateName(name.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); std::lock_guard networksLock(m_networksLock); auto it = m_networks.find(name); @@ -2734,16 +2901,16 @@ try // Scope the prune to WSLC-managed networks. filters["label"].push_back(WSLCNetworkManagedLabel); - 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); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); std::lock_guard networksLock(m_networksLock); docker_schema::PruneNetworkResult pruneResult; try { - pruneResult = m_dockerClient->PruneNetworks(filters); + pruneResult = m_runtime.Docker().PruneNetworks(filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune networks"); @@ -2814,10 +2981,10 @@ bool WSLCSession::WaitForEventOrSessionTerminating(HANDLE Event, std::chrono::mi HRESULT WSLCSession::Terminate() try { - // Ensure only one Terminate() runs. This must be checked before taking m_lock - // because OnVmExited() is called from the IORelay thread — if an external Terminate() - // holds m_lock and calls m_ioRelay.Stop(), the relay thread must not re-enter - // Terminate() and deadlock on m_lock. + // Ensure only one Terminate() runs. This must be checked before taking the runtime's exclusive + // lock because OnVmExited() is called from the IORelay thread — if an external Terminate() + // holds that lock and calls m_runtime.Relay()->Stop(), the relay thread must not re-enter + // Terminate() and deadlock on it. if (m_terminating.exchange(true)) { return S_OK; @@ -2839,8 +3006,8 @@ try { std::lock_guard lock(m_userHandlesLock); - // m_sessionTerminatingEvent is always valid, so it can be signalled without holding m_lock. - // This allows a session to be unblocked if a stuck operation is holding m_lock. + // m_sessionTerminatingEvent is always valid, so it can be signalled without holding the runtime lock. + // This allows a session to be unblocked if a stuck operation is holding the runtime lock. // N.B. This must happen under m_userHandlesLock to synchronize with potentially running operations. if (!m_sessionTerminatingEvent.is_signaled()) { @@ -2860,101 +3027,20 @@ try CancelUserCOMCallbacks(); } - sessionLock = m_lock.try_lock_exclusive(); + sessionLock = m_runtime.TryLockExclusive(); retrying = true; } - // Acquire an exclusive lock to ensure that no operation is running. - WI_VERIFY(sessionLock); - - std::lock_guard containersLock(m_containersLock); - std::lock_guard networksLock(m_networksLock); - - m_containers.clear(); - m_volumes.reset(); - m_networks.clear(); - - // Stop the IO relay. - // This stops: - // - container state monitoring. - // - container init process relays - // - execs relays - // - container logs relays - m_ioRelay.Stop(); + m_runtime.Shutdown(sessionLock, m_terminationReason, m_terminationDetails); + // Idle teardown is disabled and no operation can run past termination, so the parked VM + // factory can no longer be re-fetched; revoke it from the GIT. + if (m_vmFactoryGitCookie != 0) { - std::lock_guard allocatedPortsLock(m_allocatedPortsLock); - m_allocatedPorts.clear(); + LOG_IF_FAILED(m_git->RevokeInterfaceFromGlobal(m_vmFactoryGitCookie)); + m_vmFactoryGitCookie = 0; } - m_eventTracker.reset(); - m_dockerClient.reset(); - - // Check if the VM has already exited (e.g., killed externally). - // If so, skip operations that require a live VM to avoid unnecessary waits. - // N.B. m_vmExitedEvent may be uninitialized if Terminate() is called from the - // Initialize() error path before GetTerminationEvent() succeeds. - if (m_vmExitedEvent && m_vmExitedEvent.is_signaled()) - { - WSL_LOG("SkippingGracefulShutdown_VmDead", TraceLoggingValue(m_id, "SessionId")); - - // The VM exited on its own, so it recorded the cause. - if (m_virtualMachine) - { - wil::unique_cotaskmem_string details; - LOG_IF_FAILED(m_virtualMachine->GetTerminationReason(&m_terminationReason, &details)); - m_terminationDetails = details ? details.get() : L""; - } - } - else - { - // The VM is still alive, so this is a graceful shutdown initiated by us. - m_terminationReason = WSLCVirtualMachineTerminationReasonShutdown; - - if (m_virtualMachine) - { - m_virtualMachine->OnSessionTerminated(); - - // Stop dockerd first, then containerd (dockerd is a client of containerd). - // N.B. dockerd waits a couple seconds if there are any outstanding HTTP request sockets opened. - if (m_dockerdProcess.has_value()) - { - auto dockerdExitCode = StopProcess(m_dockerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs); - WSL_LOG("DockerdExit", TraceLoggingValue(dockerdExitCode, "code")); - } - - if (m_containerdProcess.has_value()) - { - auto containerdExitCode = StopProcess(m_containerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs); - WSL_LOG("ContainerdExit", TraceLoggingValue(containerdExitCode, "code")); - } - - // N.B. dockerd has exited by this point, so unmounting the VHD is safe since no container can be running. - if (m_storageMounted) - { - try - { - m_virtualMachine->Unmount(c_containerdStorage); - m_storageMounted = false; - } - CATCH_LOG(); - } - } - } - - m_dockerdProcess.reset(); - m_containerdProcess.reset(); - m_virtualMachine.reset(); - - // Delete the ephemeral swap VHD now that the VM is gone. - if (!m_swapVhdPath.empty()) - { - LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_swapVhdPath.c_str())); - m_swapVhdPath.clear(); - } - - m_sessionTerminatedEvent.SetEvent(); - return S_OK; } CATCH_RETURN(); @@ -3022,10 +3108,10 @@ try RETURN_HR_IF_NULL(E_POINTER, WindowsPath); RETURN_HR_IF_NULL(E_POINTER, LinuxPath); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); - return m_virtualMachine->MountWindowsFolder(WindowsPath, LinuxPath, ReadOnly); + return m_runtime.Vm().MountWindowsFolder(WindowsPath, LinuxPath, ReadOnly); } CATCH_RETURN(); @@ -3036,10 +3122,10 @@ try RETURN_HR_IF_NULL(E_POINTER, LinuxPath); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); - return m_virtualMachine->UnmountWindowsFolder(LinuxPath); + return m_runtime.Vm().UnmountWindowsFolder(LinuxPath); } CATCH_RETURN(); @@ -3048,36 +3134,37 @@ try { WSLCExecutionContext context(this); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); - std::lock_guard allocatedPortsLock(m_allocatedPortsLock); + std::lock_guard allocatedPortsLock(m_runtime.AllocatedPortsLock()); // Look for an existing allocation first. - auto it = m_allocatedPorts.find(LinuxPort); + auto& allocatedPorts = m_runtime.AllocatedPorts(); + auto it = allocatedPorts.find(LinuxPort); bool inserted = false; auto cleanup = wil::scope_exit([&]() { if (inserted) { - m_allocatedPorts.erase(it); + allocatedPorts.erase(it); } }); - if (it == m_allocatedPorts.end()) + if (it == allocatedPorts.end()) { // No existing port allocation, create a new one. - auto allocated = std::make_pair(m_virtualMachine->TryAllocatePort(LinuxPort, Family, IPPROTO_TCP), static_cast(0)); + auto allocated = std::make_pair(m_runtime.Vm().TryAllocatePort(LinuxPort, Family, IPPROTO_TCP), static_cast(0)); THROW_HR_IF(HRESULT_FROM_WIN32(WSAEADDRINUSE), allocated.first == nullptr); - it = m_allocatedPorts.emplace(LinuxPort, allocated).first; + it = allocatedPorts.emplace(LinuxPort, allocated).first; inserted = true; } auto mapping = VMPortMapping::LocalhostTcpMapping(Family, WindowsPort); mapping.AssignVmPort(it->second.first); - m_virtualMachine->MapPort(mapping); + m_runtime.Vm().MapPort(mapping); // Increase usage count. it->second.second++; @@ -3094,34 +3181,48 @@ try { WSLCExecutionContext context(this); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); - std::lock_guard allocatedPortsLock(m_allocatedPortsLock); + std::lock_guard allocatedPortsLock(m_runtime.AllocatedPortsLock()); - auto it = m_allocatedPorts.find(LinuxPort); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == m_allocatedPorts.end()); + auto& allocatedPorts = m_runtime.AllocatedPorts(); + auto it = allocatedPorts.find(LinuxPort); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == allocatedPorts.end()); auto mapping = VMPortMapping::LocalhostTcpMapping(Family, WindowsPort); mapping.AssignVmPort(it->second.first); - mapping.Attach(m_virtualMachine.value()); + mapping.Attach(m_runtime.Vm()); auto cleanup = wil::scope_exit([&]() { mapping.Release(); }); - m_virtualMachine->UnmapPort(mapping); + m_runtime.Vm().UnmapPort(mapping); it->second.second--; // If usage count drops to 0, release the port allocation. if (it->second.second == 0) { - m_allocatedPorts.erase(it); + allocatedPorts.erase(it); } return S_OK; } CATCH_RETURN(); +HRESULT WSLCSession::TriggerIdleTermination(BOOL* WasAlreadyIdle) +try +{ + WSLCExecutionContext context(this); + + THROW_HR_IF_NULL(E_POINTER, WasAlreadyIdle); + + *WasAlreadyIdle = m_runtime.TriggerIdleTerminationForTest() ? TRUE : FALSE; + + return S_OK; +} +CATCH_RETURN(); + HRESULT WSLCSession::InterfaceSupportsErrorInfo(REFIID riid) { return riid == __uuidof(IWSLCSession) || riid == __uuidof(IWSLCCompatSession) ? S_OK : S_FALSE; @@ -3408,7 +3509,11 @@ void WSLCSession::CancelUserCOMCallbacks() void WSLCSession::OnContainerDeleted(const WSLCContainerImpl* Container) { - auto lock = m_lock.lock_shared(); + // N.B. Invoked only from WSLCContainer::Delete, which already holds a VmLease (the shared + // session lock). The lease prevents a concurrent idle teardown from clearing m_containers, + // so this only needs m_containersLock. It must NOT re-acquire the shared session lock here: + // doing so while the idle worker is queued for the exclusive lock would deadlock (recursive + // shared acquire behind a pending writer). std::lock_guard containersLock(m_containersLock); // N.B. once a container transitions to a 'Deleted' state, a call to ListContainers() can remove it from m_containers. @@ -3459,26 +3564,47 @@ CATCH_RETURN(); void WSLCSession::RecoverExistingContainers() { - WI_ASSERT(m_dockerClient.has_value()); - WI_ASSERT(m_eventTracker.has_value()); - WI_ASSERT(m_virtualMachine.has_value()); + WI_ASSERT(m_runtime.HasDocker()); + WI_ASSERT(m_runtime.HasEvents()); + WI_ASSERT(m_runtime.HasVm()); - auto containers = m_dockerClient->ListContainers(true); // all=true to include stopped containers + auto containers = m_runtime.Docker().ListContainers(true); // all=true to include stopped containers for (const auto& dockerContainer : containers) { + // A kept-alive wrapper's cached state already matches dockerd's (reconciled in OnVmTornDown). + // Leave it (and its COM references) in place, just re-register its ports on the restarted VM. + if (auto existing = m_containers.find(dockerContainer.Id); existing != m_containers.end()) + { + // Isolate a port re-reservation conflict to this container so it can't fail the lazy start + // for every client: log, warn and continue, mirroring the Open() failure path below. + try + { + // Honor --rm for a survivor forced to Exited in OnVmTornDown (delete deferred): remove + // it rather than leaking it. Otherwise re-register its VM-scoped port allocations. + bool removed = false; + auto wrapper = existing->second->RemoveExitedAutoRemoveSurvivor(removed); + if (removed) + { + m_containers.erase(existing); + continue; + } + + existing->second->RecoverPorts(dockerContainer); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION_MSG("Failed to recover ports for container: %hs", dockerContainer.Id.c_str()); + EMIT_USER_WARNING( + Localization::MessageWslcFailedToRecoverContainer(wsl::shared::string::MultiByteToWide(dockerContainer.Id))); + } + continue; + } + try { auto container = WSLCContainerImpl::Open( - dockerContainer, - *this, - m_virtualMachine.value(), - m_pluginNotifier.get(), - m_volumes.value(), - std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), - m_eventTracker.value(), - m_dockerClient.value(), - m_ioRelay); + dockerContainer, *this, m_runtime, m_pluginNotifier.get(), std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1)); auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container)); WI_ASSERT(inserted); @@ -3499,10 +3625,10 @@ void WSLCSession::RecoverExistingContainers() void WSLCSession::RecoverExistingNetworks() { - WI_ASSERT(m_dockerClient.has_value()); - WI_ASSERT(m_virtualMachine.has_value()); + WI_ASSERT(m_runtime.HasDocker()); + WI_ASSERT(m_runtime.HasVm()); - auto networks = m_dockerClient->ListNetworks(); + auto networks = m_runtime.Docker().ListNetworks(); std::lock_guard networksLock(m_networksLock); diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index 6132910d46..ba66e6d58d 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -18,12 +18,16 @@ Module Name: #include "WSLCCompat.h" #include "WSLCVirtualMachine.h" #include "WSLCContainer.h" +#include "WSLCIdleState.h" #include "WSLCVolumes.h" +#include "WSLCSessionRuntime.h" #include "WSLCNetworkMetadata.h" #include "DockerEventTracker.h" #include "DockerHTTPClient.h" #include "IORelay.h" +#include #include +#include #include namespace wsl::windows::service::wslc { @@ -71,13 +75,20 @@ class UserCOMCallback // // WSLCSession - Implements IWSLCSession for container management. // Runs in a per-user COM server process for security isolation. -// The SYSTEM service creates the VM and passes IWSLCVirtualMachine to Initialize(). +// The SYSTEM service passes an IWSLCVirtualMachineFactory to Initialize(); the VM is created +// lazily on first use and may be torn down when idle and recreated on demand. // class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession : public Microsoft::WRL::RuntimeClass, IWSLCSession, IWSLCCompatSession, IFastRundown, ISupportErrorInfo> { + // WSLCContainer::Delete acquires a VmLease to keep the VM alive (and block idle + // teardown) for the duration of a container deletion. + friend class WSLCContainer; + public: - WSLCSession() = default; + WSLCSession() : m_runtime(*this) + { + } ~WSLCSession(); @@ -143,6 +154,7 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession // Container management. IFACEMETHOD(CreateContainer)(_In_ const WSLCContainerOptions* Options, _In_opt_ IWarningCallback* WarningCallback, _Out_ IWSLCContainer** Container) override; IFACEMETHOD(OpenContainer)(_In_ LPCSTR Id, _In_ IWSLCContainer** Container) override; + IFACEMETHOD(BeginContainerOperation)(_Outptr_ IUnknown** Operation) override; IFACEMETHOD(ListContainers)( _In_opt_ const WSLCListContainersOptions* Options, _Out_ WSLCContainerEntry** Containers, @@ -203,6 +215,7 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession IFACEMETHOD(UnmountWindowsFolder)(_In_ LPCSTR LinuxPath) override; IFACEMETHOD(MapVmPort)(_In_ int Family, _In_ unsigned short WindowsPort, _In_ unsigned short LinuxPort) override; IFACEMETHOD(UnmapVmPort)(_In_ int Family, _In_ unsigned short WindowsPort, _In_ unsigned short LinuxPort) override; + IFACEMETHOD(TriggerIdleTermination)(_Out_ BOOL* WasAlreadyIdle) override; // IWSLCCompatSession - converts the WSLCCompat types to the wslc.idl types and forwards to the methods above. // Methods that have an identical signature in both interfaces (Terminate, DeleteVolume, Authenticate, @@ -259,33 +272,57 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession bool WaitForEventOrSessionTerminating(HANDLE Event, std::chrono::milliseconds Timeout) const; + // Shared idle-termination state. Exposed so VM-scoped objects (e.g. running containers via + // WSLCContainerImpl's ActivityRef) can hold an activity reference for their lifetime without + // keeping the session object itself alive. + std::shared_ptr IdleStateShared() const noexcept + { + return m_runtime.IdleStateShared(); + } + + WSLCSessionRuntime& Runtime() noexcept + { + return m_runtime; + } + + const WSLCSessionRuntime& Runtime() const noexcept + { + return m_runtime; + } + + // Creates an opaque activity token that holds a reference on this session's activity count for + // its lifetime, deferring idle teardown of the VM until every outstanding token is released. + // Used both for transient client operations (BeginContainerOperation) and to keep the VM alive + // for the lifetime of a process whose wrapper a client may keep (root-namespace and exec'd + // processes). + Microsoft::WRL::ComPtr CreateActivityToken(); + private: ULONG m_id = 0; + void PersistSettings(const WSLCSessionInitSettings& Settings, PSID UserSid); + + using VmLease = WSLCSessionRuntime::VmLease; + [[nodiscard]] VmLease AcquireVmLease(); __requires_lock_held(m_userHandlesLock) void CancelUserHandleIO(); __requires_lock_held(m_userCOMCallbacksLock) void CancelUserCOMCallbacks(); - _Requires_shared_lock_held_(m_lock) void CreateContainerImpl(const WSLCContainerOptions* Options, IWSLCContainer** Container); void ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID UserSid); void Ext4Format(const std::string& Device); - _Requires_shared_lock_held_(m_lock) std::string InspectImageLockHeld(const std::string& Id); void OnContainerDeleted(const WSLCContainerImpl* Container); void OnCrashDumpWritten(const std::wstring& DumpPath, const std::string& ProcessName, ULONG Pid, ULONG Signal, ULONGLONG Timestamp); - _Requires_shared_lock_held_(m_lock) void OnImageCreated(const std::string& ImageNameOrId) noexcept; - _Requires_shared_lock_held_(m_lock) void OnImageDeleted(const std::string& ImageId) noexcept; void OnProcessLog(const gsl::span& Data, PCSTR Source); void OnContainerdExited(); void OnDockerdExited(); - void OnVmExited(); ServiceRunningProcess StartProcess( const std::string& Executable, const std::vector& Args, PCSTR LogSource, std::function&& ExitCallback); void InstallTrustedRootCertificates(); @@ -300,35 +337,41 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession void SaveImageImpl(std::pair& RequestCodePair, WSLCHandle OutputHandle, HANDLE CancelEvent); void StreamImageOperation(DockerHTTPClient::HTTPRequestContext& requestContext, LPCSTR Image, LPCSTR OperationName, IProgressCallback* ProgressCallback); - std::optional m_dockerClient; - std::optional m_virtualMachine; - std::optional m_eventTracker; - wil::unique_event m_dockerdReadyEvent{wil::EventOptions::ManualReset}; + // The VM factory is a cross-process proxy supplied by the SYSTEM service at Initialize() time + // but first used later (on demand) from a different thread/apartment. A directly stored proxy + // would fail with RPC_E_WRONG_THREAD, so it is parked in the process Global Interface Table and + // re-fetched (re-marshalled into the calling apartment) each time a VM is created. + wil::com_ptr m_git; + DWORD m_vmFactoryGitCookie{}; + + WSLCSessionRuntime m_runtime; std::wstring m_displayName; std::wstring m_creatorProcessName; std::filesystem::path m_storageVhdPath; - std::filesystem::path m_swapVhdPath; - bool m_storageMounted = false; - // 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. + // N.B. The runtime lock must be acquired before acquiring m_containersLock or m_networksLock. + // These locks protect m_containers without requiring an exclusive hold on the runtime lock. // This allows independent operations to proceed while container bookkeeping remains synchronized. - // WSLCVolumes has its own internal srwlock and does not require m_lock. + // WSLCVolumes has its own internal srwlock and does not require the runtime lock. std::mutex m_containersLock; std::unordered_map> m_containers; - std::optional m_volumes; std::mutex m_networksLock; std::unordered_map m_networks; - wil::unique_event m_sessionTerminatingEvent{wil::EventOptions::ManualReset}; - wil::unique_event m_sessionTerminatedEvent{wil::EventOptions::ManualReset}; - wil::unique_event m_vmExitedEvent; + wil::shared_event m_sessionTerminatingEvent{wil::EventOptions::ManualReset}; + wil::shared_event m_sessionTerminatedEvent{wil::EventOptions::ManualReset}; WSLCVirtualMachineTerminationReason m_terminationReason{WSLCVirtualMachineTerminationReasonUnknown}; std::wstring m_terminationDetails; - wil::srwlock m_lock; - IORelay m_ioRelay; - std::optional m_containerdProcess; - std::optional m_dockerdProcess; + + // Persisted settings required to (re)create the VM on demand. The string fields point + // into the owned storage members below (or m_displayName) so they remain valid for the + // lifetime of the session. + WSLCSessionInitSettings m_settings{}; + std::optional m_settingsCreatorProcessName; + std::optional m_settingsStoragePath; + std::optional m_settingsRootVhdTypeOverride; + std::vector m_userSid; + WSLCFeatureFlags m_featureFlags{}; std::function m_destructionCallback; std::atomic m_terminating{false}; @@ -347,14 +390,12 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession // survive insertions and unrelated erasures, so each CrashDumpSubscription stashes its own // iterator and uses it as an O(1) removal handle when the last reference is released. // The session's lifetime extends past Terminate() (the COM object outlives the VM), so this - // list may outlive m_virtualMachine; that's fine because dispatch only runs while the VM + // list may outlive the runtime VM instance; that's fine because dispatch only runs while the VM // thread is alive. mutable wil::srwlock m_crashDumpLock; _Guarded_by_(m_crashDumpLock) CrashDumpCallbackList m_crashDumpCallbacks; - // Used for testing only. - std::mutex m_allocatedPortsLock; - __guarded_by(m_allocatedPortsLock) std::map, size_t>> m_allocatedPorts; + friend class WSLCSessionRuntime; }; } // namespace wsl::windows::service::wslc diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp new file mode 100644 index 0000000000..0b1aa2204e --- /dev/null +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -0,0 +1,848 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCSessionRuntime.cpp + +Abstract: + + Contains the implementation for WSLCSessionRuntime. + +--*/ + +#include "precomp.h" +#include "WSLCSessionRuntime.h" +#include "WSLCSession.h" +#include "WSLCSessionDefaults.h" + +using wsl::windows::service::wslc::WSLCSessionRuntime; + +namespace { + +constexpr auto c_containerdStorage = wsl::windows::wslc::ContainerdStorageMountPoint; +constexpr DWORD c_processTerminateTimeoutMs = 30 * 1000; +constexpr DWORD c_processKillTimeoutMs = 10 * 1000; + +} // namespace + +namespace wsl::windows::service::wslc { + +WSLCSessionRuntime::WSLCSessionRuntime(WSLCSession& Session) noexcept : m_session(&Session) +{ +} + +void WSLCSessionRuntime::Initialize( + DWORD vmFactoryGitCookie, + wil::com_ptr git, + const WSLCSessionInitSettings* settings, + std::chrono::milliseconds idleGrace, + SessionContext sessionContext, + RuntimeHooks hooks) +{ + m_vmFactoryGitCookie = vmFactoryGitCookie; + m_git = std::move(git); + m_settings = settings; + m_id = sessionContext.Id; + m_displayName = std::move(sessionContext.DisplayName); + m_terminating = sessionContext.Terminating; + m_sessionTerminatingEvent = sessionContext.SessionTerminatingEvent; + m_sessionTerminatedEvent = sessionContext.SessionTerminatedEvent; + m_hooks = std::move(hooks); + + m_idleState->Initialize(idleGrace, [this]() { OnIdleTimer(); }); + + // Session-scoped: subscriptions must survive VM restarts. Rebound per-VM in InitializeDockerRuntime. + m_eventTracker.emplace(*m_session); + + m_initialized = true; +} + +WSLCVirtualMachine& WSLCSessionRuntime::Vm() +{ + WI_ASSERT(m_virtualMachine.has_value()); + return m_virtualMachine.value(); +} + +bool WSLCSessionRuntime::HasVm() const noexcept +{ + return m_virtualMachine.has_value(); +} + +IORelay* WSLCSessionRuntime::Relay() +{ + return m_ioRelay ? &m_ioRelay.value() : nullptr; +} + +bool WSLCSessionRuntime::HasRelay() const noexcept +{ + return m_ioRelay.has_value(); +} + +DockerHTTPClient& WSLCSessionRuntime::Docker() +{ + WI_ASSERT(m_dockerClient.has_value()); + return m_dockerClient.value(); +} + +bool WSLCSessionRuntime::HasDocker() const noexcept +{ + return m_dockerClient.has_value(); +} + +DockerEventTracker& WSLCSessionRuntime::Events() +{ + WI_ASSERT(m_eventTracker.has_value()); + return m_eventTracker.value(); +} + +bool WSLCSessionRuntime::HasEvents() const noexcept +{ + return m_eventTracker.has_value(); +} + +WSLCVolumes& WSLCSessionRuntime::Volumes() +{ + WI_ASSERT(m_volumes.has_value()); + return m_volumes.value(); +} + +bool WSLCSessionRuntime::HasVolumes() const noexcept +{ + return m_volumes.has_value(); +} + +wil::rwlock_release_exclusive_scope_exit WSLCSessionRuntime::TryLockExclusive() noexcept +{ + return m_lock.try_lock_exclusive(); +} + +IdleState& WSLCSessionRuntime::Idle() noexcept +{ + return *m_idleState; +} + +std::shared_ptr WSLCSessionRuntime::IdleStateShared() const noexcept +{ + return m_idleState; +} + +WSLCSessionRuntime::VmState WSLCSessionRuntime::State() const noexcept +{ + return m_vmState.load(); +} + +WSLCSessionRuntime::VmExitDisposition WSLCSessionRuntime::ExitDisposition() const noexcept +{ + return m_vmExitDisposition.load(); +} + +bool WSLCSessionRuntime::VmExited() const noexcept +{ + return m_vmExited.load(); +} + +void WSLCSessionRuntime::ResetDockerdReady() noexcept +{ + m_dockerdReadyEvent.ResetEvent(); +} + +bool WSLCSessionRuntime::IsDockerdReady() const noexcept +{ + return m_dockerdReadyEvent.is_signaled(); +} + +void WSLCSessionRuntime::SignalDockerdReady() noexcept +{ + m_dockerdReadyEvent.SetEvent(); +} + +void WSLCSessionRuntime::SetContainerdProcess(ServiceRunningProcess&& process) +{ + m_containerdProcess = std::move(process); +} + +void WSLCSessionRuntime::SetDockerdProcess(ServiceRunningProcess&& process) +{ + m_dockerdProcess = std::move(process); +} + +void WSLCSessionRuntime::SetSwapVhdPath(std::filesystem::path path) +{ + m_swapVhdPath = std::move(path); +} + +void WSLCSessionRuntime::SetStorageMounted(bool value) noexcept +{ + m_storageMounted = value; +} + +std::mutex& WSLCSessionRuntime::AllocatedPortsLock() noexcept +{ + return m_allocatedPortsLock; +} + +std::map, size_t>>& WSLCSessionRuntime::AllocatedPorts() noexcept +{ + return m_allocatedPorts; +} + +bool WSLCSessionRuntime::IdleTerminationEnabled() const noexcept +{ + // Only tear the VM down when there is persistent storage to recover from. A tmpfs-backed + // session would lose all image/container state on teardown, so its VM is kept alive once started. + return m_settings->StoragePath != nullptr; +} + +int WSLCSessionRuntime::StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs) +{ + auto signalResult = Process.Get().Signal(WSLCSignalSIGTERM); + if (FAILED(signalResult)) + { + LOG_HR_MSG(signalResult, "Failed to terminate process %i", Process.Get().GetPid()); + return -1; + } + + try + { + return Process.Wait(TerminateTimeoutMs); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + try + { + LOG_IF_FAILED(Process.Get().Signal(WSLCSignalSIGKILL)); + return Process.Wait(KillTimeoutMs); + } + CATCH_LOG(); + } + + return -1; +} + +void WSLCSessionRuntime::EnsureVmRunning() +{ + // Reject leases once the session is terminating/terminated, including on the running fast path: + // Shutdown() drops the lock to fire OnVmStopping, and a reentrant lease that finds the VM still + // Running must fail here rather than run work against a VM being permanently torn down. + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating->load() || m_sessionTerminatedEvent.is_signaled()); + + if (m_vmState.load() == VmState::Running) + { + return; + } + + bool started = false; + { + auto lock = m_lock.lock_exclusive(); + + // Re-check under the lock: terminating may have been set since the check above. This also + // bounds VmLease's retry loop: a lease that races with Terminate() fails here instead of + // restarting a VM that is being permanently torn down. + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating->load() || m_sessionTerminatedEvent.is_signaled()); + + if (m_vmState.load() != VmState::Running) + { + StartVmLockHeld(); + started = true; + } + } + + // Notify plugins that a VM has started, outside the exclusive lock: the handler forwards to the + // plugin, which may call back into the session (e.g. WSLCCreateProcess acquires a VM lease and + // the exclusive lock), so firing under the lock would deadlock. EnsureVmRunning's only caller + // (VmLease) holds an activity reference across this call, so idle teardown cannot race the VM + // down in the gap between releasing the lock and notifying. + if (started) + { + NotifyVmStarted(); + } +} + +void WSLCSessionRuntime::NotifyVmStarted() +{ + // Hold m_notifyLock across both the pairing-state flip and the hook invocation, so a concurrent + // NotifyVmStopping on another thread cannot deliver its OnVmStopping in between and observe this + // OnVmStarted arrive late (which would otherwise leave the plugin with a trailing "started" for a + // VM that already stopped). m_notifyLock is recursive: the handler may reentrantly restart the VM + // (e.g. WSLCCreateProcess -> NotifyVmStarted/Stopping) on this same thread, which would self-deadlock + // under a plain mutex. Suppressed once terminating, else a racing Shutdown leaves OnVmStarted unpaired. + // m_vmStartNotified tracks the VM lifecycle, not whether OnVmStarted is installed, so a hooks user + // that sets only OnVmStopping still gets paired stop notifications. + auto lock = std::lock_guard(m_notifyLock); + + if (m_terminating->load()) + { + return; + } + + m_vmStartNotified.store(true); + if (m_hooks.OnVmStarted) + { + m_hooks.OnVmStarted(); + } +} + +void WSLCSessionRuntime::NotifyVmStopping() +{ + // See NotifyVmStarted: hold m_notifyLock across both the pairing decision (paired once with a prior + // OnVmStarted, skipped if bring-up never completed) and the hook invocation, so the two notifications + // cannot interleave across threads. Recursive because the handler may reentrantly restart the VM and + // call NotifyVmStarted on this same thread. + auto lock = std::lock_guard(m_notifyLock); + + if (m_vmStartNotified.exchange(false) && m_hooks.OnVmStopping) + { + m_hooks.OnVmStopping(); + } +} + +bool WSLCSessionRuntime::TryClaimExpectedStop() noexcept +{ + auto expected = VmExitDisposition::Active; + return m_vmExitDisposition.compare_exchange_strong(expected, VmExitDisposition::StopRequested); +} + +bool WSLCSessionRuntime::TryClaimSpontaneousExit() noexcept +{ + auto expected = VmExitDisposition::Active; + return m_vmExitDisposition.compare_exchange_strong(expected, VmExitDisposition::ExitClaimed); +} + +void WSLCSessionRuntime::StartVmLockHeld() +{ + WI_ASSERT(m_vmState.load() != VmState::Running); + + WSL_LOG("WslcVmStarting", TraceLoggingValue(m_id, "SessionId")); + + m_vmState.store(VmState::Starting); + m_vmExitDisposition.store(VmExitDisposition::Active); + + // Tear back down if bring-up fails partway. The VM may have exited on its own during bring-up, + // so claim the stop first and only tear down if we win it (TryClaimExpectedStop()); otherwise + // OnVmExited() owns the teardown and we just release the lock to let its Terminate() finish. + auto startCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { + if (TryClaimExpectedStop()) + { + TearDownVmLockHeld(); + m_vmState.store(VmState::None); + } + else + { + WSL_LOG("WslcVmExitedDuringStart", TraceLoggingValue(m_id, "SessionId")); + } + }); + + // Create a fresh IO relay for this VM instance. The previous one (if any) was stopped + // during teardown and cannot be restarted. + m_ioRelay.emplace(); + + // Create the VM via the factory. Re-fetch the factory from the GIT so we call it through a + // proxy marshalled into this thread's apartment (see m_git). The VM produces crash events; + // the session multiplexes them out to any registered ICrashDumpCallback subscribers via + // OnCrashDumpWritten. + wil::com_ptr vmFactory; + THROW_IF_FAILED(m_git->GetInterfaceFromGlobal(m_vmFactoryGitCookie, __uuidof(IWSLCVirtualMachineFactory), vmFactory.put_void())); + + wil::com_ptr vm; + THROW_IF_FAILED(vmFactory->CreateVirtualMachine(&vm)); + + m_virtualMachine.emplace(vm.get(), m_settings, m_sessionTerminatingEvent.get(), WSLCVirtualMachine::TOnCrashDump(m_hooks.OnCrashDump)); + m_virtualMachine->Initialize(); + + // Get an event from the service that is signaled when the VM exits. + m_vmExitedEvent.reset(); + THROW_IF_FAILED(vm->GetTerminationEvent(&m_vmExitedEvent)); + m_vmExited.store(false); + + if (m_hooks.BringUp) + { + m_hooks.BringUp(); + } + + // Monitor for unexpected VM exit. + m_ioRelay->AddHandle( + std::make_unique(m_vmExitedEvent.get(), std::bind(&WSLCSessionRuntime::OnVmExited, this))); + + if (m_hooks.RecoverState) + { + m_hooks.RecoverState(); + } + + m_vmState.store(VmState::Running); + startCleanup.release(); + + WSL_LOG("WslcVmStarted", TraceLoggingValue(m_id, "SessionId")); +} + +void WSLCSessionRuntime::InitializeDockerRuntime(const std::filesystem::path& storagePath) +{ + // Wait for dockerd to be ready before starting the event tracker. + THROW_WIN32_IF_MSG( + ERROR_TIMEOUT, !m_dockerdReadyEvent.wait(m_settings->BootTimeoutMs), "Timed out waiting for dockerd to start"); + + [[maybe_unused]] auto [pid, ptyMaster, channel] = m_virtualMachine->Fork(WSLC_FORK::Thread); + + m_dockerClient.emplace(std::move(channel), m_virtualMachine->TerminatingEvent(), m_virtualMachine->VmId(), 10 * 1000); + + // (Re)bind the session-scoped event tracker to this VM's docker client and relay. Existing + // container subscriptions are preserved across restarts. + m_eventTracker->Connect(m_dockerClient.value(), *m_ioRelay); + + m_volumes.emplace(m_dockerClient.value(), m_virtualMachine.value(), m_eventTracker.value(), storagePath); +} + +void WSLCSessionRuntime::StopVmLockHeld() +{ + if (m_vmState.load() != VmState::Running) + { + return; + } + + WSL_LOG("WslcVmIdleStop", TraceLoggingValue(m_id, "SessionId")); + + // N.B. The caller has claimed StopRequested (via TryClaimExpectedStop), so VM/dockerd/containerd + // exit callbacks firing from the relay thread during teardown are treated as expected, not as a + // crash. + m_vmState.store(VmState::Stopping); + + TearDownVmLockHeld(); + + m_vmState.store(VmState::None); +} + +void WSLCSessionRuntime::TearDownVmLockHeld(bool CaptureTerminationReason) +{ + // Latch whether the guest is already dead before running session-state cleanup so container + // teardown (ReleaseRuntimeResources) can skip VM-dependent calls, e.g. volume unmounts, on a VM + // that has exited. A graceful stop reaches here with the VM still alive (is_signaled() false), so + // its mounts are unmounted through the live VM; StartVmLockHeld clears this for the next instance. + if (m_vmExitedEvent && m_vmExitedEvent.is_signaled()) + { + m_vmExited.store(true); + } + + if (m_hooks.TearDownSessionState) + { + m_hooks.TearDownSessionState(CaptureTerminationReason); + } + + m_volumes.reset(); + + // Stop the IO relay. + // This stops: + // - container state monitoring. + // - container init process relays + // - execs relays + // - container logs relays + if (m_ioRelay) + { + m_ioRelay->Stop(); + } + + { + std::lock_guard allocatedPortsLock(m_allocatedPortsLock); + m_allocatedPorts.clear(); + } + + // Not reset: session-scoped, subscriptions outlive the VM. Its stream handle dies with the IO relay + // above; InitializeDockerRuntime re-binds it on the next start. + m_dockerClient.reset(); + + if (CaptureTerminationReason) + { + // Default: an explicit/graceful teardown is a shutdown (the VM is still alive and we are + // bringing it down). Overridden below if the VM exited on its own and recorded a cause. + m_lastTerminationReason = WSLCVirtualMachineTerminationReasonShutdown; + m_lastTerminationDetails.clear(); + } + + // Check if the VM has already exited (e.g., killed externally). + // If so, skip operations that require a live VM to avoid unnecessary waits. + // N.B. m_vmExitedEvent may be uninitialized if teardown runs before GetTerminationEvent() succeeds. + if (m_vmExitedEvent && m_vmExitedEvent.is_signaled()) + { + WSL_LOG("SkippingGracefulShutdown_VmDead", TraceLoggingValue(m_id, "SessionId")); + + // The VM exited on its own, so it recorded the cause. + if (CaptureTerminationReason && m_virtualMachine) + { + wil::unique_cotaskmem_string details; + LOG_IF_FAILED(m_virtualMachine->GetTerminationReason(&m_lastTerminationReason, &details)); + m_lastTerminationDetails = details ? details.get() : L""; + } + } + else if (m_virtualMachine) + { + m_virtualMachine->OnSessionTerminated(); + + // Stop dockerd first, then containerd (dockerd is a client of containerd). + // N.B. dockerd waits a couple seconds if there are any outstanding HTTP request sockets opened. + if (m_dockerdProcess.has_value()) + { + auto dockerdExitCode = StopProcess(m_dockerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs); + WSL_LOG("DockerdExit", TraceLoggingValue(dockerdExitCode, "code")); + } + + if (m_containerdProcess.has_value()) + { + auto containerdExitCode = StopProcess(m_containerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs); + WSL_LOG("ContainerdExit", TraceLoggingValue(containerdExitCode, "code")); + } + + // N.B. dockerd has exited by this point, so unmounting the VHD is safe since no container can be running. + if (m_storageMounted) + { + try + { + m_virtualMachine->Unmount(c_containerdStorage); + } + CATCH_LOG(); + } + } + + m_dockerdProcess.reset(); + m_containerdProcess.reset(); + m_virtualMachine.reset(); + m_storageMounted = false; + + // Destroy the relay unless we're on its own thread (~IORelay joins the thread, which would + // deadlock). On unexpected-VM-exit path (runs on relay thread), leave it for ~WSLCSession. + if (!m_ioRelay || !m_ioRelay->IsRelayThread()) + { + m_ioRelay.reset(); + m_vmExitedEvent.reset(); + } + + // Delete the ephemeral swap VHD now that the VM is gone. + if (!m_swapVhdPath.empty()) + { + LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_swapVhdPath.c_str())); + m_swapVhdPath.clear(); + } +} + +void WSLCSessionRuntime::OnIdleTimer() +try +{ + // Idle teardown releases cross-process COM proxies (the VM and its VM-scoped state), so this + // threadpool callback must join the process MTA; otherwise those Release/calls fail with + // RPC_E_WRONG_THREAD. The function-try-block keeps this (and everything below) under CATCH_LOG: + // the threadpool callback that invokes us is noexcept, so an escaping throw would terminate. + const auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED); + + if (m_terminating->load() || !IdleTerminationEnabled()) + { + return; + } + + // Non-blocking acquire: a blocking exclusive would queue behind in-flight operations, and + // SRW locks favor waiting writers, stalling all new ops. If the lock is held, an operation + // is in flight; it holds an activity reference and will re-arm the timer (via the 1->0 + // transition) when it releases, so there is nothing to do here. + auto lock = m_lock.try_lock_exclusive(); + if (!lock) + { + return; + } + + // Re-check every teardown precondition under the lock. The activity count is the single + // source of truth for "the VM is needed"; a 0->1 transition since the timer fired (cancel + // raced the callback) is caught here. + if (m_terminating->load() || m_vmState.load() != VmState::Running || m_idleState->ActivityCount() != 0) + { + return; + } + + // Claim the stop. If we lose, OnVmExited() owns a spontaneous-exit teardown and is spinning for + // this lock, so release it and let that run instead of joining the relay ourselves. + if (!TryClaimExpectedStop()) + { + return; + } + + // Restore Active on completion (or early exit) so the next StartVmLockHeld starts clean; only + // clear our own claim. + auto dispositionCleanup = wil::scope_exit([this]() { + auto stopRequested = VmExitDisposition::StopRequested; + m_vmExitDisposition.compare_exchange_strong(stopRequested, VmExitDisposition::Active); + }); + + // Final activity re-check under the lock before the irreversible OnVmStopping notification: a + // VmLease that bumped the count since the check above is now blocked on the shared lock we hold. + // Abandon here -- no notification, no teardown -- rather than stopping a VM about to be used again. + if (m_idleState->ActivityCount() != 0) + { + return; + } + + // Fire OnVmStopping with m_lock dropped so a plugin handler may take a VM lease without + // deadlocking. A lease that races in during this window finds the VM still Running (StopVmLockHeld + // has not run yet), so it does not restart and may leave a long-lived activity token (e.g. a + // process keep-alive). Re-check the activity count after reacquiring the lock: if it is non-zero, + // abandon the teardown and re-pair the notification with a fresh OnVmStarted (with m_lock dropped, + // same reentrancy reason) rather than tearing down a VM that is in use again. StopVmLockHeld no-ops + // if a concurrent Terminate already tore the VM down, and TearDownVmLockHeld handles a VM that died + // in the window. + lock.reset(); + NotifyVmStopping(); + lock = m_lock.lock_exclusive(); + + // If the VM crashed while the lock was dropped, OnVmExited() declined the teardown (we hold the + // expected-stop claim) and its exit handle is one-shot. Do not re-pair OnVmStarted onto a dead VM; + // tear it down so a waiting lease restarts a fresh instance. StopVmLockHeld skips guest-dependent + // calls on a dead VM. + if (m_vmExitedEvent && m_vmExitedEvent.is_signaled()) + { + StopVmLockHeld(); + return; + } + + if (m_idleState->ActivityCount() != 0) + { + // Release the expected-stop claim before dropping the lock again for NotifyVmStarted(): that + // call runs with m_lock dropped, and a VM crash in that window must be claimable by + // OnVmExited() as a normal spontaneous exit. Leaving the claim held here would make + // OnVmExited() silently decline the crash (disposition mismatch) while m_vmState stays + // Running, wedging the session until an explicit Terminate/Shutdown. dispositionCleanup's + // CAS on return is then a harmless no-op. + auto stopRequested = VmExitDisposition::StopRequested; + m_vmExitDisposition.compare_exchange_strong(stopRequested, VmExitDisposition::Active); + + lock.reset(); + NotifyVmStarted(); + return; + } + + StopVmLockHeld(); +} +CATCH_LOG(); + +bool WSLCSessionRuntime::TriggerIdleTerminationForTest() +{ + // tmpfs sessions have no persistent storage to recover from, so tearing the VM down loses all + // state. Match OnIdleTimer and refuse, so this shipping hook can't force data loss. + if (!IdleTerminationEnabled()) + { + return false; + } + + // Mirror OnIdleTimer's MTA context on a dedicated thread: the incoming RPC thread is an STA, so + // both re-initializing MTA on it and running teardown inline would fail (RPC_E_CHANGED_MODE / + // RPC_E_WRONG_THREAD when releasing the VM's cross-process COM proxies). + bool wasAlreadyIdle = false; + std::exception_ptr error; + + std::thread worker([&]() { + try + { + const auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED); + + auto lock = m_lock.lock_exclusive(); + + if (m_terminating->load() || m_vmState.load() != VmState::Running) + { + wasAlreadyIdle = true; + return; + } + + if (!TryClaimExpectedStop()) + { + wasAlreadyIdle = true; + return; + } + + auto dispositionCleanup = wil::scope_exit([this]() { + auto stopRequested = VmExitDisposition::StopRequested; + m_vmExitDisposition.compare_exchange_strong(stopRequested, VmExitDisposition::Active); + }); + + // Fire OnVmStopping without holding m_lock (see OnIdleTimer) so a plugin handler can + // acquire a VM lease without deadlocking. + lock.reset(); + NotifyVmStopping(); + lock = m_lock.lock_exclusive(); + + StopVmLockHeld(); + } + catch (...) + { + error = std::current_exception(); + } + }); + + worker.join(); + + if (error) + { + std::rethrow_exception(error); + } + + return wasAlreadyIdle; +} + +WSLCSessionRuntime::VmLease WSLCSessionRuntime::AcquireVmLease() +{ + return VmLease(*this); +} + +WSLCSessionRuntime::LockedRuntime WSLCSessionRuntime::Acquire() +{ + return LockedRuntime(*this); +} + +WSLCSessionRuntime::VmLease::VmLease(WSLCSessionRuntime& Runtime) : m_runtime(&Runtime) +{ + // Record an in-flight operation before bringing the VM up so idle teardown cannot tear it down + // between EnsureVmRunning() and acquiring the shared lock. AddActivity cancels any pending idle + // timer. + m_runtime->m_idleState->AddActivity(); + + auto countCleanup = wil::scope_exit([this]() { + m_runtime->m_idleState->ReleaseActivity(); + m_runtime = nullptr; + }); + + // Activity increment may race with idle teardown. Retry until we hold the lock with VM running. + for (;;) + { + m_runtime->EnsureVmRunning(); + + m_lock = m_runtime->m_lock.lock_shared(); + + if (m_runtime->m_vmState.load() == VmState::Running) + { + break; + } + + m_lock.reset(); + } + + countCleanup.release(); +} + +WSLCSessionRuntime::VmLease::VmLease(VmLease&& Other) noexcept : + m_runtime(std::exchange(Other.m_runtime, nullptr)), m_lock(std::move(Other.m_lock)) +{ +} + +WSLCSessionRuntime::VmLease& WSLCSessionRuntime::VmLease::operator=(VmLease&& Other) noexcept +{ + if (this != &Other) + { + if (m_runtime != nullptr) + { + // Release the shared lock before the activity reference so that, if this was the last + // activity, idle teardown can immediately take the exclusive lock. + m_lock.reset(); + m_runtime->m_idleState->ReleaseActivity(); + } + + m_runtime = std::exchange(Other.m_runtime, nullptr); + m_lock = std::move(Other.m_lock); + } + + return *this; +} + +WSLCSessionRuntime::VmLease::~VmLease() +{ + if (m_runtime != nullptr) + { + // Release the shared lock before the activity reference so that, if this was the last + // activity, idle teardown can immediately take the exclusive lock. ReleaseActivity arms the + // idle timer on the 1->0 transition. + m_lock.reset(); + m_runtime->m_idleState->ReleaseActivity(); + } +} + +WSLCSessionRuntime::LockedRuntime::LockedRuntime(WSLCSessionRuntime& Runtime) : + m_runtime(&Runtime), m_lease(Runtime.AcquireVmLease()) +{ +} + +WSLCVirtualMachine& WSLCSessionRuntime::LockedRuntime::Vm() +{ + return m_runtime->Vm(); +} + +IORelay* WSLCSessionRuntime::LockedRuntime::Relay() +{ + return m_runtime->Relay(); +} + +DockerHTTPClient& WSLCSessionRuntime::LockedRuntime::Docker() +{ + return m_runtime->Docker(); +} + +void WSLCSessionRuntime::OnVmExited() +{ + // A spontaneous exit we must permanently terminate, unless an expected stop already claimed it, + // in which case the exit was wanted and we decline. + if (!TryClaimSpontaneousExit()) + { + WSL_LOG("WslcVmExitedDuringStop", TraceLoggingValue(m_id, "SessionId")); + return; + } + + WSL_LOG( + "VmExited", + TraceLoggingLevel(WINEVENT_LEVEL_WARNING), + TraceLoggingValue(m_id, "SessionId"), + TraceLoggingValue(m_displayName.c_str(), "Name"), + TraceLoggingValue(!m_sessionTerminatingEvent.is_signaled(), "Unexpected")); + + if (m_hooks.OnSpontaneousExit) + { + m_hooks.OnSpontaneousExit(); + } +} + +void WSLCSessionRuntime::Shutdown( + wil::rwlock_release_exclusive_scope_exit& runtimeLock, WSLCVirtualMachineTerminationReason& terminationReason, std::wstring& terminationDetails) +{ + if (!m_initialized) + { + return; + } + + // runtimeLock is an exclusive hold on m_lock, guaranteeing no operation is running. + WI_VERIFY(runtimeLock); + + // Notify with m_lock dropped, then re-lock for the teardown. The handler may call back into the + // session (e.g. WSLCCreateProcess) which takes a VM lease and this lock; firing under it would + // deadlock. m_terminating is set, so any such reentrant lease fails at EnsureVmRunning's gate + // rather than restarting the VM, and the reacquire can't block on it. + runtimeLock.reset(); + NotifyVmStopping(); + runtimeLock = m_lock.lock_exclusive(); + + // Tear down the VM (if running) and all VM-scoped state, capturing the termination reason; the + // reacquired exclusive hold on m_lock satisfies TearDownVmLockHeld's precondition. + TearDownVmLockHeld(/* CaptureTerminationReason */ true); + + m_vmState.store(VmState::None); + + terminationReason = m_lastTerminationReason; + terminationDetails = m_lastTerminationDetails; + + // Signal completion last so any observer of the terminated event sees a fully torn-down + // session and a populated termination reason. + m_sessionTerminatedEvent.SetEvent(); + + // Release the exclusive lock before disarming the idle timer. If a timer callback is currently + // blocked acquiring the exclusive lock (about to evaluate idle teardown), it must be able to + // obtain it, observe m_terminating, and return — otherwise Disarm()'s wait for in-flight + // callbacks below would deadlock. + runtimeLock.reset(); + + // Permanently disable idle teardown and drain any in-flight timer callback so it cannot + // reference this session after it is destroyed. + m_idleState->Disarm(); +} + +} // namespace wsl::windows::service::wslc diff --git a/src/windows/wslcsession/WSLCSessionRuntime.h b/src/windows/wslcsession/WSLCSessionRuntime.h new file mode 100644 index 0000000000..aeba133296 --- /dev/null +++ b/src/windows/wslcsession/WSLCSessionRuntime.h @@ -0,0 +1,250 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCSessionRuntime.h + +Abstract: + + Contains the definition for WSLCSessionRuntime. + +--*/ + +#pragma once + +#include "wslc.h" +#include "WSLCVirtualMachine.h" +#include "WSLCVolumes.h" +#include "WSLCIdleState.h" +#include "DockerEventTracker.h" +#include "DockerHTTPClient.h" +#include "IORelay.h" +#include "ServiceProcessLauncher.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wsl::windows::service::wslc { + +class WSLCSession; + +class WSLCSessionRuntime +{ +public: + enum class VmState + { + None, + Starting, + Running, + Stopping, + }; + + enum class VmExitDisposition + { + Active, + StopRequested, + ExitClaimed, + }; + + struct RuntimeHooks + { + std::function BringUp; + std::function RecoverState; + // Invoked while tearing down the VM, with the VM-scoped state still alive. The argument is + // true only for a permanent session shutdown (not an idle teardown): on idle teardown the + // container wrappers must be kept alive so client COM references stay valid and are reused + // when the VM restarts. + std::function TearDownSessionState; + std::function OnSpontaneousExit; + WSLCVirtualMachine::TOnCrashDump OnCrashDump; + + // Fired when a VM has started (best-effort). Invoked without the runtime lock held so the + // handler may call back into the session (e.g. to run setup in the VM). Fired every time a + // VM is (re)created for the session. + std::function OnVmStarted; + + // Fired when a VM is about to be torn down (best-effort), while the VM is still alive. Invoked + // without the runtime lock held so the handler may call back into the session or into a plugin + // that acquires a VM lease. Paired once with a prior OnVmStarted -- fires on both idle and + // permanent teardown. On a permanent teardown the session is terminating, so a callback that + // resolves this session (e.g. to create a process) fails cleanly instead of restarting the VM. + std::function OnVmStopping; + }; + + struct SessionContext + { + ULONG Id{}; + std::wstring DisplayName; + const std::atomic* Terminating{}; + wil::shared_event SessionTerminatingEvent; + wil::shared_event SessionTerminatedEvent; + }; + + class VmLease + { + public: + VmLease() = default; + explicit VmLease(WSLCSessionRuntime& Runtime); + VmLease(VmLease&& Other) noexcept; + VmLease& operator=(VmLease&& Other) noexcept; + ~VmLease(); + + VmLease(const VmLease&) = delete; + VmLease& operator=(const VmLease&) = delete; + + private: + WSLCSessionRuntime* m_runtime{}; + wil::rwlock_release_shared_scope_exit m_lock; + }; + + class LockedRuntime + { + public: + LockedRuntime() = default; + explicit LockedRuntime(WSLCSessionRuntime& Runtime); + + WSLCVirtualMachine& Vm(); + IORelay* Relay(); + DockerHTTPClient& Docker(); + + private: + WSLCSessionRuntime* m_runtime{}; + VmLease m_lease; + }; + + explicit WSLCSessionRuntime(WSLCSession& Session) noexcept; + + void Initialize( + DWORD vmFactoryGitCookie, + wil::com_ptr git, + const WSLCSessionInitSettings* settings, + std::chrono::milliseconds idleGrace, + SessionContext sessionContext, + RuntimeHooks hooks); + + WSLCVirtualMachine& Vm(); + bool HasVm() const noexcept; + IORelay* Relay(); + bool HasRelay() const noexcept; + DockerHTTPClient& Docker(); + bool HasDocker() const noexcept; + DockerEventTracker& Events(); + bool HasEvents() const noexcept; + WSLCVolumes& Volumes(); + bool HasVolumes() const noexcept; + [[nodiscard]] wil::rwlock_release_exclusive_scope_exit TryLockExclusive() noexcept; + IdleState& Idle() noexcept; + std::shared_ptr IdleStateShared() const noexcept; + VmState State() const noexcept; + VmExitDisposition ExitDisposition() const noexcept; + bool VmExited() const noexcept; + void ResetDockerdReady() noexcept; + bool IsDockerdReady() const noexcept; + void SignalDockerdReady() noexcept; + void SetContainerdProcess(ServiceRunningProcess&& process); + void SetDockerdProcess(ServiceRunningProcess&& process); + + void SetSwapVhdPath(std::filesystem::path path); + void SetStorageMounted(bool value) noexcept; + + std::mutex& AllocatedPortsLock() noexcept; + std::map, size_t>>& AllocatedPorts() noexcept; + + [[nodiscard]] bool TryClaimExpectedStop() noexcept; + [[nodiscard]] bool TryClaimSpontaneousExit() noexcept; + + _Requires_exclusive_lock_held_(m_lock) + void StartVmLockHeld(); + _Requires_exclusive_lock_held_(m_lock) + void StopVmLockHeld(); + _Requires_exclusive_lock_held_(m_lock) + void TearDownVmLockHeld(bool CaptureTerminationReason = false); + void EnsureVmRunning(); + void OnIdleTimer(); + void OnVmExited(); + void InitializeDockerRuntime(const std::filesystem::path& storagePath); + [[nodiscard]] VmLease AcquireVmLease(); + [[nodiscard]] LockedRuntime Acquire(); + + [[nodiscard]] bool TriggerIdleTerminationForTest(); + + // runtimeLock is an exclusive hold on m_lock (this runtime's lock), which is dropped and reacquired + // internally so the OnVmStopping notification can fire without it and TearDownVmLockHeld runs with it. + void Shutdown(wil::rwlock_release_exclusive_scope_exit& runtimeLock, WSLCVirtualMachineTerminationReason& terminationReason, std::wstring& terminationDetails); + +private: + bool IdleTerminationEnabled() const noexcept; + int StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs); + + // Fires the OnVmStarted hook. Must be called without the runtime lock held (the handler may + // call back into the session), with an activity reference held so idle teardown cannot race the + // VM down before the notification is delivered. + void NotifyVmStarted(); + + // Fires the OnVmStopping hook (paired once with a prior OnVmStarted via m_vmStartNotified). Must + // be called without the runtime lock held: the handler may call into a plugin that acquires a VM + // lease (which takes m_lock), so firing under the lock would deadlock. Called while the VM is + // still running so the handler can still operate on it. + void NotifyVmStopping(); + + WSLCSession* m_session{}; + RuntimeHooks m_hooks; + + ULONG m_id{}; + std::wstring m_displayName; + bool m_initialized{}; + const std::atomic* m_terminating{}; + wil::shared_event m_sessionTerminatingEvent; + wil::shared_event m_sessionTerminatedEvent; + + DWORD m_vmFactoryGitCookie{}; + wil::com_ptr m_git; + const WSLCSessionInitSettings* m_settings{}; + + std::optional m_virtualMachine; + std::optional m_ioRelay; + std::optional m_eventTracker; + std::optional m_dockerClient; + std::optional m_volumes; + std::optional m_containerdProcess; + std::optional m_dockerdProcess; + wil::unique_event m_vmExitedEvent; + // Lock-free mirror of "the current VM instance has exited": written under the runtime lock when a + // VM starts (false) or is observed dead during teardown (true), read without the lock by container + // teardown to skip VM-dependent cleanup. Avoids racing on m_vmExitedEvent, which is reset/replaced + // under the lock. + std::atomic m_vmExited{false}; + wil::unique_event m_dockerdReadyEvent{wil::EventOptions::ManualReset}; + + std::filesystem::path m_swapVhdPath; + bool m_storageMounted{false}; + std::mutex m_allocatedPortsLock; + std::map, size_t>> m_allocatedPorts; + + wil::srwlock m_lock; + std::atomic m_vmState{VmState::None}; + std::atomic m_vmExitDisposition{VmExitDisposition::Active}; + + // Set when OnVmStarted has fired for the current VM; gates the paired OnVmStopping so a VM that + // never finished starting (or had no started notification) does not emit a spurious stopping. + // The gate flip and the hook invocation both happen under m_notifyLock, so a start and a stop + // racing on different threads cannot interleave (which would otherwise let a stale OnVmStarted be + // delivered after the OnVmStopping it should have preceded). Recursive because the handler may + // reentrantly restart the VM, re-entering these notifications on the same thread. + std::recursive_mutex m_notifyLock; + std::atomic m_vmStartNotified{false}; + std::shared_ptr m_idleState{std::make_shared()}; + + WSLCVirtualMachineTerminationReason m_lastTerminationReason{WSLCVirtualMachineTerminationReasonUnknown}; + std::wstring m_lastTerminationDetails; +}; + +} // namespace wsl::windows::service::wslc diff --git a/src/windows/wslcsession/WSLCVirtualMachine.cpp b/src/windows/wslcsession/WSLCVirtualMachine.cpp index 474c1a81f3..e934dae937 100644 --- a/src/windows/wslcsession/WSLCVirtualMachine.cpp +++ b/src/windows/wslcsession/WSLCVirtualMachine.cpp @@ -34,8 +34,8 @@ constexpr auto CONTAINER_PORT_RANGE = std::pair(20002, 65535 static_assert(c_ephemeralPortRange.second < CONTAINER_PORT_RANGE.first); -VmPortAllocation::VmPortAllocation(uint16_t port, int family, int protocol, WSLCVirtualMachine& vm) : - m_port(port), m_family(family), m_protocol(protocol), m_vm(&vm) +VmPortAllocation::VmPortAllocation(uint16_t port, int family, int protocol, std::weak_ptr reservations) : + m_port(port), m_family(family), m_protocol(protocol), m_reservations(std::move(reservations)) { } @@ -52,7 +52,7 @@ VmPortAllocation& VmPortAllocation::operator=(VmPortAllocation&& Other) m_port = Other.m_port; m_family = Other.m_family; m_protocol = Other.m_protocol; - m_vm = Other.m_vm; + m_reservations = Other.m_reservations; Other.Release(); } @@ -66,16 +66,20 @@ VmPortAllocation::~VmPortAllocation() void VmPortAllocation::Reset() { - if (m_vm != nullptr) + // Release the reservation only if the owning VM (and its table) is still alive. If the VM was torn + // down the table is already gone and lock() returns null, so a surviving allocation is a safe no-op. + if (auto reservations = m_reservations.lock()) { - m_vm->ReleasePort(*this); - Release(); + std::lock_guard lock{reservations->Mutex}; + LOG_HR_IF(E_UNEXPECTED, reservations->Ports.erase(m_port) != 1); } + + Release(); } void VmPortAllocation::Release() { - m_vm = nullptr; + m_reservations.reset(); m_port = 0; m_family = 0; m_protocol = 0; @@ -1270,33 +1274,29 @@ void WSLCVirtualMachine::OnSessionTerminated() std::shared_ptr WSLCVirtualMachine::TryAllocatePort(uint16_t Port, int Family, int Protocol) { - std::lock_guard lock{m_lock}; + std::lock_guard lock{m_reservations->Mutex}; WSL_LOG("AllocatePort", TraceLoggingValue(Port, "Port")); - auto [_, inserted] = m_allocatedPorts.insert(Port); - - if (inserted) - { - return std::make_shared(Port, Family, Protocol, *this); - } - else + if (!m_reservations->Ports.insert(Port).second) { return {}; } + + return std::make_shared(Port, Family, Protocol, m_reservations); } std::shared_ptr WSLCVirtualMachine::AllocatePort(int Family, int Protocol) { - std::lock_guard lock{m_lock}; + std::lock_guard lock{m_reservations->Mutex}; for (uint32_t i = CONTAINER_PORT_RANGE.first; i <= CONTAINER_PORT_RANGE.second; i++) { uint16_t port = static_cast(i); - if (!m_allocatedPorts.contains(port)) + if (!m_reservations->Ports.contains(port)) { - WI_VERIFY(m_allocatedPorts.insert(port).second); - return std::make_shared(port, Family, Protocol, *this); + WI_VERIFY(m_reservations->Ports.insert(port).second); + return std::make_shared(port, Family, Protocol, m_reservations); } } @@ -1304,15 +1304,6 @@ std::shared_ptr WSLCVirtualMachine::AllocatePort(int Family, i THROW_HR_MSG(HRESULT_FROM_WIN32(ERROR_NO_SYSTEM_RESOURCES), "Failed to allocate port"); } -void WSLCVirtualMachine::ReleasePort(VmPortAllocation& Port) -{ - std::lock_guard lock{m_lock}; - - WSL_LOG("ReleasePort", TraceLoggingValue(Port.Port(), "Port")); - - LOG_HR_IF(E_UNEXPECTED, m_allocatedPorts.erase(Port.Port()) != 1); -} - wil::unique_socket WSLCVirtualMachine::ConnectUnixSocket(const char* Path) { auto [_, __, channel] = Fork(WSLC_FORK::Thread); diff --git a/src/windows/wslcsession/WSLCVirtualMachine.h b/src/windows/wslcsession/WSLCVirtualMachine.h index df7102dc86..59435fe147 100644 --- a/src/windows/wslcsession/WSLCVirtualMachine.h +++ b/src/windows/wslcsession/WSLCVirtualMachine.h @@ -49,11 +49,20 @@ struct WSLCProcessFd class WSLCVirtualMachine; +// Owns the set of in-use VM-side port numbers for a single VM instance. Held by shared_ptr from the +// VM (sole owner) and referenced weakly by each VmPortAllocation, so a VM teardown drops every +// reservation and any surviving allocation self-neuters instead of dangling into a freed VM. +struct VmPortReservations +{ + std::mutex Mutex; + std::set Ports; +}; + struct VmPortAllocation { NON_COPYABLE(VmPortAllocation); - VmPortAllocation(uint16_t port, int Family, int Protocol, WSLCVirtualMachine& vm); + VmPortAllocation(uint16_t port, int Family, int Protocol, std::weak_ptr reservations); VmPortAllocation(VmPortAllocation&& Other); ~VmPortAllocation(); @@ -69,7 +78,7 @@ struct VmPortAllocation uint16_t m_port{}; int m_family{}; int m_protocol{}; - WSLCVirtualMachine* m_vm{}; + std::weak_ptr m_reservations; }; struct VMPortMapping @@ -146,7 +155,6 @@ class WSLCVirtualMachine std::shared_ptr TryAllocatePort(uint16_t Port, int Family, int Protocol); std::shared_ptr AllocatePort(int Family, int Protocol); - void ReleasePort(VmPortAllocation& Port); Microsoft::WRL::ComPtr CreateLinuxProcess( _In_ LPCSTR Executable, @@ -249,7 +257,7 @@ class WSLCVirtualMachine std::thread m_processExitThread; std::thread m_crashDumpThread; - std::set m_allocatedPorts; + std::shared_ptr m_reservations = std::make_shared(); GUID m_vmId{}; diff --git a/test/windows/PluginTests.cpp b/test/windows/PluginTests.cpp index 9abfcaaedc..a42a3ce111 100644 --- a/test/windows/PluginTests.cpp +++ b/test/windows/PluginTests.cpp @@ -606,7 +606,7 @@ class PluginTests return sessionManager; } - static wil::com_ptr CreateWslcSession(LPCWSTR Name, WSLCNetworkingMode NetworkingMode = WSLCNetworkingModeNone) + static wil::com_ptr CreateWslcSession(LPCWSTR Name, WSLCNetworkingMode NetworkingMode = WSLCNetworkingModeNone, LPCWSTR StoragePath = nullptr) { WSLCSessionSettings settings{}; settings.DisplayName = Name; @@ -614,6 +614,8 @@ class PluginTests settings.MemoryMb = 4096; settings.BootTimeoutMs = 30 * 1000; settings.NetworkingMode = NetworkingMode; + settings.StoragePath = StoragePath; + settings.MaximumStorageSizeMb = 1024 * 20; // 20GB, only used when StoragePath is set. auto manager = OpenWslcSessionManager(); wil::com_ptr session; @@ -780,6 +782,67 @@ class PluginTests ValidateLogFile(ExpectedOutput); } + // Validates the VM-lifecycle hooks: OnWslcVmStarted fires each time the VM is (re)created and + // OnWslcVmStopping each time it is torn down, decoupled from the once-per-session hooks. Also + // proves the started hook can call back into the session (WSLCCreateProcess) without deadlocking. + WSL2_TEST_METHOD(WslcVmRestart) + { + ConfigurePlugin(PluginTestType::WslcVmRestart); + + // Idle termination only tears down storage-backed sessions (tmpfs state is unrecoverable), so + // this restart lifecycle test needs a dedicated persistent storage directory. + const auto storageDir = std::filesystem::current_path() / "test-storage-wslc-vm-restart"; + std::error_code storageError; + std::filesystem::remove_all(storageDir, storageError); + std::filesystem::create_directories(storageDir); + auto storageCleanup = wil::scope_exit([&]() { + std::error_code ec; + std::filesystem::remove_all(storageDir, ec); + }); + + { + auto session = CreateWslcSession(L"plugin-wslc-vm-restart", WSLCNetworkingModeNone, storageDir.c_str()); + + // First operation brings the VM up -> OnWslcVmStarted (which reentrantly runs a process). + { + wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"}); + auto process = launcher.Launch(*session); + } + + // Force idle teardown of the running VM -> OnWslcVmStopping. + BOOL wasAlreadyIdle = TRUE; + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle)); + VERIFY_IS_FALSE(wasAlreadyIdle); + + // Next operation lazily restarts the VM -> OnWslcVmStarted fires again. + { + wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"}); + auto process = launcher.Launch(*session); + } + + // Session teardown tears the second VM down -> OnWslcVmStopping, then OnWslcSessionStopping. + } + + constexpr auto ExpectedOutput = + LR"(Plugin loaded. TestMode=22 + WSLC Session created, name=plugin-wslc-vm-restart, id=*, pid=*, token=set, sid=set + WSLC VM started, session=* + WSLC VM started reentrant WSLCCreateProcess: ok + WSLC VM started mount+unmount: ok + WSLC VM stopping, session=* + WSLC VM stopping reentrant WSLCCreateProcess: ok + WSLC VM stopping mount+unmount: ok + WSLC VM started, session=* + WSLC VM started reentrant WSLCCreateProcess: ok + WSLC VM started mount+unmount: ok + WSLC VM stopping, session=* + WSLC VM stopping reentrant WSLCCreateProcess: failed + WSLC VM stopping mount+unmount: skipped + WSLC Session stopping, name=plugin-wslc-vm-restart, id=*)"; + + ValidateLogFile(ExpectedOutput); + } + // This test must run last so it doesn't break test cases that depends on plugin signature. WSL2_TEST_METHOD(InvalidPluginSignature) { diff --git a/test/windows/PluginTests.h b/test/windows/PluginTests.h index c9a779031c..bfe08fcbf7 100644 --- a/test/windows/PluginTests.h +++ b/test/windows/PluginTests.h @@ -41,7 +41,8 @@ enum class PluginTestType WslcSuccess, WslcSessionRejected, WslcContainerRejected, - WslcImagePull + WslcImagePull, + WslcVmRestart }; constexpr auto c_testType = L"TestType"; diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index edc13644b6..8ad1ec6ecc 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -11638,6 +11638,10 @@ class WSLCTests auto settings = GetDefaultSessionSettings(c_sessionName); auto session = CreateSession(settings); + // Session creation is lazy, so start the VM by launching a process before killing it. + WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"}); + auto process = launcher.Launch(*session); + KillVmByOwner(c_sessionName); WaitForSessionTermination(session.get()); @@ -11662,6 +11666,243 @@ class WSLCTests VERIFY_IS_FALSE(IsVmRunning(c_sessionName)); } + // TriggerIdleTermination runs the idle-teardown path synchronously and reports whether the VM + // was already idle. Validates the idle -> running -> forced-idle -> running lifecycle. + WSLC_TEST_METHOD(TriggerIdleTerminationRestartsVm) + { + constexpr auto c_sessionName = L"wslc-idle-trigger-test"; + + // Idle termination is only permitted for storage-backed sessions (tmpfs state is + // unrecoverable), so this lifecycle test uses a dedicated storage directory. + const auto storageDir = std::filesystem::current_path() / "test-storage-idle-restart"; + std::error_code storageError; + std::filesystem::remove_all(storageDir, storageError); + std::filesystem::create_directories(storageDir); + auto storageCleanup = wil::scope_exit([&]() { + std::error_code ec; + std::filesystem::remove_all(storageDir, ec); + }); + + auto settings = GetDefaultSessionSettings(c_sessionName); + settings.StoragePath = storageDir.c_str(); + auto session = CreateSession(settings); + + // The VM starts lazily, so a freshly created session is already idle. + BOOL wasAlreadyIdle = FALSE; + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle)); + VERIFY_IS_TRUE(wasAlreadyIdle); + VERIFY_IS_FALSE(IsVmRunning(c_sessionName)); + + // Starting a process brings the VM up. + WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"}); + auto process = launcher.Launch(*session); + VERIFY_IS_TRUE(IsVmRunning(c_sessionName)); + + // Forcing idle termination tears the running VM down. + wasAlreadyIdle = TRUE; + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle)); + VERIFY_IS_FALSE(wasAlreadyIdle); + VERIFY_IS_FALSE(IsVmRunning(c_sessionName)); + + // A second trigger is now a no-op. + wasAlreadyIdle = FALSE; + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle)); + VERIFY_IS_TRUE(wasAlreadyIdle); + + // The session survives and lazily restarts the VM on the next operation. + WSLCProcessLauncher launcher2("/bin/sleep", {"/bin/sleep", "60"}); + auto process2 = launcher2.Launch(*session); + VERIFY_IS_TRUE(IsVmRunning(c_sessionName)); + } + + // A tmpfs-backed session has no persistent storage, so its VM state cannot be recovered after a + // teardown. TriggerIdleTermination must refuse to tear such a session down (matching the + // automatic idle timer), leaving the VM running rather than destroying unrecoverable state. + WSLC_TEST_METHOD(TriggerIdleTerminationRefusedWithoutStorage) + { + constexpr auto c_sessionName = L"wslc-idle-tmpfs-test"; + auto session = CreateSession(GetDefaultSessionSettings(c_sessionName)); + + WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"}); + auto process = launcher.Launch(*session); + VERIFY_IS_TRUE(IsVmRunning(c_sessionName)); + + BOOL wasAlreadyIdle = TRUE; + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle)); + VERIFY_IS_FALSE(wasAlreadyIdle); + + // The VM must still be running: the tmpfs session was not torn down. + VERIFY_IS_TRUE(IsVmRunning(c_sessionName)); + } + + // A running container pins the VM via its activity hold; TriggerIdleTermination ignores that + // hold and forces teardown. The container's state must survive on persistent storage and be + // recovered against a fresh docker context when the VM lazily restarts. + WSLC_TEST_METHOD(TriggerIdleTerminationRecoversRunningContainer) + { + SKIP_TEST_SERVER(); + + const std::string containerName = "wslc-idle-recovery"; + + auto cleanup = wil::scope_exit([&]() { + wil::com_ptr staleContainer; + if (SUCCEEDED(m_defaultSession->OpenContainer(containerName.c_str(), &staleContainer))) + { + LOG_IF_FAILED(staleContainer->Delete(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes)); + } + }); + + WSLCContainerLauncher launcher("debian:latest", containerName, {"/bin/sleep", "600"}); + auto container = launcher.Launch(*m_defaultSession, WSLCContainerStartFlagsNone); + container.SetDeleteOnClose(false); + + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning); + VERIFY_IS_TRUE(IsVmRunning(c_testSessionName)); + + // Force idle teardown even though the container holds a running-activity reference. + BOOL wasAlreadyIdle = TRUE; + VERIFY_SUCCEEDED(m_defaultSession->TriggerIdleTermination(&wasAlreadyIdle)); + VERIFY_IS_FALSE(wasAlreadyIdle); + VERIFY_IS_FALSE(IsVmRunning(c_testSessionName)); + + // The forced teardown must reconcile the surviving wrapper in place -- not merely leave + // docker's state to be rediscovered on the next open. The init process records a synthetic + // SIGKILL exit and the container drops to Exited while its client COM references stay valid. + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited); + VERIFY_ARE_EQUAL(container.GetInitProcess().Wait(), 128 + WSLCSignalSIGKILL); + + // Re-opening lazily restarts the VM and recovers the container. It must be found (Exited, since + // its init process died with the VM) and remain operable against the new context: restarting it + // brings it back to Running on the freshly booted VM. + auto recovered = OpenContainer(m_defaultSession.get(), containerName); + VERIFY_IS_TRUE(IsVmRunning(c_testSessionName)); + VERIFY_ARE_EQUAL(recovered.State(), WslcContainerStateExited); + + VERIFY_SUCCEEDED(recovered.Get().Start(WSLCContainerStartFlagsNone, nullptr, nullptr)); + VERIFY_ARE_EQUAL(recovered.State(), WslcContainerStateRunning); + } + + // A running --rm container that survives a forced idle teardown is dropped to Exited without the + // auto-remove delete (dockerd is gone by then). On the next VM restart it must be auto-removed + // rather than leaking as a permanent Exited container. + WSLC_TEST_METHOD(TriggerIdleTerminationRemovesExitedAutoRemoveContainer) + { + SKIP_TEST_SERVER(); + + const std::string containerName = "wslc-idle-rm-recovery"; + + auto cleanup = wil::scope_exit([&]() { + wil::com_ptr staleContainer; + if (SUCCEEDED(m_defaultSession->OpenContainer(containerName.c_str(), &staleContainer))) + { + LOG_IF_FAILED(staleContainer->Delete(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes)); + } + }); + + WSLCContainerLauncher launcher("debian:latest", containerName, {"/bin/sleep", "600"}); + launcher.SetContainerFlags(WSLCContainerFlagsRm); + auto container = launcher.Launch(*m_defaultSession, WSLCContainerStartFlagsNone); + container.SetDeleteOnClose(false); + + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning); + VERIFY_IS_TRUE(IsVmRunning(c_testSessionName)); + + BOOL wasAlreadyIdle = TRUE; + VERIFY_SUCCEEDED(m_defaultSession->TriggerIdleTermination(&wasAlreadyIdle)); + VERIFY_IS_FALSE(wasAlreadyIdle); + VERIFY_IS_FALSE(IsVmRunning(c_testSessionName)); + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited); + + // Restart the VM via an unrelated operation, which recovers surviving containers; the exited + // --rm survivor must be auto-removed and absent from the container list. + WSLCProcessLauncher restartLauncher("/bin/true", {"/bin/true"}); + auto process = restartLauncher.Launch(*m_defaultSession); + process.GetExitEvent().wait(5000); + VERIFY_IS_TRUE(IsVmRunning(c_testSessionName)); + + auto [containers, ports] = ListContainers(m_defaultSession.get()); + bool found = false; + for (size_t i = 0; i < containers.size(); i++) + { + if (containerName == containers[i].Name) + { + found = true; + break; + } + } + VERIFY_IS_FALSE(found); + } + + // Hammer the idle-teardown path concurrently with VM-level operations to surface deadlocks or + // stale-state races. Operations may fail while the VM is being torn down; that is tolerated, but + // the workers must never hang and the session must remain usable afterwards. + WSLC_TEST_METHOD(TriggerIdleTerminationConcurrentWithOperations) + { + constexpr auto c_sessionName = L"wslc-idle-hammer-test"; + + // Idle termination only tears down storage-backed sessions, so a dedicated storage directory + // is required for the teardown path to actually run under the concurrent hammering. + const auto storageDir = std::filesystem::current_path() / "test-storage-idle-hammer"; + std::error_code storageError; + std::filesystem::remove_all(storageDir, storageError); + std::filesystem::create_directories(storageDir); + auto storageCleanup = wil::scope_exit([&]() { + std::error_code ec; + std::filesystem::remove_all(storageDir, ec); + }); + + auto settings = GetDefaultSessionSettings(c_sessionName); + settings.StoragePath = storageDir.c_str(); + auto session = CreateSession(settings); + + std::atomic stop = false; + std::atomic opFailures = 0; + + // Run the hammering worker via a future so the join is bounded: a real teardown/operation + // deadlock would otherwise hang the test host indefinitely instead of failing. If the worker + // does not drain within the timeout after we signal stop, treat it as a deadlock and fail. + auto worker = std::async(std::launch::async, [&]() { + while (!stop.load()) + { + try + { + WSLCProcessLauncher launcher("/bin/true", {"/bin/true"}); + auto process = launcher.Launch(*session); + process.GetExitEvent().wait(5000); + } + catch (...) + { + opFailures.fetch_add(1); + } + } + }); + + for (int i = 0; i < 25; ++i) + { + BOOL wasAlreadyIdle = FALSE; + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle)); + } + + stop.store(true); + + // A real teardown/operation deadlock would leave the worker wedged forever. The std::future + // destructor blocks until the task completes, so letting a failed VERIFY unwind here would hang + // the test host -- exactly what this test guards against. Fail fast with a dump on timeout so we + // never unwind with an unfinished async task. + FAIL_FAST_IF_MSG( + worker.wait_for(std::chrono::seconds(60)) != std::future_status::ready, + "hammering worker did not drain after stop; likely teardown/operation deadlock"); + + worker.get(); + + LogInfo("TriggerIdleTerminationConcurrentWithOperations tolerated %u operation failures", opFailures.load()); + + // The session must still be usable after the hammering. + WSLCProcessLauncher launcher("/bin/true", {"/bin/true"}); + auto process = launcher.Launch(*session); + VERIFY_IS_TRUE(process.GetExitEvent().wait(30000)); + } + // Helper: COM callback that captures all warnings received. class CapturingWarningCallback : public Microsoft::WRL::RuntimeClass, IWarningCallback, IFastRundown> @@ -11729,13 +11970,21 @@ class WSLCTests VERIFY_SUCCEEDED(sessionManager2->CreateSession(&settings2, WSLCSessionFlagsNone, warningCallback.Get(), &session2)); wsl::windows::common::security::ConfigureForCOMImpersonation(session2.get()); - // Verify the warning matches the expected localized message for the corrupt container. + // The VM (and container recovery) starts lazily on the first operation. Trigger it via a + // callback-bearing operation (CreateNetwork) so recovery warnings reach the warning callback. + WSLCNetworkOptions triggerNetwork{}; + triggerNetwork.Name = "wslc-recovery-trigger"; + triggerNetwork.Driver = "bridge"; + VERIFY_SUCCEEDED(session2->CreateNetwork(&triggerNetwork, warningCallback.Get())); + + // Recovery runs during the lazy VM start under this operation's context, so the failure + // warning is delivered to its warning callback. auto warnings = warningCallback->GetWarnings(); - auto expectedWarning = std::format( + auto recoveryWarning = std::format( L"wsl: {}\n", wsl::shared::Localization::MessageWslcFailedToRecoverContainer(wsl::shared::string::MultiByteToWide(containerId))); - VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == expectedWarning; })); + VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == recoveryWarning; })); VERIFY_SUCCEEDED(session2->Terminate()); } @@ -11797,12 +12046,20 @@ class WSLCTests VERIFY_SUCCEEDED(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, warningCallback.Get(), &session)); wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); - // Verify the warning matches the expected localized message for the missing volume. + // The VM (and volume recovery) starts lazily on the first operation. Trigger it via a + // callback-bearing operation (CreateNetwork) so recovery warnings reach the warning callback. + WSLCNetworkOptions triggerNetwork{}; + triggerNetwork.Name = "wslc-recovery-trigger"; + triggerNetwork.Driver = "bridge"; + VERIFY_SUCCEEDED(session->CreateNetwork(&triggerNetwork, warningCallback.Get())); + + // Recovery runs during the lazy VM start under this operation's context, so the failure + // warning is delivered to its warning callback. auto warnings = warningCallback->GetWarnings(); - auto expectedWarning = + auto recoveryWarning = std::format(L"wsl: {}\n", wsl::shared::Localization::MessageWslcFailedToRecoverVolume(L"wslc-test-warning-recovery")); - VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == expectedWarning; })); + VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == recoveryWarning; })); // Clean up the orphaned volume from Docker's metadata. LOG_IF_FAILED(session->DeleteVolume("wslc-test-warning-recovery")); @@ -11862,10 +12119,19 @@ class WSLCTests VERIFY_SUCCEEDED(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, warningCallback.Get(), &session)); wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); + // The VM (and guest volume recovery) starts lazily on the first operation. Trigger it via a + // callback-bearing operation (CreateNetwork) so recovery warnings reach the warning callback. + WSLCNetworkOptions triggerNetwork{}; + triggerNetwork.Name = "wslc-recovery-trigger"; + triggerNetwork.Driver = "bridge"; + VERIFY_SUCCEEDED(session->CreateNetwork(&triggerNetwork, warningCallback.Get())); + + // Recovery runs during the lazy VM start under this operation's context, so the failure + // warning is delivered to its warning callback. auto warnings = warningCallback->GetWarnings(); - auto expectedWarning = std::format(L"wsl: {}\n", wsl::shared::Localization::MessageWslcFailedToRecoverVolume(c_volumeName)); + auto recoveryWarning = std::format(L"wsl: {}\n", wsl::shared::Localization::MessageWslcFailedToRecoverVolume(c_volumeName)); - VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == expectedWarning; })); + VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == recoveryWarning; })); // Clean up the volume from Docker's metadata. ExpectCommandResult(session.get(), {"/usr/bin/docker", "volume", "rm", "-f", c_volumeName}, 0); diff --git a/test/windows/testplugin/Plugin.cpp b/test/windows/testplugin/Plugin.cpp index 17087e6edc..3d1201ee28 100644 --- a/test/windows/testplugin/Plugin.cpp +++ b/test/windows/testplugin/Plugin.cpp @@ -539,6 +539,84 @@ HRESULT OnWslcImageDeleted(const WSLCSessionInformation* Session, LPCSTR ImageId return S_OK; } +HRESULT OnWslcVmStarted(const WSLCSessionInformation* Session) +try +{ + // Only log/exercise for the dedicated VM-restart test so other WSLC plugin tests (which start + // and stop VMs incidentally) are not affected by extra log lines. + if (g_testType != PluginTestType::WslcVmRestart) + { + return S_OK; + } + + g_logfile << "WSLC VM started, session=" << Session->SessionId << std::endl; + + // Prove the VM is usable from within the started hook, and that calling back into the session + // (WSLCCreateProcess acquires a VM lease + the runtime lock) does not deadlock. + std::vector args = {"/bin/true", nullptr}; + WSLCProcessHandle process = nullptr; + const auto hr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, nullptr); + g_logfile << "WSLC VM started reentrant WSLCCreateProcess: " << (SUCCEEDED(hr) ? "ok" : "failed") << std::endl; + if (SUCCEEDED(hr)) + { + g_api->WSLCReleaseProcess(process); + } + + // Also exercise a reentrant mount + unmount from the started hook; the session is alive here so + // both calls succeed, validating that mount management reentrant from OnVmStarted does not deadlock. + constexpr auto* mountpoint = "/test-plugin/vm-started-mount"; + const auto mountHr = g_api->WSLCMountFolder(Session->SessionId, L"C:\\", mountpoint, TRUE); + if (SUCCEEDED(mountHr)) + { + const auto unmountHr = g_api->WSLCUnmountFolder(Session->SessionId, mountpoint); + g_logfile << "WSLC VM started mount+unmount: " << (SUCCEEDED(unmountHr) ? "ok" : "failed") << std::endl; + } + else + { + g_logfile << "WSLC VM started mount+unmount: skipped" << std::endl; + } + + return S_OK; +} +CATCH_RETURN(); + +HRESULT OnWslcVmStopping(const WSLCSessionInformation* Session) +try +{ + if (g_testType != PluginTestType::WslcVmRestart) + { + return S_OK; + } + + g_logfile << "WSLC VM stopping, session=" << Session->SessionId << std::endl; + + // Proves OnVmStopping doesn't deadlock a plugin that calls back in: on idle teardown the session + // is alive so these restart the VM and succeed; on permanent teardown they fail cleanly. + std::vector args = {"/bin/true", nullptr}; + WSLCProcessHandle process = nullptr; + const auto processHr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, nullptr); + g_logfile << "WSLC VM stopping reentrant WSLCCreateProcess: " << (SUCCEEDED(processHr) ? "ok" : "failed") << std::endl; + if (SUCCEEDED(processHr)) + { + g_api->WSLCReleaseProcess(process); + } + + constexpr auto* mountpoint = "/test-plugin/vm-stopping-mount"; + const auto mountHr = g_api->WSLCMountFolder(Session->SessionId, L"C:\\", mountpoint, TRUE); + if (SUCCEEDED(mountHr)) + { + const auto unmountHr = g_api->WSLCUnmountFolder(Session->SessionId, mountpoint); + g_logfile << "WSLC VM stopping mount+unmount: " << (SUCCEEDED(unmountHr) ? "ok" : "failed") << std::endl; + } + else + { + g_logfile << "WSLC VM stopping mount+unmount: skipped" << std::endl; + } + + return S_OK; +} +CATCH_RETURN(); + EXTERN_C __declspec(dllexport) HRESULT WSLPLUGINAPI_ENTRYPOINTV1(const WSLPluginAPIV1* Api, WSLPluginHooksV1* Hooks) { try @@ -550,7 +628,7 @@ EXTERN_C __declspec(dllexport) HRESULT WSLPLUGINAPI_ENTRYPOINTV1(const WSLPlugin THROW_HR_IF(E_UNEXPECTED, !g_logfile); g_testType = static_cast(ReadDword(key.get(), nullptr, c_testType, static_cast(PluginTestType::Invalid))); - THROW_HR_IF(E_INVALIDARG, static_cast(g_testType) <= 0 || static_cast(g_testType) > static_cast(PluginTestType::WslcImagePull)); + THROW_HR_IF(E_INVALIDARG, static_cast(g_testType) <= 0 || static_cast(g_testType) > static_cast(PluginTestType::WslcVmRestart)); g_logfile << "Plugin loaded. TestMode=" << static_cast(g_testType) << std::endl; g_api = Api; @@ -566,6 +644,8 @@ EXTERN_C __declspec(dllexport) HRESULT WSLPLUGINAPI_ENTRYPOINTV1(const WSLPlugin Hooks->ContainerStopping = &OnWslcContainerStopping; Hooks->ImageCreated = &OnWslcImageCreated; Hooks->ImageDeleted = &OnWslcImageDeleted; + Hooks->WslcVmStarted = &OnWslcVmStarted; + Hooks->WslcVmStopping = &OnWslcVmStopping; if (g_testType == PluginTestType::FailToLoad) { diff --git a/test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp b/test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp index c04c4b767c..c512452cf8 100644 --- a/test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp @@ -109,9 +109,14 @@ class WSLCE2ESessionEnterTests WSLC_TEST_METHOD(WSLCE2E_SessionEnter_StoragePathNotFound) { auto result = RunWslc(L"system session enter does-not-exist"); - const auto expectedPath = std::filesystem::absolute(L"does-not-exist").wstring(); + + // The CLI resolves the storage argument to an absolute path (see EnterSession task) and the + // service validates it eagerly at session creation, reporting the friendly "No WSLC session + // found in ''" message rather than a bare system error. + const auto storagePath = std::filesystem::absolute(L"does-not-exist").wstring(); result.Verify({ - .Stderr = std::format(L"No WSLC session found in '{}'\r\nError code: ERROR_PATH_NOT_FOUND\r\n", expectedPath), + .Stderr = wsl::shared::Localization::MessageWslcSessionStorageNotFound(storagePath) + + L"\r\nError code: ERROR_PATH_NOT_FOUND\r\n", .ExitCode = 1, }); } diff --git a/test/windows/wslc/e2e/WSLCE2EWarningTests.cpp b/test/windows/wslc/e2e/WSLCE2EWarningTests.cpp index 81afbef07a..a488dcddca 100644 --- a/test/windows/wslc/e2e/WSLCE2EWarningTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EWarningTests.cpp @@ -8,8 +8,9 @@ Module Name: Abstract: - End-to-end tests validating that warnings emitted by the WSLC COM service are - surfaced on the wslc.exe CLI's stderr via the IWarningCallback integration. + End-to-end tests validating how warnings emitted by the WSLC COM service are + surfaced (or intentionally suppressed) on the wslc.exe CLI's stderr via the + IWarningCallback integration. --*/ #include "precomp.h" @@ -72,9 +73,10 @@ class WSLCE2EWarningTests CATCH_LOG() // Injects a container with corrupt WSLC metadata into the default session's storage, - // then verifies that running the wslc.exe CLI surfaces the COM service's recovery - // warning on stderr. - WSLC_TEST_METHOD(WSLCE2E_Warning_ContainerRecoveryPrintedOnStderr) + // then verifies that running the wslc.exe CLI does not surface the COM service's recovery + // warning on stderr: recovery runs outside the user's current command, so it is logged + // (and written to the event log) rather than streamed back via IWarningCallback. + WSLC_TEST_METHOD(WSLCE2E_Warning_ContainerRecoveryNotPrintedOnStderr) { std::string corruptContainerId; @@ -98,15 +100,18 @@ class WSLCE2EWarningTests // Terminate the default session so the next wslc command recreates it and runs recovery. EnsureSessionIsTerminated(); - // Run the CLI: recovery of the corrupt container fails and the warning is printed on stderr. + // Run the CLI: recovery of the corrupt container fails, but because the recovery runs + // outside the user's current command, the warning is not printed on stderr. auto result = RunWslc(L"container list"); VERIFY_IS_TRUE(result.ExitCode.has_value()); VERIFY_ARE_EQUAL(0u, result.ExitCode.value()); - VERIFY_IS_TRUE(result.Stderr.has_value()); - const auto expectedStderr = std::format( - L"wsl: {}\r\n", wsl::shared::Localization::MessageWslcFailedToRecoverContainer(string::MultiByteToWide(corruptContainerId))); - VERIFY_ARE_EQUAL(expectedStderr, result.Stderr.value()); + const auto recoveryWarning = + wsl::shared::Localization::MessageWslcFailedToRecoverContainer(string::MultiByteToWide(corruptContainerId)); + if (result.Stderr.has_value()) + { + VERIFY_IS_TRUE(result.Stderr.value().find(recoveryWarning) == std::wstring::npos); + } } };