From 9db9e5979eab6e50e10e2abfa15d8d2c02902edf Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 23 Jun 2026 10:25:36 -0700 Subject: [PATCH 01/10] wslc: idle-terminate per-user container VMs and lazily restart Per-user WSLC container session VMs now idle-terminate when no container is in a non-terminal (Created/Running) state, freeing host memory, and lazily restart on the next operation that needs the VM. - Centralize VM lifecycle in WSLCSession via TearDownVmLockHeld / StartVmLockHeld and an atomic VmExitDisposition (Active / StopRequested / ExitClaimed) to arbitrate expected stops vs. spontaneous VM exits without a polling thread. - Gate VM-requiring entrypoints behind AcquireVmLease(), which brings the VM up on demand and keeps it alive for the operation's duration. - Add IWSLCSession::BeginContainerOperation so a CLI command can hold the VM alive across resolve + operate + streamed output. - Preserve the session WarningCallback for the lifetime of the session so warnings emitted by the lazy VM start (e.g. resource recovery) are still delivered to the CLI invocation. - Remove the dtor lock in HcsVirtualMachine; OnExit/OnCrash are lock-free. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/service/exe/HcsVirtualMachine.cpp | 4 +- .../service/exe/WSLCSessionManager.cpp | 3 +- src/windows/service/inc/wslc.idl | 8 + .../wslc/services/ContainerService.cpp | 11 + src/windows/wslc/services/SessionModel.h | 18 +- src/windows/wslc/services/SessionService.cpp | 5 +- src/windows/wslcsession/IORelay.cpp | 9 + src/windows/wslcsession/IORelay.h | 6 + src/windows/wslcsession/WSLCContainer.cpp | 66 ++ src/windows/wslcsession/WSLCContainer.h | 10 + .../wslcsession/WSLCExecutionContext.h | 16 +- src/windows/wslcsession/WSLCIdleState.h | 226 +++++ src/windows/wslcsession/WSLCProcess.h | 10 + .../wslcsession/WSLCProcessControl.cpp | 10 +- src/windows/wslcsession/WSLCSession.cpp | 779 ++++++++++++++---- src/windows/wslcsession/WSLCSession.h | 137 ++- test/windows/WSLCTests.cpp | 16 + .../wslc/e2e/WSLCE2ESessionEnterTests.cpp | 9 +- 18 files changed, 1177 insertions(+), 166 deletions(-) create mode 100644 src/windows/wslcsession/WSLCIdleState.h diff --git a/src/windows/service/exe/HcsVirtualMachine.cpp b/src/windows/service/exe/HcsVirtualMachine.cpp index 17fd76a633..5c95870009 100644 --- a/src/windows/service/exe/HcsVirtualMachine.cpp +++ b/src/windows/service/exe/HcsVirtualMachine.cpp @@ -351,7 +351,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/WSLCSessionManager.cpp b/src/windows/service/exe/WSLCSessionManager.cpp index e5e0540e98..fcd300a977 100644 --- a/src/windows/service/exe/WSLCSessionManager.cpp +++ b/src/windows/service/exe/WSLCSessionManager.cpp @@ -285,8 +285,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. diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 7639c45177..5d798f44ff 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -633,6 +633,14 @@ interface IWSLCSession : IUnknown // Container management. HRESULT CreateContainer([in] const WSLCContainerOptions* Options, [in, unique] IWarningCallback* WarningCallback, [out] IWSLCContainer** Container); HRESULT OpenContainer([in, ref] LPCSTR Id, [out] IWSLCContainer** Container); + + // 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); HRESULT ListContainers([in, unique] const WSLCListContainersOptions* Options,[out, size_is(, *Count)] WSLCContainerEntry** Containers,[out] ULONG* Count, [out, size_is(, *PortsCount)] WSLCContainerPortMapping** Ports, [out] ULONG* PortsCount); HRESULT PruneContainers([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out] WSLCPruneContainersResults* Result); diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index 9a85ebf988..3c46a25787 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -296,6 +296,7 @@ std::wstring ContainerService::FormatRelativeTime(ULONGLONG timestamp) int ContainerService::Attach(Session& session, const std::string& id) { + auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); @@ -457,6 +458,7 @@ CreateContainerResult ContainerService::Create(Session& session, const std::stri int ContainerService::Start(Session& session, const std::string& id, bool attach) { + auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); WSLCContainerStartFlags flags = attach ? WSLCContainerStartFlagsAttach : WSLCContainerStartFlagsNone; @@ -487,6 +489,7 @@ int ContainerService::Start(Session& session, const std::string& id, bool attach void ContainerService::Stop(Session& session, const std::string& id, StopContainerOptions options) { + 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); @@ -494,6 +497,7 @@ void ContainerService::Stop(Session& session, const std::string& id, StopContain void ContainerService::Kill(Session& session, const std::string& id, WSLCSignal signal) { + auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); THROW_IF_FAILED(container->Kill(signal)); @@ -501,6 +505,7 @@ void ContainerService::Kill(Session& session, const std::string& id, WSLCSignal void ContainerService::Delete(Session& session, const std::string& id, bool force) { + 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)); @@ -555,6 +560,7 @@ std::vector ContainerService::List( int ContainerService::Exec(Session& session, const std::string& id, ContainerOptions options) { + auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); @@ -586,6 +592,7 @@ int ContainerService::Exec(Session& session, const std::string& id, ContainerOpt InspectContainer ContainerService::Inspect(Session& session, const std::string& id) { + auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); wil::unique_cotaskmem_ansistring output; @@ -604,6 +611,8 @@ void ContainerService::Export(Session& session, const std::string& id, const std void ContainerService::Export(Session& session, const std::string& id, HANDLE outputHandle) { + auto operation = session.BeginContainerOperation(); + wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); @@ -615,6 +624,7 @@ void ContainerService::Export(Session& session, const std::string& id, HANDLE ou void ContainerService::Logs(Session& session, const std::string& id, bool follow, bool timestamps, ULONGLONG since, ULONGLONG until, ULONGLONG tail) { + auto operation = session.BeginContainerOperation(); wil::com_ptr container; THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); @@ -642,6 +652,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) { + 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..491df6fc3b 100644 --- a/src/windows/wslc/services/SessionModel.h +++ b/src/windows/wslc/services/SessionModel.h @@ -22,7 +22,8 @@ struct Session NON_COPYABLE(Session); DEFAULT_MOVABLE(Session); - explicit Session(wil::com_ptr session) : m_session(std::move(session)) + explicit Session(wil::com_ptr session, wil::com_ptr warningCallback = {}) : + m_session(std::move(session)), m_warningCallback(std::move(warningCallback)) { } @@ -31,8 +32,23 @@ 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; + + // Kept alive for the lifetime of the session model (i.e. the whole CLI command) so the service + // can deliver warnings emitted by lazy/background work — such as resource recovery on the first + // VM start — back to this CLI invocation, even though no single COM call carries the callback. + wil::com_ptr m_warningCallback; }; } // namespace wsl::windows::wslc::models \ No newline at end of file diff --git a/src/windows/wslc/services/SessionService.cpp b/src/windows/wslc/services/SessionService.cpp index 86ebcebbe7..ff8008a22f 100644 --- a/src/windows/wslc/services/SessionService.cpp +++ b/src/windows/wslc/services/SessionService.cpp @@ -61,7 +61,10 @@ Session SessionService::OpenOrCreateDefaultSession() 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)); + + // Hold the warning callback for the session's lifetime so warnings emitted by a lazy VM start + // (e.g. resource recovery) are still delivered to this CLI invocation. + return Session(std::move(session), wil::com_ptr(warningCallback.Get())); } int SessionService::Attach(const Session& session) 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 674b0b0cd9..c5a74906f9 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -570,6 +570,12 @@ WSLCContainerImpl::WSLCContainerImpl( m_initProcessFlags(InitProcessFlags), m_containerFlags(ContainerFlags) { + // Acquire the activity hold up front for containers recovered or created in an active state, so + // a recovered running container keeps the VM alive even before any client opens its wrapper. + if (m_state == WslcContainerStateCreated || m_state == WslcContainerStateRunning) + { + m_activityHold = ActivityRef(m_wslcSession.IdleStateShared()); + } } WSLCContainerImpl::~WSLCContainerImpl() @@ -1276,6 +1282,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()); @@ -2177,6 +2189,24 @@ __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 Created/Running and release the hold once it reaches + // a terminal state, even when no client holds the wrapper (e.g. a detached `run -d` container). + // Dropping the hold on the transition to Exited is what lets an otherwise-idle VM be torn down. + UpdateActivityHoldLockHeld(); +} + +__requires_lock_held(m_lock) void WSLCContainerImpl::UpdateActivityHoldLockHeld() noexcept +{ + const bool active = (m_state == WslcContainerStateCreated || m_state == WslcContainerStateRunning); + if (active && !m_activityHold) + { + m_activityHold = ActivityRef(m_wslcSession.IdleStateShared()); + } + else if (!active && m_activityHold) + { + m_activityHold.reset(); + } } WSLCContainer::WSLCContainer(WSLCContainerImpl* impl, WSLCSession& session, std::function&& OnDeleted) : @@ -2185,6 +2215,7 @@ WSLCContainer::WSLCContainer(WSLCContainerImpl* impl, WSLCSession& session, std: } HRESULT WSLCContainer::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* Stdout, WSLCHandle* Stderr) +try { WSLCExecutionContext context(&m_session); @@ -2196,8 +2227,10 @@ HRESULT WSLCContainer::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* *Stdout = {}; *Stderr = {}; + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Attach, DetachKeys, Stdin, Stdout, Stderr); } +CATCH_RETURN(); HRESULT WSLCContainer::GetState(WSLCContainerState* Result) { @@ -2255,6 +2288,7 @@ HRESULT WSLCContainer::GetInitProcess(IWSLCProcess** Process) } HRESULT WSLCContainer::Exec(const WSLCProcessOptions* Options, const WSLCProcessStartOptions* StartOptions, IWSLCProcess** Process) +try { WSLCExecutionContext context(&m_session); @@ -2263,22 +2297,35 @@ HRESULT WSLCContainer::Exec(const WSLCProcessOptions* Options, const WSLCProcess RETURN_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Options->Flags, ~WSLCProcessFlagsValid), "Invalid flags: 0x%x", Options->Flags); *Process = nullptr; + + auto vmLease = m_session.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.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.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Stop, Signal, {}, true); } +CATCH_RETURN(); HRESULT WSLCContainer::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, IWarningCallback* WarningCallback) try @@ -2287,11 +2334,13 @@ try THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCContainerStartFlagsValid), "Invalid flags: 0x%x", Flags); + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Start, Flags, StartOptions); } CATCH_RETURN(); HRESULT WSLCContainer::Inspect(LPSTR* Output) +try { WSLCExecutionContext context(&m_session); @@ -2299,8 +2348,10 @@ HRESULT WSLCContainer::Inspect(LPSTR* Output) *Output = nullptr; + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Inspect, Output); } +CATCH_RETURN(); HRESULT WSLCContainer::Stats(LPSTR* Output) try @@ -2310,6 +2361,8 @@ try RETURN_HR_IF(E_POINTER, Output == nullptr); *Output = nullptr; + + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Stats, Output); } CATCH_RETURN(); @@ -2322,6 +2375,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.AcquireVmLease(); auto [lock, impl] = LockImpl(); impl->Delete(Flags); @@ -2347,11 +2405,14 @@ try CATCH_LOG(); HRESULT WSLCContainer::Export(WSLCHandle TarHandle) +try { WSLCExecutionContext context(&m_session); + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Export, TarHandle); } +CATCH_RETURN(); HRESULT WSLCContainer::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) try @@ -2364,6 +2425,7 @@ try *Stdout = {}; *Stderr = {}; + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Logs, Flags, Stdout, Stderr, Since, Until, Tail); } CATCH_RETURN(); @@ -2541,6 +2603,8 @@ HRESULT WSLCContainer::ConnectToNetwork(const WSLCNetworkConnectionOptions* Opti try { COMServiceExecutionContext context; + + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::ConnectToNetwork, Options); } CATCH_RETURN(); @@ -2549,6 +2613,8 @@ HRESULT WSLCContainer::DisconnectFromNetwork(LPCSTR NetworkName) try { COMServiceExecutionContext context; + + auto vmLease = m_session.AcquireVmLease(); return CallImpl(&WSLCContainerImpl::DisconnectFromNetwork, NetworkName); } CATCH_RETURN(); diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index 10fd57fd37..ef797933cf 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" @@ -176,6 +177,10 @@ class WSLCContainerImpl void MapPorts(); void UnmapPorts(); + // Acquires or releases the activity hold so it is held exactly while the container is in an + // active (Created/Running) state, 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; mutable wil::srwlock m_lock; @@ -220,6 +225,11 @@ class WSLCContainerImpl DockerEventTracker::EventTrackingReference m_containerEvents; IORelay& m_ioRelay; std::string m_networkMode; + + // Held (non-empty) exactly while the container is Created/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..b3abec5a0e 100644 --- a/src/windows/wslcsession/WSLCExecutionContext.h +++ b/src/windows/wslcsession/WSLCExecutionContext.h @@ -27,7 +27,19 @@ class WSLCExecutionContext : public wsl::windows::common::COMServiceExecutionCon protected: bool CollectUserWarning(const std::wstring& warning) override { - if (m_warningCallback != nullptr) + IWarningCallback* callback = m_warningCallback; + + // When the operation carries no explicit callback, fall back to the callback supplied when + // the session was created/entered. This routes warnings emitted outside a callback-bearing + // operation (e.g. resource recovery during the lazy VM start) back to the session creator. + wil::com_ptr sessionCallback; + if (callback == nullptr && m_session != nullptr) + { + sessionCallback = m_session->AcquireWarningCallback(); + callback = sessionCallback.get(); + } + + if (callback != nullptr) { std::unique_ptr comCallback; if (m_session != nullptr) @@ -35,7 +47,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 ea3b246d9a..6ce9da1675 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -43,8 +43,41 @@ constexpr auto c_storageVhdFilename = wsl::windows::wslc::DefaultStorageVhdName; constexpr DWORD c_processTerminateTimeoutMs = 30 * 1000; constexpr DWORD c_processKillTimeoutMs = 10 * 1000; +// Grace period to keep an otherwise-idle VM running before tearing it down. 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 +365,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 +376,32 @@ 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()); + + 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), + !std::filesystem::exists(storagePath / c_storageVhdFilename)); + } + else if (!std::filesystem::exists(storagePath / c_storageVhdFilename)) + { + // 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 +409,24 @@ 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)); + + // Park the warning callback too. The VM (and resource recovery) is created lazily on the + // first operation, which may not carry its own warning callback, so recovery warnings are + // routed back to this callback via AcquireWarningCallback()/WSLCExecutionContext. + if (WarningCallback != nullptr) + { + THROW_IF_FAILED(m_git->RegisterInterfaceInGlobal(WarningCallback, __uuidof(IWarningCallback), &m_warningCallbackGitCookie)); + } + + // 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,61 +434,430 @@ 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. + // The VM is created lazily on the first operation that requires it (see EnsureVmRunning) and + // torn down once the session has been continuously idle (activity count zero) for the grace + // period. Wire up the idle-teardown timer; IdleState arms it whenever the activity count drops + // to zero and cancels it when activity resumes, so no dedicated worker thread is needed. + m_idleState->Initialize(c_vmIdleGracePeriod, [this]() { OnIdleTimer(); }); + + 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; + } + + if (UserSid != nullptr) + { + const auto length = GetLengthSid(UserSid); + const auto* bytes = reinterpret_cast(UserSid); + m_userSid.assign(bytes, bytes + length); + } + else + { + m_userSid.clear(); + } +} + +bool WSLCSession::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; +} + +void WSLCSession::EnsureVmRunning() +{ + if (m_vmState.load() == VmState::Running) + { + return; + } + + auto lock = m_lock.lock_exclusive(); + + // Do not (re)start the VM once the session is terminating or has terminated. 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) + { + return; + } + + StartVmLockHeld(); +} + +bool WSLCSession::TryClaimExpectedStop() noexcept +{ + auto expected = VmExitDisposition::Active; + return m_vmExitDisposition.compare_exchange_strong(expected, VmExitDisposition::StopRequested); +} + +bool WSLCSession::TryClaimSpontaneousExit() noexcept +{ + auto expected = VmExitDisposition::Active; + return m_vmExitDisposition.compare_exchange_strong(expected, VmExitDisposition::ExitClaimed); +} + +void WSLCSession::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)); + THROW_IF_FAILED(vmFactory->CreateVirtualMachine(&vm)); m_virtualMachine.emplace( vm.get(), - Settings, + &m_settings, m_sessionTerminatingEvent.get(), std::bind(&WSLCSession::OnCrashDumpWritten, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); - - // 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()); }); - 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)); // Configure storage. - ConfigureStorage(*Settings, tokenInfo->User.Sid); + ConfigureStorage(m_settings, m_userSid.empty() ? nullptr : reinterpret_cast(m_userSid.data())); // Mirror the host's trusted root CAs into the VM before dockerd starts. InstallTrustedRootCertificates(); - // Launch containerd first + // Launch containerd first, then dockerd with the external containerd socket. StartContainerd(); - // Launch dockerd with external containerd socket + // Reset the readiness event before (re)starting dockerd so a stale signal from a prior + // VM instance is not observed. + m_dockerdReadyEvent.ResetEvent(); StartDockerd(); // 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"); + ERROR_TIMEOUT, !m_dockerdReadyEvent.wait(m_settings.BootTimeoutMs), "Timed out waiting for dockerd to start"); auto [_, __, channel] = m_virtualMachine->Fork(WSLC_FORK::Thread); m_dockerClient.emplace(std::move(channel), m_virtualMachine->TerminatingEvent(), m_virtualMachine->VmId(), 10 * 1000); // Start the event tracker. - m_eventTracker.emplace(m_dockerClient.value(), *this, m_ioRelay); + m_eventTracker.emplace(m_dockerClient.value(), *this, *m_ioRelay); m_volumes.emplace(m_dockerClient.value(), m_virtualMachine.value(), m_eventTracker.value(), m_storageVhdPath.parent_path()); // Monitor for unexpected VM exit. - m_ioRelay.AddHandle(std::make_unique(m_vmExitedEvent.get(), std::bind(&WSLCSession::OnVmExited, this))); + m_ioRelay->AddHandle(std::make_unique(m_vmExitedEvent.get(), std::bind(&WSLCSession::OnVmExited, this))); // Recover any existing resources from storage. RecoverExistingNetworks(); RecoverExistingContainers(); - errorCleanup.release(); - return S_OK; + m_vmState.store(VmState::Running); + startCleanup.release(); + + WSL_LOG("WslcVmStarted", TraceLoggingValue(m_id, "SessionId")); +} + +void WSLCSession::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 WSLCSession::TearDownVmLockHeld(bool CaptureTerminationReason) +{ + 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 + if (m_ioRelay) + { + m_ioRelay->Stop(); + } + + { + std::lock_guard allocatedPortsLock(m_allocatedPortsLock); + m_allocatedPorts.clear(); + } + + m_eventTracker.reset(); + 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_terminationReason = WSLCVirtualMachineTerminationReasonShutdown; + } + + // 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_terminationReason, &details)); + m_terminationDetails = 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. + try + { + m_virtualMachine->Unmount(c_containerdStorage); + } + CATCH_LOG(); + } + + m_dockerdProcess.reset(); + m_containerdProcess.reset(); + m_virtualMachine.reset(); + + // 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 WSLCSession::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); + }); + + StopVmLockHeld(); +} +CATCH_LOG(); + +WSLCSession::VmLease WSLCSession::AcquireVmLease() +{ + return VmLease(*this); +} + +WSLCSession::VmLease::VmLease(WSLCSession& Session) : m_session(&Session) +{ + // 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_session->m_idleState->AddActivity(); + + auto countCleanup = wil::scope_exit([this]() { + m_session->m_idleState->ReleaseActivity(); + m_session = nullptr; + }); + + // Activity increment may race with idle teardown. Retry until we hold the lock with VM running. + for (;;) + { + m_session->EnsureVmRunning(); + + m_lock = m_session->m_lock.lock_shared(); + + if (m_session->m_vmState.load() == VmState::Running) + { + break; + } + + m_lock.reset(); + } + + countCleanup.release(); +} + +WSLCSession::VmLease::VmLease(VmLease&& Other) noexcept : + m_session(std::exchange(Other.m_session, nullptr)), m_lock(std::move(Other.m_lock)) +{ +} + +WSLCSession::VmLease& WSLCSession::VmLease::operator=(VmLease&& Other) noexcept +{ + if (this != &Other) + { + if (m_session != 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_session->m_idleState->ReleaseActivity(); + } + + m_session = std::exchange(Other.m_session, nullptr); + m_lock = std::move(Other.m_lock); + } + + return *this; +} + +WSLCSession::VmLease::~VmLease() +{ + if (m_session != 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_session->m_idleState->ReleaseActivity(); + } } -CATCH_RETURN() WSLCSession::~WSLCSession() { @@ -484,23 +925,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")); @@ -573,7 +998,7 @@ CATCH_RETURN(); void WSLCSession::OnDockerdExited() { - if (!m_sessionTerminatingEvent.is_signaled()) + if (!m_sessionTerminatingEvent.is_signaled() && m_vmExitDisposition.load() != VmExitDisposition::StopRequested) { WSL_LOG("UnexpectedDockerdExit", TraceLoggingValue(m_displayName.c_str(), "Name")); } @@ -581,7 +1006,7 @@ void WSLCSession::OnDockerdExited() void WSLCSession::OnContainerdExited() { - if (!m_sessionTerminatingEvent.is_signaled()) + if (!m_sessionTerminatingEvent.is_signaled() && m_vmExitDisposition.load() != VmExitDisposition::StopRequested) { WSL_LOG("UnexpectedContainerdExit", TraceLoggingValue(m_displayName.c_str(), "Name")); } @@ -589,6 +1014,14 @@ void WSLCSession::OnContainerdExited() void WSLCSession::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), @@ -631,13 +1064,13 @@ ServiceRunningProcess WSLCSession::StartProcess( auto process = launcher.Launch(*m_virtualMachine); - m_ioRelay.AddHandle(std::make_unique( + m_ioRelay->AddHandle(std::make_unique( process.GetStdHandle(1), [this, LogSource](const auto& data) { OnProcessLog(data, LogSource); }, false)); - m_ioRelay.AddHandle(std::make_unique( + m_ioRelay->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_ioRelay->AddHandle(std::make_unique(process.GetExitEvent(), std::move(ExitCallback))); return process; } @@ -845,7 +1278,7 @@ try auto [repo, tagOrDigest] = wslutil::ParseImage(Image); EnforceRegistryAllowlist(repo); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); if (!tagOrDigest.has_value()) @@ -903,7 +1336,7 @@ try comCall = RegisterUserCOMCallback(); } - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); @@ -1232,7 +1665,7 @@ 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()); @@ -1265,7 +1698,7 @@ 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()); @@ -1436,7 +1869,7 @@ 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()); @@ -1469,7 +1902,7 @@ 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()); @@ -1544,7 +1977,7 @@ 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()); @@ -1653,7 +2086,7 @@ 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()); @@ -1727,7 +2160,7 @@ 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()); @@ -1764,7 +2197,7 @@ try auto [repo, tagOrDigest] = wslutil::ParseImage(Image); EnforceRegistryAllowlist(repo); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); auto requestContext = m_dockerClient->PushImage(repo, tagOrDigest, RegistryAuthenticationInformation); @@ -1785,7 +2218,7 @@ try *Output = nullptr; - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); *Output = wil::make_unique_ansistring(InspectImageLockHeld(ImageNameOrId).c_str()).release(); @@ -1833,7 +2266,7 @@ try *IdentityToken = nullptr; - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); wil::unique_cotaskmem_ansistring token; @@ -1865,7 +2298,7 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); docker_schema::PruneImageResult pruneResult; @@ -1926,7 +2359,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); }); @@ -2004,7 +2437,7 @@ void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptio std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), m_eventTracker.value(), m_dockerClient.value(), - m_ioRelay); + *m_ioRelay); // 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 +2470,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). @@ -2076,6 +2509,73 @@ 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_idleState->AddActivity(); + auto countCleanup = wil::scope_exit([this]() { m_idleState->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_idleState; + 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; + + // 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,7 +2610,7 @@ try filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Options->Filters, Options->FiltersCount); } - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); std::vector dockerContainers; @@ -2189,7 +2689,7 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); std::lock_guard containersLock{m_containersLock}; @@ -2260,10 +2760,18 @@ try *Errno = -1; // Make sure not to return 0 if something fails. } - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); auto process = m_virtualMachine->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()); + THROW_IF_FAILED(process.CopyTo(Process)); return S_OK; @@ -2286,7 +2794,7 @@ try THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Path), !std::filesystem::path(Path).is_absolute()); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); // Attach the disk to the VM (AttachDisk() performs the access check for the VHD file). @@ -2314,7 +2822,7 @@ try auto driverOpts = wslutil::ParseKeyValuePairs(Options->DriverOpts, Options->DriverOptsCount); auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCVolumeMetadataLabel); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); if (Options->Name != nullptr && Options->Name[0] != '\0') @@ -2334,7 +2842,7 @@ try RETURN_HR_IF_NULL(E_POINTER, Name); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); m_volumes->DeleteVolume(Name); @@ -2355,7 +2863,7 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); auto volumeList = m_volumes->ListVolumes(std::move(filters)); @@ -2387,7 +2895,7 @@ try std::string name = Name; ValidateName(name.c_str(), WSLC_MAX_VOLUME_NAME_LENGTH); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); std::string json = m_volumes->InspectVolume(name); @@ -2412,7 +2920,7 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); WSLCVolumes::PruneVolumesResult pruneResult; @@ -2491,7 +2999,7 @@ try auto driverOpts = wslutil::ParseKeyValuePairs(Options->DriverOpts, Options->DriverOptsCount); auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCNetworkManagedLabel); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); @@ -2600,7 +3108,7 @@ try std::string name = Name; ValidateName(name.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); @@ -2640,7 +3148,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 +3187,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,7 +3242,7 @@ try // Scope the prune to WSLC-managed networks. filters["label"].push_back(WSLCNetworkManagedLabel); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); @@ -2867,97 +3375,56 @@ try // 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); + // Tear down the VM (if running) and all VM-scoped state, capturing the termination reason. + // This mirrors the soft teardown used for idle shutdown, but here it is permanent. + TearDownVmLockHeld(/* CaptureTerminationReason */ true); - m_containers.clear(); - m_volumes.reset(); - m_networks.clear(); + m_vmState.store(VmState::None); - // Stop the IO relay. - // This stops: - // - container state monitoring. - // - container init process relays - // - execs relays - // - container logs relays - m_ioRelay.Stop(); + // Signal completion last so any observer of the terminated event sees a fully torn-down + // session and a populated termination reason. + m_sessionTerminatedEvent.SetEvent(); - { - std::lock_guard allocatedPortsLock(m_allocatedPortsLock); - m_allocatedPorts.clear(); - } + // 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. + sessionLock.reset(); - m_eventTracker.reset(); - m_dockerClient.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(); - // 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()) + // 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) { - 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""; - } + LOG_IF_FAILED(m_git->RevokeInterfaceFromGlobal(m_vmFactoryGitCookie)); + m_vmFactoryGitCookie = 0; } - 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(); - } - } + if (m_warningCallbackGitCookie != 0) + { + LOG_IF_FAILED(m_git->RevokeInterfaceFromGlobal(m_warningCallbackGitCookie)); + m_warningCallbackGitCookie = 0; } - m_dockerdProcess.reset(); - m_containerdProcess.reset(); - m_virtualMachine.reset(); + return S_OK; +} +CATCH_RETURN(); - // Delete the ephemeral swap VHD now that the VM is gone. - if (!m_swapVhdPath.empty()) +wil::com_ptr WSLCSession::AcquireWarningCallback() const +{ + wil::com_ptr callback; + if (m_warningCallbackGitCookie != 0) { - LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_swapVhdPath.c_str())); - m_swapVhdPath.clear(); + // Best-effort: the creating client's proxy may already be gone (e.g. the CLI exited before + // a later VM restart), in which case the warning falls through to the default sink. + LOG_IF_FAILED(m_git->GetInterfaceFromGlobal(m_warningCallbackGitCookie, __uuidof(IWarningCallback), callback.put_void())); } - m_sessionTerminatedEvent.SetEvent(); - - return S_OK; + return callback; } -CATCH_RETURN(); HRESULT WSLCSession::RegisterCrashDumpCallback(_In_ ICrashDumpCallback* Callback, _Out_ IUnknown** Subscription) try @@ -3022,7 +3489,7 @@ try RETURN_HR_IF_NULL(E_POINTER, WindowsPath); RETURN_HR_IF_NULL(E_POINTER, LinuxPath); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); return m_virtualMachine->MountWindowsFolder(WindowsPath, LinuxPath, ReadOnly); @@ -3036,7 +3503,7 @@ try RETURN_HR_IF_NULL(E_POINTER, LinuxPath); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); return m_virtualMachine->UnmountWindowsFolder(LinuxPath); @@ -3048,7 +3515,7 @@ try { WSLCExecutionContext context(this); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); std::lock_guard allocatedPortsLock(m_allocatedPortsLock); @@ -3094,7 +3561,7 @@ try { WSLCExecutionContext context(this); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); std::lock_guard allocatedPortsLock(m_allocatedPortsLock); @@ -3408,7 +3875,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); WI_VERIFY(m_containers.erase(Container->ID()) == 1); @@ -3476,7 +3947,7 @@ void WSLCSession::RecoverExistingContainers() std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), m_eventTracker.value(), m_dockerClient.value(), - m_ioRelay); + *m_ioRelay); auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container)); WI_ASSERT(inserted); diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index aa86d47cb5..a6b0c4d83e 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -18,12 +18,15 @@ Module Name: #include "WSLCCompat.h" #include "WSLCVirtualMachine.h" #include "WSLCContainer.h" +#include "WSLCIdleState.h" #include "WSLCVolumes.h" #include "WSLCNetworkMetadata.h" #include "DockerEventTracker.h" #include "DockerHTTPClient.h" #include "IORelay.h" +#include #include +#include #include namespace wsl::windows::service::wslc { @@ -71,11 +74,16 @@ 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; @@ -143,6 +151,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, @@ -247,6 +256,13 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession UserCOMCallback RegisterUserCOMCallback(); void UnregisterUserCOMCallback(DWORD ThreadId); + // Returns the warning callback supplied when the session was created/entered, re-marshalled + // into the calling apartment. Used as a fallback by WSLCExecutionContext so that warnings + // emitted by operations that carry no explicit callback (e.g. resource recovery during the + // lazy VM start) still reach the session creator. Returns null if no callback was supplied + // or the creating client's proxy is no longer reachable. + wil::com_ptr AcquireWarningCallback() const; + HANDLE SessionTerminatingEvent() const noexcept { return m_sessionTerminatingEvent.get(); @@ -259,9 +275,94 @@ 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_idleState; + } + + // 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; + // VM lifecycle state for on-demand creation / idle termination. + enum class VmState + { + None, + Starting, + Running, + Stopping, + }; + + // Single-owner arbitration for a VM exit: exactly one side tears a given VM instance down, + // resolved atomically. An expected stop (idle teardown or bring-up cleanup, on a lock-holding + // thread) and a spontaneous exit (OnVmExited, lock-free on the relay thread) each try to claim + // it; the loser declines. This avoids both a missed teardown and a deadlock (a lock-holder + // joining the relay thread while OnVmExited spins for the lock in Terminate()). A fresh VM + // starts Active. Claim via TryClaim*(), not by touching the atomic directly. + enum class VmExitDisposition + { + Active, // Running normally; a VM exit is unexpected and triggers a permanent Terminate(). + StopRequested, // An expected (soft) stop is in progress; OnVmExited treats the exit as expected. + ExitClaimed, // OnVmExited owns the permanent teardown of a spontaneous exit. + }; + + // Claims an expected (soft) stop of the current VM from a lock-holding thread. On success a + // racing OnVmExited() declines, so the caller may tear down (joining the relay thread is safe). + // On failure OnVmExited() already owns a spontaneous-exit teardown and is spinning for the lock + // in Terminate(); the caller must not tear down or it deadlocks joining the relay. + [[nodiscard]] bool TryClaimExpectedStop() noexcept; + + // Claims the teardown of a spontaneous VM exit from OnVmExited(). Fails if an expected stop is + // already in progress, in which case the exit was wanted and the caller declines. + [[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(); + + // Idle-teardown callback invoked by IdleState's timer once the VM has been continuously idle + // (activity count zero) for the grace period. Runs on a threadpool thread. + void OnIdleTimer(); + bool IdleTerminationEnabled() const noexcept; + void PersistSettings(const WSLCSessionInitSettings& Settings, PSID UserSid); + + // RAII lease taken at the top of every VM-requiring operation. On construction it + // ensures the VM is running (lazily restarting it if it was idle-terminated) and records + // an in-flight operation so idle teardown is deferred; it then holds the shared session + // lock for the operation's duration. On destruction it releases the lock and triggers an + // idle check. + class VmLease + { + public: + VmLease() = default; + explicit VmLease(WSLCSession& Session); + VmLease(VmLease&& Other) noexcept; + VmLease& operator=(VmLease&& Other) noexcept; + ~VmLease(); + + VmLease(const VmLease&) = delete; + VmLease& operator=(const VmLease&) = delete; + + private: + WSLCSession* m_session{}; + wil::rwlock_release_shared_scope_exit m_lock; + }; + + [[nodiscard]] VmLease AcquireVmLease(); + __requires_lock_held(m_userHandlesLock) void CancelUserHandleIO(); __requires_lock_held(m_userCOMCallbacksLock) void CancelUserCOMCallbacks(); @@ -301,6 +402,19 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession void StreamImageOperation(DockerHTTPClient::HTTPRequestContext& requestContext, LPCSTR Image, LPCSTR OperationName, IProgressCallback* ProgressCallback); std::optional m_dockerClient; + + // 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{}; + + // The warning callback supplied at Initialize() is parked in the GIT for the same reason as + // the VM factory: it is used later, on demand, from other threads/apartments (a directly + // stored proxy would fail with RPC_E_WRONG_THREAD). Zero if no callback was supplied. + DWORD m_warningCallbackGitCookie{}; + std::optional m_virtualMachine; std::optional m_eventTracker; wil::unique_event m_dockerdReadyEvent{wil::EventOptions::ManualReset}; @@ -326,7 +440,26 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession WSLCVirtualMachineTerminationReason m_terminationReason{WSLCVirtualMachineTerminationReasonUnknown}; std::wstring m_terminationDetails; wil::srwlock m_lock; - IORelay m_ioRelay; + std::optional m_ioRelay; + + // VM lifecycle / idle-termination state. + std::atomic m_vmState{VmState::None}; + std::atomic m_vmExitDisposition{VmExitDisposition::Active}; + // In-flight activity count, idle timer and teardown callback, decoupled from this object's + // lifetime (see IdleState in WSLCIdleState.h) so activity tokens and container COM wrappers can + // safely manage activity without keeping the session alive. See WSLCContainerImpl's ActivityRef + // (m_activityHold), WSLCSession::VmLease and CreateActivityToken(). + std::shared_ptr m_idleState{std::make_shared()}; + + // 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; + std::optional m_containerdProcess; std::optional m_dockerdProcess; WSLCFeatureFlags m_featureFlags{}; diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 94fb0ed261..0a36406554 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -11396,6 +11396,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()); @@ -11487,6 +11491,10 @@ class WSLCTests VERIFY_SUCCEEDED(sessionManager2->CreateSession(&settings2, WSLCSessionFlagsNone, warningCallback.Get(), &session2)); wsl::windows::common::security::ConfigureForCOMImpersonation(session2.get()); + // The VM (and container recovery) starts lazily on the first operation. Trigger it so + // recovery runs and its warning is delivered to the session's warning callback. + VERIFY_IS_TRUE(WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "exit 0"}).Launch(*session2).GetExitEvent().wait(30000)); + // Verify the warning matches the expected localized message for the corrupt container. auto warnings = warningCallback->GetWarnings(); auto expectedWarning = std::format( @@ -11555,6 +11563,10 @@ class WSLCTests VERIFY_SUCCEEDED(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, warningCallback.Get(), &session)); wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); + // The VM (and volume recovery) starts lazily on the first operation. Trigger it so + // recovery runs and its warning is delivered to the session's warning callback. + VERIFY_IS_TRUE(WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "exit 0"}).Launch(*session).GetExitEvent().wait(30000)); + // Verify the warning matches the expected localized message for the missing volume. auto warnings = warningCallback->GetWarnings(); auto expectedWarning = @@ -11620,6 +11632,10 @@ 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 so + // recovery runs and its warning is delivered to the session's warning callback. + VERIFY_IS_TRUE(WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "exit 0"}).Launch(*session).GetExitEvent().wait(30000)); + auto warnings = warningCallback->GetWarnings(); auto expectedWarning = std::format(L"wsl: {}\n", wsl::shared::Localization::MessageWslcFailedToRecoverVolume(c_volumeName)); 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, }); } From a708a019aae2dd9972e09a27b9ea153bad467246 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Fri, 26 Jun 2026 13:09:44 -0700 Subject: [PATCH 02/10] wslc: don't pin the session VM for merely-created containers Only Running containers now hold an activity reference that keeps the per-user session VM alive. Previously a container in either Created or Running state held the reference, so a `create`d-but-never-started container pinned the VM indefinitely and defeated idle termination. A created container's metadata persists on the containerd VHD across VM teardown and is rebuilt by RecoverExistingContainers on the next VM-requiring operation, so create -> idle-terminate -> start later works; the 30s grace period covers the common create-then-start gap. Also fix m_stateChangedAt recovery for created containers: docker inspect reports FinishedAt as the zero date ("0001-01-01T00:00:00Z") for a never-started container, which parsed to year 1 and rendered as "created 2026 years ago". Use the container's Created time for the Created state. This recovery path was previously unreachable, since created containers never got torn down. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslcsession/WSLCContainer.cpp | 32 ++++++++++++++++------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index c5a74906f9..14c259b00c 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -570,9 +570,10 @@ WSLCContainerImpl::WSLCContainerImpl( m_initProcessFlags(InitProcessFlags), m_containerFlags(ContainerFlags) { - // Acquire the activity hold up front for containers recovered or created in an active state, so - // a recovered running container keeps the VM alive even before any client opens its wrapper. - if (m_state == WslcContainerStateCreated || m_state == WslcContainerStateRunning) + // 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.IdleStateShared()); } @@ -1908,11 +1909,21 @@ std::unique_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) + { + // 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 { - container->m_stateChangedAt = ParseDockerTimestamp(timestamp); + const auto& timestamp = (state == WslcContainerStateRunning) ? inspectData.State.StartedAt : inspectData.State.FinishedAt; + + if (!timestamp.empty()) + { + container->m_stateChangedAt = ParseDockerTimestamp(timestamp); + } } } catch (...) @@ -2190,15 +2201,16 @@ __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 Created/Running and release the hold once it reaches - // a terminal state, even when no client holds the wrapper (e.g. a detached `run -d` container). - // Dropping the hold on the transition to Exited is what lets an otherwise-idle VM be torn down. + // 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 == WslcContainerStateCreated || m_state == WslcContainerStateRunning); + const bool active = (m_state == WslcContainerStateRunning); if (active && !m_activityHold) { m_activityHold = ActivityRef(m_wslcSession.IdleStateShared()); From aef3497b350a3913dfc18fbf46b650f73386880d Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Fri, 26 Jun 2026 14:10:56 -0700 Subject: [PATCH 03/10] wslc: fix stale Created/Running comments on the activity hold Addresses review feedback: WSLCContainer.h still described the activity hold as held while Created/Running, but it now only pins the VM while Running. Update the two header comments to match the implementation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslcsession/WSLCContainer.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index ef797933cf..5032dd6de4 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -177,8 +177,8 @@ class WSLCContainerImpl void MapPorts(); void UnmapPorts(); - // Acquires or releases the activity hold so it is held exactly while the container is in an - // active (Created/Running) state, keeping the session's VM alive across idle teardown. + // 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; @@ -226,9 +226,9 @@ class WSLCContainerImpl IORelay& m_ioRelay; std::string m_networkMode; - // Held (non-empty) exactly while the container is Created/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. + // 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; }; From b74e68e865128cd85a74acbfc448acb98ea9581c Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 29 Jun 2026 17:32:29 -0700 Subject: [PATCH 04/10] wslc: address idle-termination review feedback - Make the VM idle grace period configurable via settings.yaml (session.idleTimeout, default 30s) instead of a hardcoded constant. - Assert UserSid is non-null in PersistSettings rather than tolerating a null SID. - Drop the session warning-callback GIT fallback; warnings emitted outside a callback-bearing operation are logged and event-logged only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/common/WSLCUserSettings.cpp | 8 +++ src/windows/common/WSLCUserSettings.h | 2 + .../service/exe/WSLCSessionManager.cpp | 2 + src/windows/service/inc/wslc.idl | 2 + .../wslcsession/WSLCExecutionContext.h | 11 ---- src/windows/wslcsession/WSLCSession.cpp | 54 +++++-------------- src/windows/wslcsession/WSLCSession.h | 12 ----- 7 files changed, 26 insertions(+), 65 deletions(-) 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/service/exe/WSLCSessionManager.cpp b/src/windows/service/exe/WSLCSessionManager.cpp index fcd300a977..1e0a9d06c7 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. @@ -472,6 +473,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/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 5d798f44ff..181f2b4418 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -461,6 +461,7 @@ typedef struct _WSLCSessionSettings { WSLCFeatureFlags FeatureFlags; WSLCHandle DmesgOutput; WSLCSessionStorageFlags StorageFlags; + ULONG IdleTimeoutSec; // Below options are used for debugging purposes only. [unique] LPCWSTR RootVhdOverride; @@ -595,6 +596,7 @@ typedef struct _WSLCSessionInitSettings WSLCNetworkingMode NetworkingMode; WSLCFeatureFlags FeatureFlags; [unique] LPCSTR RootVhdTypeOverride; + ULONG IdleTimeoutSec; } WSLCSessionInitSettings; [ diff --git a/src/windows/wslcsession/WSLCExecutionContext.h b/src/windows/wslcsession/WSLCExecutionContext.h index b3abec5a0e..62b21ed8c6 100644 --- a/src/windows/wslcsession/WSLCExecutionContext.h +++ b/src/windows/wslcsession/WSLCExecutionContext.h @@ -28,17 +28,6 @@ class WSLCExecutionContext : public wsl::windows::common::COMServiceExecutionCon bool CollectUserWarning(const std::wstring& warning) override { IWarningCallback* callback = m_warningCallback; - - // When the operation carries no explicit callback, fall back to the callback supplied when - // the session was created/entered. This routes warnings emitted outside a callback-bearing - // operation (e.g. resource recovery during the lazy VM start) back to the session creator. - wil::com_ptr sessionCallback; - if (callback == nullptr && m_session != nullptr) - { - sessionCallback = m_session->AcquireWarningCallback(); - callback = sessionCallback.get(); - } - if (callback != nullptr) { std::unique_ptr comCallback; diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 6ce9da1675..c9cd93e214 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -43,10 +43,11 @@ constexpr auto c_storageVhdFilename = wsl::windows::wslc::DefaultStorageVhdName; constexpr DWORD c_processTerminateTimeoutMs = 30 * 1000; constexpr DWORD c_processKillTimeoutMs = 10 * 1000; -// Grace period to keep an otherwise-idle VM running before tearing it down. 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. +// Default grace period to keep an otherwise-idle VM running before tearing it down (used when the +// session's settings.yaml does not override it). 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 { @@ -415,14 +416,6 @@ try m_git = wil::CoCreateInstance(CLSID_StdGlobalInterfaceTable, CLSCTX_INPROC_SERVER); THROW_IF_FAILED(m_git->RegisterInterfaceInGlobal(VmFactory, __uuidof(IWSLCVirtualMachineFactory), &m_vmFactoryGitCookie)); - // Park the warning callback too. The VM (and resource recovery) is created lazily on the - // first operation, which may not carry its own warning callback, so recovery warnings are - // routed back to this callback via AcquireWarningCallback()/WSLCExecutionContext. - if (WarningCallback != nullptr) - { - THROW_IF_FAILED(m_git->RegisterInterfaceInGlobal(WarningCallback, __uuidof(IWarningCallback), &m_warningCallbackGitCookie)); - } - // 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()); @@ -438,7 +431,8 @@ try // torn down once the session has been continuously idle (activity count zero) for the grace // period. Wire up the idle-teardown timer; IdleState arms it whenever the activity count drops // to zero and cancels it when activity resumes, so no dedicated worker thread is needed. - m_idleState->Initialize(c_vmIdleGracePeriod, [this]() { OnIdleTimer(); }); + const auto idleGracePeriod = m_settings.IdleTimeoutSec > 0 ? std::chrono::seconds(m_settings.IdleTimeoutSec) : c_vmIdleGracePeriod; + m_idleState->Initialize(idleGracePeriod, [this]() { OnIdleTimer(); }); return S_OK; } @@ -481,16 +475,11 @@ void WSLCSession::PersistSettings(const WSLCSessionInitSettings& Settings, PSID m_settings.RootVhdTypeOverride = nullptr; } - if (UserSid != nullptr) - { - const auto length = GetLengthSid(UserSid); - const auto* bytes = reinterpret_cast(UserSid); - m_userSid.assign(bytes, bytes + length); - } - else - { - m_userSid.clear(); - } + THROW_HR_IF(E_UNEXPECTED, UserSid == nullptr); + + const auto length = GetLengthSid(UserSid); + const auto* bytes = reinterpret_cast(UserSid); + m_userSid.assign(bytes, bytes + length); } bool WSLCSession::IdleTerminationEnabled() const noexcept @@ -3403,29 +3392,10 @@ try m_vmFactoryGitCookie = 0; } - if (m_warningCallbackGitCookie != 0) - { - LOG_IF_FAILED(m_git->RevokeInterfaceFromGlobal(m_warningCallbackGitCookie)); - m_warningCallbackGitCookie = 0; - } - return S_OK; } CATCH_RETURN(); -wil::com_ptr WSLCSession::AcquireWarningCallback() const -{ - wil::com_ptr callback; - if (m_warningCallbackGitCookie != 0) - { - // Best-effort: the creating client's proxy may already be gone (e.g. the CLI exited before - // a later VM restart), in which case the warning falls through to the default sink. - LOG_IF_FAILED(m_git->GetInterfaceFromGlobal(m_warningCallbackGitCookie, __uuidof(IWarningCallback), callback.put_void())); - } - - return callback; -} - HRESULT WSLCSession::RegisterCrashDumpCallback(_In_ ICrashDumpCallback* Callback, _Out_ IUnknown** Subscription) try { diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index a6b0c4d83e..8eff3eb4a3 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -256,13 +256,6 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession UserCOMCallback RegisterUserCOMCallback(); void UnregisterUserCOMCallback(DWORD ThreadId); - // Returns the warning callback supplied when the session was created/entered, re-marshalled - // into the calling apartment. Used as a fallback by WSLCExecutionContext so that warnings - // emitted by operations that carry no explicit callback (e.g. resource recovery during the - // lazy VM start) still reach the session creator. Returns null if no callback was supplied - // or the creating client's proxy is no longer reachable. - wil::com_ptr AcquireWarningCallback() const; - HANDLE SessionTerminatingEvent() const noexcept { return m_sessionTerminatingEvent.get(); @@ -410,11 +403,6 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession wil::com_ptr m_git; DWORD m_vmFactoryGitCookie{}; - // The warning callback supplied at Initialize() is parked in the GIT for the same reason as - // the VM factory: it is used later, on demand, from other threads/apartments (a directly - // stored proxy would fail with RPC_E_WRONG_THREAD). Zero if no callback was supplied. - DWORD m_warningCallbackGitCookie{}; - std::optional m_virtualMachine; std::optional m_eventTracker; wil::unique_event m_dockerdReadyEvent{wil::EventOptions::ManualReset}; From 8c860b81c6fe58c08b0f8deb697cf3d7e007895a Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 30 Jun 2026 09:00:26 -0700 Subject: [PATCH 05/10] wslc: update recovery warning tests for log-only behavior Recovery warnings emitted during lazy VM start run outside the user's current command, so they are now logged (and written to the event log) instead of being routed back to the session-creation warning callback. Update the three WarningCallback*Recovery unit tests and the e2e test to assert the warning is no longer delivered to the session callback / printed on stderr. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/windows/WSLCTests.cpp | 26 +++++++++++-------- test/windows/wslc/e2e/WSLCE2EWarningTests.cpp | 25 +++++++++++------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 0a36406554..c8f315a8bf 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -11492,16 +11492,17 @@ class WSLCTests wsl::windows::common::security::ConfigureForCOMImpersonation(session2.get()); // The VM (and container recovery) starts lazily on the first operation. Trigger it so - // recovery runs and its warning is delivered to the session's warning callback. + // recovery runs. VERIFY_IS_TRUE(WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "exit 0"}).Launch(*session2).GetExitEvent().wait(30000)); - // Verify the warning matches the expected localized message for the corrupt container. + // Recovery runs under the triggering operation's context, which carries no warning + // callback, so the warning is logged rather than delivered to the session 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_FALSE(std::ranges::any_of(warnings, [&](const auto& w) { return w == recoveryWarning; })); VERIFY_SUCCEEDED(session2->Terminate()); } @@ -11564,15 +11565,16 @@ class WSLCTests wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); // The VM (and volume recovery) starts lazily on the first operation. Trigger it so - // recovery runs and its warning is delivered to the session's warning callback. + // recovery runs. VERIFY_IS_TRUE(WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "exit 0"}).Launch(*session).GetExitEvent().wait(30000)); - // Verify the warning matches the expected localized message for the missing volume. + // Recovery runs under the triggering operation's context, which carries no warning + // callback, so the warning is logged rather than delivered to the session 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_FALSE(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")); @@ -11633,13 +11635,15 @@ class WSLCTests wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); // The VM (and guest volume recovery) starts lazily on the first operation. Trigger it so - // recovery runs and its warning is delivered to the session's warning callback. + // recovery runs. VERIFY_IS_TRUE(WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "exit 0"}).Launch(*session).GetExitEvent().wait(30000)); + // Recovery runs under the triggering operation's context, which carries no warning + // callback, so the warning is logged rather than delivered to the session 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_FALSE(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/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); + } } }; From fdb6bf793056b27323eda53b7761c52c756e1777 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 30 Jun 2026 09:45:13 -0700 Subject: [PATCH 06/10] wslc: address review feedback on lazy VM lifecycle - BeginContainerOperation: reject new operations once the session is terminating/terminated, mirroring EnsureVmRunning's gate, so a started operation cannot pin a VM that is being torn down. - TearDownVmLockHeld: reset m_storageMounted after unmounting so the flag does not stay stale across an idle teardown. - CLI Session model: stop retaining the IWarningCallback for the session lifetime. Recovery warnings from lazy VM start are logged rather than delivered to the session callback, so the stashed callback (and its now-misleading comments) is dead state. The callback is still passed to CreateSession, where it is consumed during initialization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslc/services/SessionModel.h | 8 +------- src/windows/wslc/services/SessionService.cpp | 7 +++---- src/windows/wslcsession/WSLCSession.cpp | 5 +++++ 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/windows/wslc/services/SessionModel.h b/src/windows/wslc/services/SessionModel.h index 491df6fc3b..946e8c3bf4 100644 --- a/src/windows/wslc/services/SessionModel.h +++ b/src/windows/wslc/services/SessionModel.h @@ -22,8 +22,7 @@ struct Session NON_COPYABLE(Session); DEFAULT_MOVABLE(Session); - explicit Session(wil::com_ptr session, wil::com_ptr warningCallback = {}) : - m_session(std::move(session)), m_warningCallback(std::move(warningCallback)) + explicit Session(wil::com_ptr session) : m_session(std::move(session)) { } @@ -44,11 +43,6 @@ struct Session private: wil::com_ptr m_session; - - // Kept alive for the lifetime of the session model (i.e. the whole CLI command) so the service - // can deliver warnings emitted by lazy/background work — such as resource recovery on the first - // VM start — back to this CLI invocation, even though no single COM call carries the callback. - wil::com_ptr m_warningCallback; }; } // namespace wsl::windows::wslc::models \ No newline at end of file diff --git a/src/windows/wslc/services/SessionService.cpp b/src/windows/wslc/services/SessionService.cpp index ff8008a22f..8b59509d73 100644 --- a/src/windows/wslc/services/SessionService.cpp +++ b/src/windows/wslc/services/SessionService.cpp @@ -56,15 +56,14 @@ 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()); - // Hold the warning callback for the session's lifetime so warnings emitted by a lazy VM start - // (e.g. resource recovery) are still delivered to this CLI invocation. - return Session(std::move(session), wil::com_ptr(warningCallback.Get())); + return Session(std::move(session)); } int SessionService::Attach(const Session& session) diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index c9cd93e214..617b683da8 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -710,6 +710,7 @@ void WSLCSession::TearDownVmLockHeld(bool CaptureTerminationReason) 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. @@ -2556,6 +2557,10 @@ try 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(); From 623aa4184491419a501fc230ca0116b615554ed5 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Thu, 2 Jul 2026 13:07:15 -0700 Subject: [PATCH 07/10] wslc: use non-throwing filesystem::exists for storage VHD probe Probe the storage VHD once with the std::error_code overload so an access-denied/transient I/O error surfaces as a clear Win32 error via WIL instead of a generic filesystem_error-to-HRESULT conversion, and avoid the duplicate exists() check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslcsession/WSLCSession.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 617b683da8..b10fa5e609 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -388,15 +388,18 @@ try 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), - !std::filesystem::exists(storagePath / c_storageVhdFilename)); + HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), Localization::MessageWslcSessionStorageNotFound(Settings->StoragePath), !vhdExists); } - else if (!std::filesystem::exists(storagePath / c_storageVhdFilename)) + else if (!vhdExists) { // New session: the target path (if it exists) must be an empty directory. ValidateNewSessionStorageDirectory(storagePath); From 6d5cb7e420f9c782c48f5e614b2b2ed4db460dab Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 6 Jul 2026 10:06:07 -0700 Subject: [PATCH 08/10] wslc: extract WSLCSessionRuntime from WSLCSession Move the VM instance and its lifecycle state (VM, IO relay, docker client, event tracker, volumes, containerd/dockerd processes, exit-disposition and vm-state atomics, session lock, idle state, published-port bookkeeping, and ephemeral swap/storage state) out of WSLCSession into a new WSLCSessionRuntime. WSLCSession retains identity and orchestration and drives the runtime through BringUp/RecoverState/TearDownSessionState/OnSpontaneousExit/OnCrashDump hooks plus a SessionContext carrying the lifecycle primitives the runtime observes, so the runtime no longer reaches into WSLCSession private members. VM state is reached through the runtime, with Acquire()/LockedRuntime as the ergonomic lease accessor. Concurrency invariants are preserved: the exit-disposition claim protocol, the relay-thread teardown guard, lock ordering, GIT re-fetch per VM creation, and release-lock-before-Disarm in shutdown. The VM-exit monitor is armed between bring-up and state recovery to match the pre-extraction ordering. This is an isolated refactor commit on top of the idle-terminate work so it can be reverted independently. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslcsession/CMakeLists.txt | 2 + src/windows/wslcsession/WSLCContainer.cpp | 28 +- src/windows/wslcsession/WSLCSession.cpp | 771 +++++------------- src/windows/wslcsession/WSLCSession.h | 125 +-- .../wslcsession/WSLCSessionRuntime.cpp | 661 +++++++++++++++ src/windows/wslcsession/WSLCSessionRuntime.h | 205 +++++ 6 files changed, 1100 insertions(+), 692 deletions(-) create mode 100644 src/windows/wslcsession/WSLCSessionRuntime.cpp create mode 100644 src/windows/wslcsession/WSLCSessionRuntime.h diff --git a/src/windows/wslcsession/CMakeLists.txt b/src/windows/wslcsession/CMakeLists.txt index 6002df79fb..c2ec8e3955 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,7 @@ set(HEADERS WSLCProcessControl.h WSLCProcessIO.h WSLCSession.h + WSLCSessionRuntime.h WSLCSessionFactory.h WSLCSessionReference.h WSLCVirtualMachine.h diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 14c259b00c..74ece81bd4 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -575,7 +575,7 @@ WSLCContainerImpl::WSLCContainerImpl( // 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.IdleStateShared()); + m_activityHold = ActivityRef(m_wslcSession.Runtime().IdleStateShared()); } } @@ -2213,7 +2213,7 @@ __requires_lock_held(m_lock) void WSLCContainerImpl::UpdateActivityHoldLockHeld( const bool active = (m_state == WslcContainerStateRunning); if (active && !m_activityHold) { - m_activityHold = ActivityRef(m_wslcSession.IdleStateShared()); + m_activityHold = ActivityRef(m_wslcSession.Runtime().IdleStateShared()); } else if (!active && m_activityHold) { @@ -2239,7 +2239,7 @@ try *Stdout = {}; *Stderr = {}; - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Attach, DetachKeys, Stdin, Stdout, Stderr); } CATCH_RETURN(); @@ -2310,7 +2310,7 @@ try *Process = nullptr; - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Exec, Options, StartOptions, Process); } CATCH_RETURN(); @@ -2323,7 +2323,7 @@ try // 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.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Stop, Signal, TimeoutSeconds, false); } CATCH_RETURN(); @@ -2334,7 +2334,7 @@ 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.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Stop, Signal, {}, true); } CATCH_RETURN(); @@ -2346,7 +2346,7 @@ try THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCContainerStartFlagsValid), "Invalid flags: 0x%x", Flags); - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Start, Flags, StartOptions); } CATCH_RETURN(); @@ -2360,7 +2360,7 @@ try *Output = nullptr; - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Inspect, Output); } CATCH_RETURN(); @@ -2374,7 +2374,7 @@ try *Output = nullptr; - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Stats, Output); } CATCH_RETURN(); @@ -2391,7 +2391,7 @@ try // 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.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); auto [lock, impl] = LockImpl(); impl->Delete(Flags); @@ -2421,7 +2421,7 @@ try { WSLCExecutionContext context(&m_session); - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Export, TarHandle); } CATCH_RETURN(); @@ -2437,7 +2437,7 @@ try *Stdout = {}; *Stderr = {}; - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Logs, Flags, Stdout, Stderr, Since, Until, Tail); } CATCH_RETURN(); @@ -2616,7 +2616,7 @@ try { COMServiceExecutionContext context; - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::ConnectToNetwork, Options); } CATCH_RETURN(); @@ -2626,7 +2626,7 @@ try { COMServiceExecutionContext context; - auto vmLease = m_session.AcquireVmLease(); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::DisconnectFromNetwork, NetworkName); } CATCH_RETURN(); diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index b10fa5e609..e470196da7 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -430,12 +430,59 @@ try TraceLoggingValue(m_displayName.c_str(), "DisplayName"), TraceLoggingValue(m_creatorProcessName.c_str(), "CreatorProcess")); - // The VM is created lazily on the first operation that requires it (see EnsureVmRunning) and - // torn down once the session has been continuously idle (activity count zero) for the grace - // period. Wire up the idle-teardown timer; IdleState arms it whenever the activity count drops - // to zero and cancels it when activity resumes, so no dedicated worker thread is needed. const auto idleGracePeriod = m_settings.IdleTimeoutSec > 0 ? std::chrono::seconds(m_settings.IdleTimeoutSec) : c_vmIdleGracePeriod; - m_idleState->Initialize(idleGracePeriod, [this]() { OnIdleTimer(); }); + + WSLCSessionRuntime::RuntimeHooks hooks; + hooks.BringUp = [this]() { + // Configure storage. + ConfigureStorage(m_settings, m_userSid.empty() ? nullptr : reinterpret_cast(m_userSid.data())); + + // Mirror the host's trusted root CAs into the VM before dockerd starts. + InstallTrustedRootCertificates(); + + // Launch containerd first, then dockerd with the external containerd socket. + StartContainerd(); + + // Reset the readiness event before (re)starting dockerd so a stale signal from a prior + // VM instance is not observed. + m_runtime.DockerdReadyEvent().ResetEvent(); + StartDockerd(); + + m_runtime.InitializeDockerRuntime(m_storageVhdPath.parent_path()); + }; + + hooks.RecoverState = [this]() { + RecoverExistingNetworks(); + RecoverExistingContainers(); + }; + + hooks.TearDownSessionState = [this]() { + std::lock_guard containersLock(m_containersLock); + std::lock_guard networksLock(m_networksLock); + + m_containers.clear(); + m_networks.clear(); + }; + + hooks.OnSpontaneousExit = [this]() { LOG_IF_FAILED(Terminate()); }; + + hooks.OnCrashDump = std::bind( + &WSLCSession::OnCrashDumpWritten, + this, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3, + std::placeholders::_4, + std::placeholders::_5); + + WSLCSessionRuntime::SessionContext sessionContext; + sessionContext.Id = m_id; + sessionContext.DisplayName = m_displayName; + sessionContext.Terminating = &m_terminating; + sessionContext.SessionTerminatingEvent = std::addressof(m_sessionTerminatingEvent); + sessionContext.SessionTerminatedEvent = std::addressof(m_sessionTerminatedEvent); + + m_runtime.Initialize(m_vmFactoryGitCookie, m_git, &m_settings, idleGracePeriod, std::move(sessionContext), std::move(hooks)); return S_OK; } @@ -485,371 +532,9 @@ void WSLCSession::PersistSettings(const WSLCSessionInitSettings& Settings, PSID m_userSid.assign(bytes, bytes + length); } -bool WSLCSession::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; -} - -void WSLCSession::EnsureVmRunning() -{ - if (m_vmState.load() == VmState::Running) - { - return; - } - - auto lock = m_lock.lock_exclusive(); - - // Do not (re)start the VM once the session is terminating or has terminated. 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) - { - return; - } - - StartVmLockHeld(); -} - -bool WSLCSession::TryClaimExpectedStop() noexcept -{ - auto expected = VmExitDisposition::Active; - return m_vmExitDisposition.compare_exchange_strong(expected, VmExitDisposition::StopRequested); -} - -bool WSLCSession::TryClaimSpontaneousExit() noexcept -{ - auto expected = VmExitDisposition::Active; - return m_vmExitDisposition.compare_exchange_strong(expected, VmExitDisposition::ExitClaimed); -} - -void WSLCSession::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(), - std::bind(&WSLCSession::OnCrashDumpWritten, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); - 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)); - - // Configure storage. - ConfigureStorage(m_settings, m_userSid.empty() ? nullptr : reinterpret_cast(m_userSid.data())); - - // Mirror the host's trusted root CAs into the VM before dockerd starts. - InstallTrustedRootCertificates(); - - // Launch containerd first, then dockerd with the external containerd socket. - StartContainerd(); - - // Reset the readiness event before (re)starting dockerd so a stale signal from a prior - // VM instance is not observed. - m_dockerdReadyEvent.ResetEvent(); - StartDockerd(); - - // 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"); - - auto [_, __, channel] = m_virtualMachine->Fork(WSLC_FORK::Thread); - - m_dockerClient.emplace(std::move(channel), m_virtualMachine->TerminatingEvent(), m_virtualMachine->VmId(), 10 * 1000); - - // Start the event tracker. - m_eventTracker.emplace(m_dockerClient.value(), *this, *m_ioRelay); - - m_volumes.emplace(m_dockerClient.value(), m_virtualMachine.value(), m_eventTracker.value(), m_storageVhdPath.parent_path()); - - // Monitor for unexpected VM exit. - m_ioRelay->AddHandle(std::make_unique(m_vmExitedEvent.get(), std::bind(&WSLCSession::OnVmExited, this))); - - // Recover any existing resources from storage. - RecoverExistingNetworks(); - RecoverExistingContainers(); - - m_vmState.store(VmState::Running); - startCleanup.release(); - - WSL_LOG("WslcVmStarted", TraceLoggingValue(m_id, "SessionId")); -} - -void WSLCSession::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 WSLCSession::TearDownVmLockHeld(bool CaptureTerminationReason) -{ - 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 - if (m_ioRelay) - { - m_ioRelay->Stop(); - } - - { - std::lock_guard allocatedPortsLock(m_allocatedPortsLock); - m_allocatedPorts.clear(); - } - - m_eventTracker.reset(); - 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_terminationReason = WSLCVirtualMachineTerminationReasonShutdown; - } - - // 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_terminationReason, &details)); - m_terminationDetails = 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. - 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 WSLCSession::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); - }); - - StopVmLockHeld(); -} -CATCH_LOG(); - WSLCSession::VmLease WSLCSession::AcquireVmLease() { - return VmLease(*this); -} - -WSLCSession::VmLease::VmLease(WSLCSession& Session) : m_session(&Session) -{ - // 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_session->m_idleState->AddActivity(); - - auto countCleanup = wil::scope_exit([this]() { - m_session->m_idleState->ReleaseActivity(); - m_session = nullptr; - }); - - // Activity increment may race with idle teardown. Retry until we hold the lock with VM running. - for (;;) - { - m_session->EnsureVmRunning(); - - m_lock = m_session->m_lock.lock_shared(); - - if (m_session->m_vmState.load() == VmState::Running) - { - break; - } - - m_lock.reset(); - } - - countCleanup.release(); -} - -WSLCSession::VmLease::VmLease(VmLease&& Other) noexcept : - m_session(std::exchange(Other.m_session, nullptr)), m_lock(std::move(Other.m_lock)) -{ -} - -WSLCSession::VmLease& WSLCSession::VmLease::operator=(VmLease&& Other) noexcept -{ - if (this != &Other) - { - if (m_session != 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_session->m_idleState->ReleaseActivity(); - } - - m_session = std::exchange(Other.m_session, nullptr); - m_lock = std::move(Other.m_lock); - } - - return *this; -} - -WSLCSession::VmLease::~VmLease() -{ - if (m_session != 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_session->m_idleState->ReleaseActivity(); - } + return m_runtime.AcquireVmLease(); } WSLCSession::~WSLCSession() @@ -874,8 +559,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; } @@ -893,7 +578,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())); @@ -901,7 +586,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)) { @@ -933,31 +618,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); + auto& swapVhdPath = m_runtime.SwapVhdPath(); + swapVhdPath = storagePath / "swap.vhdx"; + 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 (...) { @@ -991,7 +677,7 @@ CATCH_RETURN(); void WSLCSession::OnDockerdExited() { - if (!m_sessionTerminatingEvent.is_signaled() && m_vmExitDisposition.load() != VmExitDisposition::StopRequested) + if (!m_sessionTerminatingEvent.is_signaled() && m_runtime.ExitDispositionAtomic().load() != WSLCSessionRuntime::VmExitDisposition::StopRequested) { WSL_LOG("UnexpectedDockerdExit", TraceLoggingValue(m_displayName.c_str(), "Name")); } @@ -999,32 +685,12 @@ void WSLCSession::OnDockerdExited() void WSLCSession::OnContainerdExited() { - if (!m_sessionTerminatingEvent.is_signaled() && m_vmExitDisposition.load() != VmExitDisposition::StopRequested) + if (!m_sessionTerminatingEvent.is_signaled() && m_runtime.ExitDispositionAtomic().load() != WSLCSessionRuntime::VmExitDisposition::StopRequested) { WSL_LOG("UnexpectedContainerdExit", TraceLoggingValue(m_displayName.c_str(), "Name")); } } -void WSLCSession::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")); - - LOG_IF_FAILED(Terminate()); -} - void WSLCSession::OnProcessLog(const gsl::span& Buffer, PCSTR Source) try { @@ -1040,11 +706,11 @@ try TraceLoggingValue(entry.c_str(), "Content"), TraceLoggingValue(m_displayName.c_str(), "Name")); - if (!m_dockerdReadyEvent.is_signaled()) + if (!m_runtime.DockerdReadyEvent().is_signaled()) { if (entry.find(c_dockerdReadyLogLine) != std::string::npos) { - m_dockerdReadyEvent.SetEvent(); + m_runtime.DockerdReadyEvent().SetEvent(); } } } @@ -1055,15 +721,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; } @@ -1081,7 +747,7 @@ void WSLCSession::StartContainerd() args.emplace_back("debug"); } - m_containerdProcess = StartProcess("/usr/bin/containerd", args, "containerd", std::bind(&WSLCSession::OnContainerdExited, this)); + m_runtime.ContainerdProcess() = StartProcess("/usr/bin/containerd", args, "containerd", std::bind(&WSLCSession::OnContainerdExited, this)); WSL_LOG("ContainerdStarted"); } @@ -1094,7 +760,7 @@ void WSLCSession::StartDockerd() args.emplace_back("--debug"); } - m_dockerdProcess = StartProcess("/usr/bin/dockerd", args, "dockerd", std::bind(&WSLCSession::OnDockerdExited, this)); + m_runtime.DockerdProcess() = StartProcess("/usr/bin/dockerd", args, "dockerd", std::bind(&WSLCSession::OnDockerdExited, this)); WSL_LOG("DockerdStarted"); } @@ -1114,7 +780,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()})); @@ -1271,8 +937,8 @@ try auto [repo, tagOrDigest] = wslutil::ParseImage(Image); EnforceRegistryAllowlist(repo); - auto lock = AcquireVmLease(); - 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()) { @@ -1286,7 +952,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); @@ -1329,16 +995,16 @@ try comCall = RegisterUserCOMCallback(); } - auto lock = AcquireVmLease(); + 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)); + THROW_IF_FAILED(runtime.Vm().MountWindowsFolder(Options->ContextPath, mountPath.c_str(), TRUE)); auto unmountFolder = - wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { m_virtualMachine->UnmountWindowsFolder(mountPath.c_str()); }); + 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)) @@ -1383,7 +1049,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(); @@ -1660,9 +1326,9 @@ try 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); @@ -1693,9 +1359,9 @@ try 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"); @@ -1725,7 +1391,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(); @@ -1864,9 +1530,9 @@ try RETURN_HR_IF(E_INVALIDARG, strlen(ImageNameOrID) > WSLC_MAX_IMAGE_NAME_LENGTH); 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; } @@ -1897,9 +1563,9 @@ try 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; } @@ -1909,7 +1575,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); @@ -1972,12 +1638,12 @@ try 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"); @@ -2081,12 +1747,12 @@ try 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) @@ -2155,11 +1821,11 @@ try 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) { @@ -2191,9 +1857,9 @@ try EnforceRegistryAllowlist(repo); 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->PushImage(repo, tagOrDigest, RegistryAuthenticationInformation); + auto requestContext = m_runtime.Docker().PushImage(repo, tagOrDigest, RegistryAuthenticationInformation); StreamImageOperation(*requestContext, Image, "Push", ProgressCallback); return S_OK; @@ -2212,7 +1878,7 @@ try *Output = nullptr; auto lock = AcquireVmLease(); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); *Output = wil::make_unique_ansistring(InspectImageLockHeld(ImageNameOrId).c_str()).release(); @@ -2225,7 +1891,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) { @@ -2260,13 +1926,13 @@ try *IdentityToken = nullptr; 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()); 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); @@ -2292,12 +1958,12 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); auto lock = AcquireVmLease(); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + 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"); @@ -2373,10 +2039,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') @@ -2423,14 +2089,14 @@ void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptio *containerOptions, containerName, *this, - m_virtualMachine.value(), + m_runtime.Vm(), m_pluginNotifier.get(), m_networks, - m_volumes.value(), + m_runtime.Volumes(), std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), - m_eventTracker.value(), - m_dockerClient.value(), - *m_ioRelay); + m_runtime.Events(), + m_runtime.Docker(), + *m_runtime.Relay()); // 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)); @@ -2478,7 +2144,7 @@ try try { - inspectResult = m_dockerClient->InspectContainer(Id); + inspectResult = m_runtime.Docker().InspectContainer(Id); } catch (DockerHTTPException& e) { @@ -2534,14 +2200,14 @@ 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_idleState->AddActivity(); - auto countCleanup = wil::scope_exit([this]() { m_idleState->ReleaseActivity(); }); + 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_idleState; + 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. @@ -2608,12 +2274,12 @@ try } auto lock = AcquireVmLease(); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + 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"); @@ -2687,7 +2353,7 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); auto lock = AcquireVmLease(); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); std::lock_guard containersLock{m_containersLock}; @@ -2695,7 +2361,7 @@ try try { - pruneResult = m_dockerClient->PruneContainers(filters); + pruneResult = m_runtime.Docker().PruneContainers(filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune containers"); @@ -2757,10 +2423,10 @@ try *Errno = -1; // Make sure not to return 0 if something fails. } - auto lock = AcquireVmLease(); - 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 = m_virtualMachine->CreateLinuxProcess(Executable, *Options, TtyRows, TtyColumns, Errno); + 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 @@ -2779,7 +2445,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()); } @@ -2792,16 +2458,16 @@ try THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Path), !std::filesystem::path(Path).is_absolute()); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + 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; } @@ -2820,14 +2486,14 @@ try auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCVolumeMetadataLabel); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + 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(); @@ -2840,9 +2506,9 @@ try RETURN_HR_IF_NULL(E_POINTER, Name); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + 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(); @@ -2861,9 +2527,9 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + 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()) { @@ -2893,9 +2559,9 @@ try ValidateName(name.c_str(), WSLC_MAX_VOLUME_NAME_LENGTH); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + 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; @@ -2918,12 +2584,12 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + 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"); @@ -2949,32 +2615,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) @@ -2997,8 +2637,8 @@ try auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCNetworkManagedLabel); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + 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)); @@ -3037,7 +2677,7 @@ try docker_schema::CreateNetworkResponse createResult; try { - createResult = m_dockerClient->CreateNetwork(request); + createResult = m_runtime.Docker().CreateNetwork(request); } catch (const DockerHTTPException& e) { @@ -3051,14 +2691,14 @@ 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) { @@ -3106,8 +2746,8 @@ try ValidateName(name.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + 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); @@ -3116,7 +2756,7 @@ try try { - m_dockerClient->RemoveNetwork(name); + m_runtime.Docker().RemoveNetwork(name); } catch (const DockerHTTPException& e) { @@ -3240,15 +2880,15 @@ try filters["label"].push_back(WSLCNetworkManagedLabel); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + 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"); @@ -3319,10 +2959,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 + // Ensure only one Terminate() runs. This must be checked before taking m_runtime.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. + // holds m_runtime.Lock() and calls m_runtime.Relay()->Stop(), the relay thread must not re-enter + // Terminate() and deadlock on m_runtime.Lock(). if (m_terminating.exchange(true)) { return S_OK; @@ -3344,8 +2984,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 m_runtime.Lock(). + // This allows a session to be unblocked if a stuck operation is holding m_runtime.Lock(). // N.B. This must happen under m_userHandlesLock to synchronize with potentially running operations. if (!m_sessionTerminatingEvent.is_signaled()) { @@ -3365,32 +3005,11 @@ try CancelUserCOMCallbacks(); } - sessionLock = m_lock.try_lock_exclusive(); + sessionLock = m_runtime.Lock().try_lock_exclusive(); retrying = true; } - // Acquire an exclusive lock to ensure that no operation is running. - WI_VERIFY(sessionLock); - - // Tear down the VM (if running) and all VM-scoped state, capturing the termination reason. - // This mirrors the soft teardown used for idle shutdown, but here it is permanent. - TearDownVmLockHeld(/* CaptureTerminationReason */ true); - - m_vmState.store(VmState::None); - - // 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. - sessionLock.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(); + 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. @@ -3468,9 +3087,9 @@ try RETURN_HR_IF_NULL(E_POINTER, LinuxPath); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + 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(); @@ -3482,9 +3101,9 @@ try RETURN_HR_IF_NULL(E_POINTER, LinuxPath); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + 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(); @@ -3494,35 +3113,36 @@ try WSLCExecutionContext context(this); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + 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++; @@ -3540,27 +3160,28 @@ try WSLCExecutionContext context(this); auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + 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; @@ -3906,11 +3527,11 @@ 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) { @@ -3919,13 +3540,13 @@ void WSLCSession::RecoverExistingContainers() auto container = WSLCContainerImpl::Open( dockerContainer, *this, - m_virtualMachine.value(), + m_runtime.Vm(), m_pluginNotifier.get(), - m_volumes.value(), + m_runtime.Volumes(), std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1), - m_eventTracker.value(), - m_dockerClient.value(), - *m_ioRelay); + m_runtime.Events(), + m_runtime.Docker(), + *m_runtime.Relay()); auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container)); WI_ASSERT(inserted); @@ -3946,10 +3567,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 8eff3eb4a3..8b2c7332f1 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -20,6 +20,7 @@ Module Name: #include "WSLCContainer.h" #include "WSLCIdleState.h" #include "WSLCVolumes.h" +#include "WSLCSessionRuntime.h" #include "WSLCNetworkMetadata.h" #include "DockerEventTracker.h" #include "DockerHTTPClient.h" @@ -85,7 +86,9 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession friend class WSLCContainer; public: - WSLCSession() = default; + WSLCSession() : m_runtime(*this) + { + } ~WSLCSession(); @@ -273,7 +276,17 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession // keeping the session object itself alive. std::shared_ptr IdleStateShared() const noexcept { - return m_idleState; + 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 @@ -285,101 +298,30 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession private: ULONG m_id = 0; - - // VM lifecycle state for on-demand creation / idle termination. - enum class VmState - { - None, - Starting, - Running, - Stopping, - }; - - // Single-owner arbitration for a VM exit: exactly one side tears a given VM instance down, - // resolved atomically. An expected stop (idle teardown or bring-up cleanup, on a lock-holding - // thread) and a spontaneous exit (OnVmExited, lock-free on the relay thread) each try to claim - // it; the loser declines. This avoids both a missed teardown and a deadlock (a lock-holder - // joining the relay thread while OnVmExited spins for the lock in Terminate()). A fresh VM - // starts Active. Claim via TryClaim*(), not by touching the atomic directly. - enum class VmExitDisposition - { - Active, // Running normally; a VM exit is unexpected and triggers a permanent Terminate(). - StopRequested, // An expected (soft) stop is in progress; OnVmExited treats the exit as expected. - ExitClaimed, // OnVmExited owns the permanent teardown of a spontaneous exit. - }; - - // Claims an expected (soft) stop of the current VM from a lock-holding thread. On success a - // racing OnVmExited() declines, so the caller may tear down (joining the relay thread is safe). - // On failure OnVmExited() already owns a spontaneous-exit teardown and is spinning for the lock - // in Terminate(); the caller must not tear down or it deadlocks joining the relay. - [[nodiscard]] bool TryClaimExpectedStop() noexcept; - - // Claims the teardown of a spontaneous VM exit from OnVmExited(). Fails if an expected stop is - // already in progress, in which case the exit was wanted and the caller declines. - [[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(); - - // Idle-teardown callback invoked by IdleState's timer once the VM has been continuously idle - // (activity count zero) for the grace period. Runs on a threadpool thread. - void OnIdleTimer(); - bool IdleTerminationEnabled() const noexcept; void PersistSettings(const WSLCSessionInitSettings& Settings, PSID UserSid); - // RAII lease taken at the top of every VM-requiring operation. On construction it - // ensures the VM is running (lazily restarting it if it was idle-terminated) and records - // an in-flight operation so idle teardown is deferred; it then holds the shared session - // lock for the operation's duration. On destruction it releases the lock and triggers an - // idle check. - class VmLease - { - public: - VmLease() = default; - explicit VmLease(WSLCSession& Session); - VmLease(VmLease&& Other) noexcept; - VmLease& operator=(VmLease&& Other) noexcept; - ~VmLease(); - - VmLease(const VmLease&) = delete; - VmLease& operator=(const VmLease&) = delete; - - private: - WSLCSession* m_session{}; - wil::rwlock_release_shared_scope_exit m_lock; - }; - + 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(); @@ -394,8 +336,6 @@ 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; - // 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 @@ -403,41 +343,24 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession wil::com_ptr m_git; DWORD m_vmFactoryGitCookie{}; - std::optional m_virtualMachine; - std::optional m_eventTracker; - wil::unique_event m_dockerdReadyEvent{wil::EventOptions::ManualReset}; + 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. m_runtime.Lock() must be acquired before acquiring m_containersLock or m_networksLock. + // These locks protect m_containers without requiring an exclusive m_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 m_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; WSLCVirtualMachineTerminationReason m_terminationReason{WSLCVirtualMachineTerminationReasonUnknown}; std::wstring m_terminationDetails; - wil::srwlock m_lock; - std::optional m_ioRelay; - - // VM lifecycle / idle-termination state. - std::atomic m_vmState{VmState::None}; - std::atomic m_vmExitDisposition{VmExitDisposition::Active}; - // In-flight activity count, idle timer and teardown callback, decoupled from this object's - // lifetime (see IdleState in WSLCIdleState.h) so activity tokens and container COM wrappers can - // safely manage activity without keeping the session alive. See WSLCContainerImpl's ActivityRef - // (m_activityHold), WSLCSession::VmLease and CreateActivityToken(). - std::shared_ptr m_idleState{std::make_shared()}; // 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 @@ -448,8 +371,6 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession std::optional m_settingsRootVhdTypeOverride; std::vector m_userSid; - std::optional m_containerdProcess; - std::optional m_dockerdProcess; WSLCFeatureFlags m_featureFlags{}; std::function m_destructionCallback; std::atomic m_terminating{false}; @@ -468,14 +389,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..88bc3f769b --- /dev/null +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -0,0 +1,661 @@ +/*++ + +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" + +using wsl::windows::service::wslc::WSLCSessionRuntime; + +namespace { + +constexpr auto c_containerdStorage = "/var/lib/docker"; +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(); }); + + 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::srwlock& WSLCSessionRuntime::Lock() noexcept +{ + return m_lock; +} + +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(); +} + +std::atomic& WSLCSessionRuntime::StateAtomic() noexcept +{ + return m_vmState; +} + +std::atomic& WSLCSessionRuntime::ExitDispositionAtomic() noexcept +{ + return m_vmExitDisposition; +} + +wil::unique_event& WSLCSessionRuntime::VmExitedEvent() noexcept +{ + return m_vmExitedEvent; +} + +wil::unique_event& WSLCSessionRuntime::DockerdReadyEvent() noexcept +{ + return m_dockerdReadyEvent; +} + +std::optional& WSLCSessionRuntime::ContainerdProcess() +{ + return m_containerdProcess; +} + +std::optional& WSLCSessionRuntime::DockerdProcess() +{ + return m_dockerdProcess; +} + +std::filesystem::path& WSLCSessionRuntime::SwapVhdPath() noexcept +{ + return m_swapVhdPath; +} + +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() +{ + if (m_vmState.load() == VmState::Running) + { + return; + } + + auto lock = m_lock.lock_exclusive(); + + // Do not (re)start the VM once the session is terminating or has terminated. 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) + { + return; + } + + StartVmLockHeld(); +} + +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)); + + 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"); + + auto [_, __, channel] = m_virtualMachine->Fork(WSLC_FORK::Thread); + + m_dockerClient.emplace(std::move(channel), m_virtualMachine->TerminatingEvent(), m_virtualMachine->VmId(), 10 * 1000); + + // Start the event tracker. + m_eventTracker.emplace(m_dockerClient.value(), *m_session, *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) +{ + if (m_hooks.TearDownSessionState) + { + m_hooks.TearDownSessionState(); + } + + 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(); + } + + m_eventTracker.reset(); + 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. + 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); + }); + + StopVmLockHeld(); +} +CATCH_LOG(); + +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& sessionLock, + WSLCVirtualMachineTerminationReason& terminationReason, + std::wstring& terminationDetails) +{ + if (!m_initialized) + { + return; + } + + // Acquire an exclusive lock to ensure that no operation is running. + WI_VERIFY(sessionLock); + + // Tear down the VM (if running) and all VM-scoped state, capturing the termination reason. + // This mirrors the soft teardown used for idle shutdown, but here it is permanent. + TearDownVmLockHeld(/* CaptureTerminationReason */ true); + + m_vmState.store(VmState::None); + + // 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. + sessionLock.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(); + + terminationReason = m_lastTerminationReason; + terminationDetails = m_lastTerminationDetails; +} + +} // 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..724fa9214a --- /dev/null +++ b/src/windows/wslcsession/WSLCSessionRuntime.h @@ -0,0 +1,205 @@ +/*++ + +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; + std::function TearDownSessionState; + std::function OnSpontaneousExit; + WSLCVirtualMachine::TOnCrashDump OnCrashDump; + }; + + struct SessionContext + { + ULONG Id{}; + std::wstring DisplayName; + const std::atomic* Terminating{}; + wil::unique_event* SessionTerminatingEvent{}; + wil::unique_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; + wil::srwlock& Lock() noexcept; + IdleState& Idle() noexcept; + std::shared_ptr IdleStateShared() const noexcept; + VmState State() const noexcept; + VmExitDisposition ExitDisposition() const noexcept; + std::atomic& StateAtomic() noexcept; + std::atomic& ExitDispositionAtomic() noexcept; + wil::unique_event& VmExitedEvent() noexcept; + wil::unique_event& DockerdReadyEvent() noexcept; + std::optional& ContainerdProcess(); + std::optional& DockerdProcess(); + + std::filesystem::path& SwapVhdPath() noexcept; + 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(); + + void Shutdown( + wil::rwlock_release_exclusive_scope_exit& sessionLock, + WSLCVirtualMachineTerminationReason& terminationReason, + std::wstring& terminationDetails); + +private: + bool IdleTerminationEnabled() const noexcept; + int StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs); + + WSLCSession* m_session{}; + RuntimeHooks m_hooks; + + ULONG m_id{}; + std::wstring m_displayName; + bool m_initialized{}; + const std::atomic* m_terminating{}; + wil::unique_event* m_sessionTerminatingEvent{}; + wil::unique_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; + 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}; + std::shared_ptr m_idleState{std::make_shared()}; + + WSLCVirtualMachineTerminationReason m_lastTerminationReason{WSLCVirtualMachineTerminationReasonUnknown}; + std::wstring m_lastTerminationDetails; +}; + +} // namespace wsl::windows::service::wslc From 4625dde952759bed1502420bfc72ae49f361ad02 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 6 Jul 2026 11:13:13 -0700 Subject: [PATCH 09/10] fix formatting --- src/windows/wslcsession/WSLCSession.cpp | 17 ++++++----------- src/windows/wslcsession/WSLCSessionRuntime.cpp | 16 ++++++---------- src/windows/wslcsession/WSLCSessionRuntime.h | 14 +++++++------- 3 files changed, 19 insertions(+), 28 deletions(-) diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index e470196da7..61cc24d832 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -467,13 +467,7 @@ try hooks.OnSpontaneousExit = [this]() { LOG_IF_FAILED(Terminate()); }; hooks.OnCrashDump = std::bind( - &WSLCSession::OnCrashDumpWritten, - this, - std::placeholders::_1, - std::placeholders::_2, - std::placeholders::_3, - std::placeholders::_4, - std::placeholders::_5); + &WSLCSession::OnCrashDumpWritten, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5); WSLCSessionRuntime::SessionContext sessionContext; sessionContext.Id = m_id; @@ -747,7 +741,8 @@ void WSLCSession::StartContainerd() args.emplace_back("debug"); } - m_runtime.ContainerdProcess() = StartProcess("/usr/bin/containerd", args, "containerd", std::bind(&WSLCSession::OnContainerdExited, this)); + m_runtime.ContainerdProcess() = + StartProcess("/usr/bin/containerd", args, "containerd", std::bind(&WSLCSession::OnContainerdExited, this)); WSL_LOG("ContainerdStarted"); } @@ -1003,8 +998,7 @@ try THROW_IF_FAILED(CoCreateGuid(&volumeId)); auto mountPath = std::format("/mnt/{}", wsl::shared::string::GuidToString(volumeId)); 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()); }); + 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)) @@ -2691,7 +2685,8 @@ try EMIT_USER_WARNING(wsl::shared::string::MultiByteToWide(createResult.Warning)); } - auto removeNetworkCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, &name]() { m_runtime.Docker().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}. diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp index 88bc3f769b..3ad490862f 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.cpp +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -290,11 +290,7 @@ void WSLCSessionRuntime::StartVmLockHeld() 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.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. @@ -307,7 +303,8 @@ void WSLCSessionRuntime::StartVmLockHeld() } // Monitor for unexpected VM exit. - m_ioRelay->AddHandle(std::make_unique(m_vmExitedEvent.get(), std::bind(&WSLCSessionRuntime::OnVmExited, this))); + m_ioRelay->AddHandle( + std::make_unique(m_vmExitedEvent.get(), std::bind(&WSLCSessionRuntime::OnVmExited, this))); if (m_hooks.RecoverState) { @@ -579,7 +576,8 @@ WSLCSessionRuntime::VmLease::~VmLease() } } -WSLCSessionRuntime::LockedRuntime::LockedRuntime(WSLCSessionRuntime& Runtime) : m_runtime(&Runtime), m_lease(Runtime.AcquireVmLease()) +WSLCSessionRuntime::LockedRuntime::LockedRuntime(WSLCSessionRuntime& Runtime) : + m_runtime(&Runtime), m_lease(Runtime.AcquireVmLease()) { } @@ -622,9 +620,7 @@ void WSLCSessionRuntime::OnVmExited() } void WSLCSessionRuntime::Shutdown( - wil::rwlock_release_exclusive_scope_exit& sessionLock, - WSLCVirtualMachineTerminationReason& terminationReason, - std::wstring& terminationDetails) + wil::rwlock_release_exclusive_scope_exit& sessionLock, WSLCVirtualMachineTerminationReason& terminationReason, std::wstring& terminationDetails) { if (!m_initialized) { diff --git a/src/windows/wslcsession/WSLCSessionRuntime.h b/src/windows/wslcsession/WSLCSessionRuntime.h index 724fa9214a..2cfd9e8963 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.h +++ b/src/windows/wslcsession/WSLCSessionRuntime.h @@ -145,9 +145,12 @@ class WSLCSessionRuntime [[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); + _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(); @@ -155,10 +158,7 @@ class WSLCSessionRuntime [[nodiscard]] VmLease AcquireVmLease(); [[nodiscard]] LockedRuntime Acquire(); - void Shutdown( - wil::rwlock_release_exclusive_scope_exit& sessionLock, - WSLCVirtualMachineTerminationReason& terminationReason, - std::wstring& terminationDetails); + void Shutdown(wil::rwlock_release_exclusive_scope_exit& sessionLock, WSLCVirtualMachineTerminationReason& terminationReason, std::wstring& terminationDetails); private: bool IdleTerminationEnabled() const noexcept; From e51b0d93935a13fe4b23bb19c858f3d7b24010b4 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 7 Jul 2026 14:00:39 -0700 Subject: [PATCH 10/10] wslc: skip containerd VHD unmount when storage was never mounted Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslcsession/WSLCSessionRuntime.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/windows/wslcsession/WSLCSessionRuntime.cpp b/src/windows/wslcsession/WSLCSessionRuntime.cpp index 3ad490862f..0f50c7ceb8 100644 --- a/src/windows/wslcsession/WSLCSessionRuntime.cpp +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -422,11 +422,14 @@ void WSLCSessionRuntime::TearDownVmLockHeld(bool CaptureTerminationReason) } // N.B. dockerd has exited by this point, so unmounting the VHD is safe since no container can be running. - try + if (m_storageMounted) { - m_virtualMachine->Unmount(c_containerdStorage); + try + { + m_virtualMachine->Unmount(c_containerdStorage); + } + CATCH_LOG(); } - CATCH_LOG(); } m_dockerdProcess.reset();