diff --git a/src/windows/common/WSLCUserSettings.cpp b/src/windows/common/WSLCUserSettings.cpp index b0c3e1e86..0a8581c28 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 30e3f3f9a..7219fa7ad 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/HcsVirtualMachine.cpp b/src/windows/service/exe/HcsVirtualMachine.cpp index 17fd76a63..5c9587000 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 e5e0540e9..1e0a9d06c 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. @@ -285,8 +286,7 @@ void WSLCSessionManagerImpl::CreateSession( g_pluginManager, sessionId, creatorPid, std::wstring(resolvedDisplayName), wil::shared_handle(sharedToken), std::vector(storedSid)); // Create the VM factory in the SYSTEM service (privileged). The per-user session - // uses it to create the VM. Funneling VM creation through a factory lets the session - // own when VMs are created, rather than having one handed to it up front. + // uses it to create VMs on demand and recreate them after idle-termination. auto vmFactory = Microsoft::WRL::Make(Settings); // Launch per-user COM server factory and add it to a fresh per-session job object for crash cleanup. @@ -473,6 +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 7639c4517..181f2b441 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; [ @@ -633,6 +635,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 9a85ebf98..3c46a2578 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 ab6b5824e..946e8c3bf 100644 --- a/src/windows/wslc/services/SessionModel.h +++ b/src/windows/wslc/services/SessionModel.h @@ -31,6 +31,16 @@ struct Session return m_session.get(); } + // Acquires an activity token that keeps the VM alive for the duration of a client-side + // container operation (resolve + operate, plus any streamed output). Hold the returned + // pointer for the whole operation; releasing it lets the VM idle-terminate again. + [[nodiscard]] wil::com_ptr BeginContainerOperation() const + { + wil::com_ptr operation; + THROW_IF_FAILED(m_session->BeginContainerOperation(&operation)); + return operation; + } + private: wil::com_ptr m_session; }; diff --git a/src/windows/wslc/services/SessionService.cpp b/src/windows/wslc/services/SessionService.cpp index 86ebcebbe..8b59509d7 100644 --- a/src/windows/wslc/services/SessionService.cpp +++ b/src/windows/wslc/services/SessionService.cpp @@ -56,11 +56,13 @@ Session SessionService::OpenOrCreateDefaultSession() { auto manager = CreateSessionManager(); - // Null Settings = default session with server-determined name and settings. + // Null Settings = default session with server-determined name and settings. The warning callback + // is consumed during CreateSession (session initialization); it is not retained afterwards. wil::com_ptr session; auto warningCallback = Microsoft::WRL::Make(); THROW_IF_FAILED(manager->CreateSession(nullptr, WSLCSessionFlagsNone, warningCallback.Get(), &session)); wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); + return Session(std::move(session)); } diff --git a/src/windows/wslcsession/CMakeLists.txt b/src/windows/wslcsession/CMakeLists.txt index 6002df79f..c2ec8e395 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/IORelay.cpp b/src/windows/wslcsession/IORelay.cpp index 6677bca87..09f5e05cd 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 879d3fee1..844c16dee 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 674b0b0cd..74ece81bd 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -570,6 +570,13 @@ WSLCContainerImpl::WSLCContainerImpl( m_initProcessFlags(InitProcessFlags), m_containerFlags(ContainerFlags) { + // Acquire the activity hold up front for a container recovered in the running state, so it keeps + // the VM alive even before any client opens its wrapper. A merely-created (never-started) + // container does not pin the VM: its metadata survives teardown and the VM restarts on next use. + if (m_state == WslcContainerStateRunning) + { + m_activityHold = ActivityRef(m_wslcSession.Runtime().IdleStateShared()); + } } WSLCContainerImpl::~WSLCContainerImpl() @@ -1276,6 +1283,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()); @@ -1896,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) { - container->m_stateChangedAt = ParseDockerTimestamp(timestamp); + // A created-but-never-started container has no StartedAt/FinishedAt; its state last + // changed when it was created. + container->m_stateChangedAt = static_cast(dockerContainer.Created); + } + else + { + const auto& timestamp = (state == WslcContainerStateRunning) ? inspectData.State.StartedAt : inspectData.State.FinishedAt; + + if (!timestamp.empty()) + { + container->m_stateChangedAt = ParseDockerTimestamp(timestamp); + } } } catch (...) @@ -2177,6 +2200,25 @@ __requires_lock_held(m_lock) void WSLCContainerImpl::Transition(WSLCContainerSta m_state = State; m_stateChangedAt = stateChangedAt.value_or(static_cast(std::time(nullptr))); + + // Keep the VM alive while this container is Running and release the hold once it leaves that + // state, even when no client holds the wrapper (e.g. a detached `run -d` container). Dropping + // the hold on the transition out of Running is what lets an otherwise-idle VM be torn down; a + // Created or Exited container does not pin the VM, since its metadata survives teardown. + UpdateActivityHoldLockHeld(); +} + +__requires_lock_held(m_lock) void WSLCContainerImpl::UpdateActivityHoldLockHeld() noexcept +{ + const bool active = (m_state == WslcContainerStateRunning); + if (active && !m_activityHold) + { + m_activityHold = ActivityRef(m_wslcSession.Runtime().IdleStateShared()); + } + else if (!active && m_activityHold) + { + m_activityHold.reset(); + } } WSLCContainer::WSLCContainer(WSLCContainerImpl* impl, WSLCSession& session, std::function&& OnDeleted) : @@ -2185,6 +2227,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 +2239,10 @@ HRESULT WSLCContainer::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* *Stdout = {}; *Stderr = {}; + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Attach, DetachKeys, Stdin, Stdout, Stderr); } +CATCH_RETURN(); HRESULT WSLCContainer::GetState(WSLCContainerState* Result) { @@ -2255,6 +2300,7 @@ HRESULT WSLCContainer::GetInitProcess(IWSLCProcess** Process) } HRESULT WSLCContainer::Exec(const WSLCProcessOptions* Options, const WSLCProcessStartOptions* StartOptions, IWSLCProcess** Process) +try { WSLCExecutionContext context(&m_session); @@ -2263,22 +2309,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.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Exec, Options, StartOptions, Process); } +CATCH_RETURN(); HRESULT WSLCContainer::Stop(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds) +try { WSLCExecutionContext context(&m_session); + // Hold a VM lease for the whole operation: --rm containers self-delete during Stop, which + // disconnects the wrapper and drops activity. Without the lease, the idle worker can fire + // during the post-stop destroy wait (up to 60s) and tear the VM down mid-call. + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Stop, Signal, TimeoutSeconds, false); } +CATCH_RETURN(); HRESULT WSLCContainer::Kill(_In_ WSLCSignal Signal) +try { WSLCExecutionContext context(&m_session); + // Hold a VM lease for the same reason as Stop(): --rm can self-delete and drop activity. + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Stop, Signal, {}, true); } +CATCH_RETURN(); HRESULT WSLCContainer::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, IWarningCallback* WarningCallback) try @@ -2287,11 +2346,13 @@ try THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCContainerStartFlagsValid), "Invalid flags: 0x%x", Flags); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Start, Flags, StartOptions); } CATCH_RETURN(); HRESULT WSLCContainer::Inspect(LPSTR* Output) +try { WSLCExecutionContext context(&m_session); @@ -2299,8 +2360,10 @@ HRESULT WSLCContainer::Inspect(LPSTR* Output) *Output = nullptr; + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Inspect, Output); } +CATCH_RETURN(); HRESULT WSLCContainer::Stats(LPSTR* Output) try @@ -2310,6 +2373,8 @@ try RETURN_HR_IF(E_POINTER, Output == nullptr); *Output = nullptr; + + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Stats, Output); } CATCH_RETURN(); @@ -2322,6 +2387,11 @@ try THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCDeleteFlagsValid), "Invalid flags: 0x%x", Flags); // Special case for Delete(): If deletion is successful, notify the WSLCSession that the container has been deleted. + // Hold a VM lease across the whole operation: deleting a container makes it inactive and + // can trigger an idle teardown. Without the lease the idle worker could take the session + // lock exclusively and clear m_containers (destroying this container) concurrently, racing + // the delete and inverting the container->session lock order. + auto vmLease = m_session.Runtime().AcquireVmLease(); auto [lock, impl] = LockImpl(); impl->Delete(Flags); @@ -2347,11 +2417,14 @@ try CATCH_LOG(); HRESULT WSLCContainer::Export(WSLCHandle TarHandle) +try { WSLCExecutionContext context(&m_session); + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Export, TarHandle); } +CATCH_RETURN(); HRESULT WSLCContainer::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) try @@ -2364,6 +2437,7 @@ try *Stdout = {}; *Stderr = {}; + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::Logs, Flags, Stdout, Stderr, Since, Until, Tail); } CATCH_RETURN(); @@ -2541,6 +2615,8 @@ HRESULT WSLCContainer::ConnectToNetwork(const WSLCNetworkConnectionOptions* Opti try { COMServiceExecutionContext context; + + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::ConnectToNetwork, Options); } CATCH_RETURN(); @@ -2549,6 +2625,8 @@ HRESULT WSLCContainer::DisconnectFromNetwork(LPCSTR NetworkName) try { COMServiceExecutionContext context; + + auto vmLease = m_session.Runtime().AcquireVmLease(); return CallImpl(&WSLCContainerImpl::DisconnectFromNetwork, NetworkName); } CATCH_RETURN(); diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index 10fd57fd3..5032dd6de 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 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; 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 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 5d75bfed1..62b21ed8c 100644 --- a/src/windows/wslcsession/WSLCExecutionContext.h +++ b/src/windows/wslcsession/WSLCExecutionContext.h @@ -27,7 +27,8 @@ class WSLCExecutionContext : public wsl::windows::common::COMServiceExecutionCon protected: bool CollectUserWarning(const std::wstring& warning) override { - if (m_warningCallback != nullptr) + IWarningCallback* callback = m_warningCallback; + if (callback != nullptr) { std::unique_ptr comCallback; if (m_session != nullptr) @@ -35,7 +36,7 @@ class WSLCExecutionContext : public wsl::windows::common::COMServiceExecutionCon comCallback = std::make_unique(m_session->RegisterUserCOMCallback()); } - auto hr = m_warningCallback->OnWarning(warning.c_str()); + auto hr = callback->OnWarning(warning.c_str()); if (SUCCEEDED(hr) || hr == RPC_E_CALL_CANCELED || hr == HRESULT_FROM_WIN32(ERROR_CANCELLED)) { return true; diff --git a/src/windows/wslcsession/WSLCIdleState.h b/src/windows/wslcsession/WSLCIdleState.h new file mode 100644 index 000000000..712022eec --- /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 1a79525dc..9ce606a37 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 0c0e61eac..8c8da1837 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 ea3b246d9..61cc24d83 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -43,8 +43,42 @@ constexpr auto c_storageVhdFilename = wsl::windows::wslc::DefaultStorageVhdName; constexpr DWORD c_processTerminateTimeoutMs = 30 * 1000; constexpr DWORD c_processKillTimeoutMs = 10 * 1000; +// Default grace period to keep an otherwise-idle VM running before tearing it down (used when the +// session's 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 { +// Validates the target path for a NEW session (one with no existing storage VHD): if the path +// already exists it must be an empty directory, so session storage is never mixed with unrelated +// user files. A non-existent path is fine (it will be created). Enforced eagerly at session +// creation and again when the storage VHD is lazily created. +void ValidateNewSessionStorageDirectory(const std::filesystem::path& StoragePath) +{ + // status's error_code distinguishes "doesn't exist yet" (OK, we'll create it) from other I/O errors. + std::error_code ec; + const auto status = std::filesystem::status(StoragePath, ec); + if (ec && ec.value() != ERROR_FILE_NOT_FOUND && ec.value() != ERROR_PATH_NOT_FOUND) + { + THROW_IF_WIN32_ERROR_MSG(ec.value(), "status failed for %ls", StoragePath.c_str()); + } + + if (!std::filesystem::exists(status)) + { + return; + } + + THROW_HR_WITH_USER_ERROR_IF( + E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeDirectory(StoragePath.c_str()), !std::filesystem::is_directory(status)); + + const bool empty = std::filesystem::is_empty(StoragePath, ec); + THROW_IF_WIN32_ERROR_MSG(ec.value(), "is_empty failed for %ls", StoragePath.c_str()); + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeEmpty(StoragePath.c_str()), !empty); +} + // Group policy: WSLContainerRegistryAllowlist restricts which container-image // registries can be pulled from or pushed to. The check is enforced here at the // service boundary so it covers ALL callers (wslc.exe CLI, the WslcSDK C API, and @@ -332,7 +366,7 @@ HRESULT WSLCSession::Initialize( try { RETURN_HR_IF(E_POINTER, Settings == nullptr || VmFactory == nullptr); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_INITIALIZED), m_virtualMachine.has_value()); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_INITIALIZED), m_vmFactoryGitCookie != 0); THROW_HR_IF_MSG( E_INVALIDARG, WI_IsAnyFlagSet(Settings->FeatureFlags, ~WSLCFeatureFlagsValid), "Invalid feature flags: 0x%x", Settings->FeatureFlags); @@ -343,9 +377,35 @@ try Settings->StorageFlags); // Set up a warning context for the duration of initialization so that non-fatal - // failures (e.g., container/volume/network recovery) are streamed to the CLI. + // failures are streamed to the CLI. WSLCExecutionContext warningContext(this, WarningCallback); + // The VM (and storage VHD) is created lazily on the first operation. Validate the storage + // configuration eagerly here so misconfiguration is reported at session creation rather than + // surfacing later on the first VM-starting operation. + if (Settings->StoragePath != nullptr) + { + const std::filesystem::path storagePath{Settings->StoragePath}; + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Settings->StoragePath), !storagePath.is_absolute()); + + const auto vhdPath = storagePath / c_storageVhdFilename; + std::error_code existsError; + const bool vhdExists = std::filesystem::exists(vhdPath, existsError); + THROW_IF_WIN32_ERROR_MSG(existsError.value(), "exists failed for %ls", vhdPath.c_str()); + + if (WI_IsFlagSet(Settings->StorageFlags, WSLCSessionStorageFlagsNoCreate)) + { + // The storage VHD must already exist (ConfigureStorage will not create it). + THROW_HR_WITH_USER_ERROR_IF( + HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), Localization::MessageWslcSessionStorageNotFound(Settings->StoragePath), !vhdExists); + } + else if (!vhdExists) + { + // New session: the target path (if it exists) must be an empty directory. + ValidateNewSessionStorageDirectory(storagePath); + } + } + // N.B. No locking is required because Initialize() is always called before the session is returned to the caller. m_id = Settings->SessionId; m_displayName = Settings->DisplayName ? Settings->DisplayName : L""; @@ -353,8 +413,16 @@ try m_featureFlags = Settings->FeatureFlags; m_pluginNotifier = PluginNotifier; - // Get user token for the current process + // Park the VM factory in the Global Interface Table. It is supplied here (on the call that + // creates the session) but used on demand from other threads/apartments; storing the raw + // proxy and calling it later would raise RPC_E_WRONG_THREAD. + m_git = wil::CoCreateInstance(CLSID_StdGlobalInterfaceTable, CLSCTX_INPROC_SERVER); + THROW_IF_FAILED(m_git->RegisterInterfaceInGlobal(VmFactory, __uuidof(IWSLCVirtualMachineFactory), &m_vmFactoryGitCookie)); + + // Persist a deep copy of the settings (and the creating user's SID) required to + // (re)create the VM on demand. const auto tokenInfo = wil::get_token_information(GetCurrentProcessToken()); + PersistSettings(*Settings, tokenInfo->User.Sid); WSL_LOG( "SessionInitialized", @@ -362,61 +430,106 @@ try TraceLoggingValue(m_displayName.c_str(), "DisplayName"), TraceLoggingValue(m_creatorProcessName.c_str(), "CreatorProcess")); - // Create the VM through the factory. The VM produces crash events; the session multiplexes - // them out to any registered ICrashDumpCallback subscribers via OnCrashDumpWritten. - wil::com_ptr vm; - THROW_IF_FAILED(VmFactory->CreateVirtualMachine(&vm)); + const auto idleGracePeriod = m_settings.IdleTimeoutSec > 0 ? std::chrono::seconds(m_settings.IdleTimeoutSec) : c_vmIdleGracePeriod; - m_virtualMachine.emplace( - vm.get(), - Settings, - m_sessionTerminatingEvent.get(), - std::bind(&WSLCSession::OnCrashDumpWritten, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); + WSLCSessionRuntime::RuntimeHooks hooks; + hooks.BringUp = [this]() { + // Configure storage. + ConfigureStorage(m_settings, m_userSid.empty() ? nullptr : reinterpret_cast(m_userSid.data())); - // Make sure that everything is destroyed correctly if an exception is thrown. - auto errorCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_FAILED(Terminate()); }); + // Mirror the host's trusted root CAs into the VM before dockerd starts. + InstallTrustedRootCertificates(); - m_virtualMachine->Initialize(); + // Launch containerd first, then dockerd with the external containerd socket. + StartContainerd(); - // Get an event from the service that is signaled when the VM exits. - THROW_IF_FAILED(vm->GetTerminationEvent(&m_vmExitedEvent)); + // Reset the readiness event before (re)starting dockerd so a stale signal from a prior + // VM instance is not observed. + m_runtime.DockerdReadyEvent().ResetEvent(); + StartDockerd(); - // Configure storage. - ConfigureStorage(*Settings, tokenInfo->User.Sid); + m_runtime.InitializeDockerRuntime(m_storageVhdPath.parent_path()); + }; - // Mirror the host's trusted root CAs into the VM before dockerd starts. - InstallTrustedRootCertificates(); + hooks.RecoverState = [this]() { + RecoverExistingNetworks(); + RecoverExistingContainers(); + }; - // Launch containerd first - StartContainerd(); + hooks.TearDownSessionState = [this]() { + std::lock_guard containersLock(m_containersLock); + std::lock_guard networksLock(m_networksLock); - // Launch dockerd with external containerd socket - StartDockerd(); + m_containers.clear(); + m_networks.clear(); + }; - // Wait for dockerd to be ready before starting the event tracker. - THROW_WIN32_IF_MSG( - ERROR_TIMEOUT, !m_dockerdReadyEvent.wait(Settings->BootTimeoutMs), "Timed out waiting for dockerd to start"); + hooks.OnSpontaneousExit = [this]() { LOG_IF_FAILED(Terminate()); }; - auto [_, __, channel] = m_virtualMachine->Fork(WSLC_FORK::Thread); + hooks.OnCrashDump = std::bind( + &WSLCSession::OnCrashDumpWritten, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5); - m_dockerClient.emplace(std::move(channel), m_virtualMachine->TerminatingEvent(), m_virtualMachine->VmId(), 10 * 1000); + 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); - // Start the event tracker. - m_eventTracker.emplace(m_dockerClient.value(), *this, m_ioRelay); + m_runtime.Initialize(m_vmFactoryGitCookie, m_git, &m_settings, idleGracePeriod, std::move(sessionContext), std::move(hooks)); - m_volumes.emplace(m_dockerClient.value(), m_virtualMachine.value(), m_eventTracker.value(), m_storageVhdPath.parent_path()); + return S_OK; +} +CATCH_RETURN() - // Monitor for unexpected VM exit. - m_ioRelay.AddHandle(std::make_unique(m_vmExitedEvent.get(), std::bind(&WSLCSession::OnVmExited, this))); +void WSLCSession::PersistSettings(const WSLCSessionInitSettings& Settings, PSID UserSid) +{ + m_settings = Settings; - // Recover any existing resources from storage. - RecoverExistingNetworks(); - RecoverExistingContainers(); + // Repoint the string fields at storage owned by the session so they outlive the caller's buffers. + m_settings.DisplayName = m_displayName.c_str(); - errorCleanup.release(); - return S_OK; + if (Settings.CreatorProcessName != nullptr) + { + m_settingsCreatorProcessName = Settings.CreatorProcessName; + m_settings.CreatorProcessName = m_settingsCreatorProcessName->c_str(); + } + else + { + m_settings.CreatorProcessName = nullptr; + } + + if (Settings.StoragePath != nullptr) + { + m_settingsStoragePath = Settings.StoragePath; + m_settings.StoragePath = m_settingsStoragePath->c_str(); + } + else + { + m_settings.StoragePath = nullptr; + } + + if (Settings.RootVhdTypeOverride != nullptr) + { + m_settingsRootVhdTypeOverride = Settings.RootVhdTypeOverride; + m_settings.RootVhdTypeOverride = m_settingsRootVhdTypeOverride->c_str(); + } + else + { + m_settings.RootVhdTypeOverride = nullptr; + } + + THROW_HR_IF(E_UNEXPECTED, UserSid == nullptr); + + const auto length = GetLengthSid(UserSid); + const auto* bytes = reinterpret_cast(UserSid); + m_userSid.assign(bytes, bytes + length); +} + +WSLCSession::VmLease WSLCSession::AcquireVmLease() +{ + return m_runtime.AcquireVmLease(); } -CATCH_RETURN() WSLCSession::~WSLCSession() { @@ -440,8 +553,8 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID if (Settings.StoragePath == nullptr) { // If no storage path is specified, use a tmpfs for convenience. - m_virtualMachine->Mount("", c_containerdStorage, "tmpfs", "", 0); - m_storageMounted = true; + m_runtime.Vm().Mount("", c_containerdStorage, "tmpfs", "", 0); + m_runtime.SetStorageMounted(true); return; } @@ -459,7 +572,7 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID { if (diskLun.has_value()) { - m_virtualMachine->DetachDisk(diskLun.value()); + m_runtime.Vm().DetachDisk(diskLun.value()); } LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_storageVhdPath.c_str())); @@ -467,7 +580,7 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID }); auto result = - wil::ResultFromException([&]() { diskDevice = m_virtualMachine->AttachDisk(m_storageVhdPath.c_str(), false).second; }); + wil::ResultFromException([&]() { diskDevice = m_runtime.Vm().AttachDisk(m_storageVhdPath.c_str(), false).second; }); if (FAILED(result)) { @@ -484,23 +597,7 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID WI_IsFlagSet(Settings.StorageFlags, WSLCSessionStorageFlagsNoCreate)); // Reject any non-empty existing path so we don't mix user files with session storage. - // status's error_code distinguishes "doesn't exist yet" (OK, we'll create it) from other I/O errors. - std::error_code ec; - const auto status = std::filesystem::status(storagePath, ec); - if (ec && ec.value() != ERROR_FILE_NOT_FOUND && ec.value() != ERROR_PATH_NOT_FOUND) - { - THROW_IF_WIN32_ERROR_MSG(ec.value(), "status failed for %ls", storagePath.c_str()); - } - - if (std::filesystem::exists(status)) - { - THROW_HR_WITH_USER_ERROR_IF( - E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeDirectory(storagePath.c_str()), !std::filesystem::is_directory(status)); - - const bool empty = std::filesystem::is_empty(storagePath, ec); - THROW_IF_WIN32_ERROR_MSG(ec.value(), "is_empty failed for %ls", storagePath.c_str()); - THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeEmpty(storagePath.c_str()), !empty); - } + ValidateNewSessionStorageDirectory(storagePath); // If the VHD wasn't found, create it. WSL_LOG("CreateStorageVhd", TraceLoggingValue(m_storageVhdPath.c_str(), "StorageVhdPath")); @@ -515,31 +612,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 (...) { @@ -573,7 +671,7 @@ CATCH_RETURN(); void WSLCSession::OnDockerdExited() { - if (!m_sessionTerminatingEvent.is_signaled()) + if (!m_sessionTerminatingEvent.is_signaled() && m_runtime.ExitDispositionAtomic().load() != WSLCSessionRuntime::VmExitDisposition::StopRequested) { WSL_LOG("UnexpectedDockerdExit", TraceLoggingValue(m_displayName.c_str(), "Name")); } @@ -581,24 +679,12 @@ void WSLCSession::OnDockerdExited() void WSLCSession::OnContainerdExited() { - if (!m_sessionTerminatingEvent.is_signaled()) + if (!m_sessionTerminatingEvent.is_signaled() && m_runtime.ExitDispositionAtomic().load() != WSLCSessionRuntime::VmExitDisposition::StopRequested) { WSL_LOG("UnexpectedContainerdExit", TraceLoggingValue(m_displayName.c_str(), "Name")); } } -void WSLCSession::OnVmExited() -{ - WSL_LOG( - "VmExited", - TraceLoggingLevel(WINEVENT_LEVEL_WARNING), - TraceLoggingValue(m_id, "SessionId"), - TraceLoggingValue(m_displayName.c_str(), "Name"), - TraceLoggingValue(!m_sessionTerminatingEvent.is_signaled(), "Unexpected")); - - LOG_IF_FAILED(Terminate()); -} - void WSLCSession::OnProcessLog(const gsl::span& Buffer, PCSTR Source) try { @@ -614,11 +700,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(); } } } @@ -629,15 +715,15 @@ ServiceRunningProcess WSLCSession::StartProcess( { ServiceProcessLauncher launcher{Executable, Args, {{"PATH=/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin"}}}; - auto process = launcher.Launch(*m_virtualMachine); + auto process = launcher.Launch(m_runtime.Vm()); - m_ioRelay.AddHandle(std::make_unique( + m_runtime.Relay()->AddHandle(std::make_unique( process.GetStdHandle(1), [this, LogSource](const auto& data) { OnProcessLog(data, LogSource); }, false)); - m_ioRelay.AddHandle(std::make_unique( + m_runtime.Relay()->AddHandle(std::make_unique( process.GetStdHandle(2), [this, LogSource](const auto& data) { OnProcessLog(data, LogSource); }, false)); - m_ioRelay.AddHandle(std::make_unique(process.GetExitEvent(), std::move(ExitCallback))); + m_runtime.Relay()->AddHandle(std::make_unique(process.GetExitEvent(), std::move(ExitCallback))); return process; } @@ -655,7 +741,8 @@ 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"); } @@ -668,7 +755,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"); } @@ -688,7 +775,7 @@ try const auto script = std::format("cat > '{}'", c_certPath); ServiceProcessLauncher launcher("/bin/sh", {"/bin/sh", "--norc", "-c", script}, {}, WSLCProcessFlagsStdin); - auto process = launcher.Launch(*m_virtualMachine); + auto process = launcher.Launch(m_runtime.Vm()); std::unique_ptr writeStdin( new WriteHandle(process.GetStdHandle(WSLCFDStdin), std::vector{pem.begin(), pem.end()})); @@ -845,8 +932,8 @@ try auto [repo, tagOrDigest] = wslutil::ParseImage(Image); EnforceRegistryAllowlist(repo); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + auto runtime = m_runtime.Acquire(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); if (!tagOrDigest.has_value()) { @@ -860,7 +947,7 @@ try registryAuth = std::string(RegistryAuthenticationInformation); } - auto requestContext = m_dockerClient->PullImage(repo, tagOrDigest, registryAuth); + auto requestContext = runtime.Docker().PullImage(repo, tagOrDigest, registryAuth); StreamImageOperation(*requestContext, Image, "Pull", ProgressCallback); OnImageCreated(Image); @@ -903,16 +990,15 @@ try comCall = RegisterUserCOMCallback(); } - auto lock = m_lock.lock_shared(); + auto runtime = m_runtime.Acquire(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); GUID volumeId{}; THROW_IF_FAILED(CoCreateGuid(&volumeId)); auto mountPath = std::format("/mnt/{}", wsl::shared::string::GuidToString(volumeId)); - THROW_IF_FAILED(m_virtualMachine->MountWindowsFolder(Options->ContextPath, mountPath.c_str(), TRUE)); - auto unmountFolder = - wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { m_virtualMachine->UnmountWindowsFolder(mountPath.c_str()); }); + THROW_IF_FAILED(runtime.Vm().MountWindowsFolder(Options->ContextPath, mountPath.c_str(), TRUE)); + auto unmountFolder = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { runtime.Vm().UnmountWindowsFolder(mountPath.c_str()); }); std::vector buildArgs{"/usr/bin/docker", "build", "--progress=rawjson"}; if (WI_IsFlagSet(Options->Flags, WSLCBuildImageFlagsNoCache)) @@ -957,7 +1043,7 @@ try WSL_LOG("BuildImageStart", TraceLoggingValue(wsl::shared::string::Join(buildArgs, ' ').c_str(), "Command")); ServiceProcessLauncher buildLauncher(buildArgs[0], buildArgs, {}, WSLCProcessFlagsStdin); - auto buildProcess = buildLauncher.Launch(*m_virtualMachine); + auto buildProcess = buildLauncher.Launch(runtime.Vm()); auto io = CreateIOContext(); @@ -1232,11 +1318,11 @@ try { WSLCExecutionContext context(this, WarningCallback); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); - auto requestContext = m_dockerClient->LoadImage(ContentSize); + auto requestContext = m_runtime.Docker().LoadImage(ContentSize); std::ignore = ImportImageImpl(*requestContext, ImageHandle, LoadCallback); @@ -1265,11 +1351,11 @@ try tag = tagOrDigest.value(); } - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); - auto requestContext = m_dockerClient->ImportImage(repo, tag, ContentSize); + auto requestContext = m_runtime.Docker().ImportImage(repo, tag, ContentSize); auto imageId = ImportImageImpl(*requestContext, ImageHandle); THROW_HR_IF_MSG(E_UNEXPECTED, !imageId.has_value(), "Docker import succeeded but did not return an image ID"); @@ -1299,7 +1385,7 @@ std::optional WSLCSession::ImportImageImpl(DockerHTTPClient::HTTPRe comCall = RegisterUserCOMCallback(); } - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); auto io = CreateIOContext(); @@ -1436,11 +1522,11 @@ try RETURN_HR_IF_NULL(E_POINTER, ImageNameOrID); RETURN_HR_IF(E_INVALIDARG, strlen(ImageNameOrID) > WSLC_MAX_IMAGE_NAME_LENGTH); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); - auto retVal = m_dockerClient->SaveImage(ImageNameOrID); + auto retVal = m_runtime.Docker().SaveImage(ImageNameOrID); SaveImageImpl(retVal, OutHandle, CancelEvent); return S_OK; } @@ -1469,11 +1555,11 @@ try names.emplace_back(ImageNames->Values[i]); } - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); - auto retVal = m_dockerClient->SaveImages(names); + auto retVal = m_runtime.Docker().SaveImages(names); SaveImageImpl(retVal, OutHandle, CancelEvent); return S_OK; } @@ -1483,7 +1569,7 @@ void WSLCSession::SaveImageImpl(std::pair& SocketC { auto userHandle = OpenUserHandle(OutputHandle); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); auto io = CreateIOContext(CancelEvent); @@ -1544,14 +1630,14 @@ try filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Options->Filters, Options->FiltersCount); } - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); std::vector images; try { - images = m_dockerClient->ListImages(all, digests, filters); + images = m_runtime.Docker().ListImages(all, digests, filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list images"); @@ -1653,14 +1739,14 @@ try *DeletedImages = nullptr; *Count = 0; - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); std::vector deletedImages; try { - deletedImages = m_dockerClient->DeleteImage( + deletedImages = m_runtime.Docker().DeleteImage( Options->Image, WI_IsFlagSet(Options->Flags, WSLCDeleteImageFlagsForce), WI_IsFlagSet(Options->Flags, WSLCDeleteImageFlagsNoPrune)); } catch (const DockerHTTPException& e) @@ -1727,13 +1813,13 @@ try RETURN_HR_IF_NULL(E_POINTER, Options->Tag); RETURN_HR_IF(E_INVALIDARG, strlen(Options->Repo) + strlen(Options->Tag) + 1 > WSLC_MAX_IMAGE_NAME_LENGTH); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); try { - m_dockerClient->TagImage(Options->Image, Options->Repo, Options->Tag); + m_runtime.Docker().TagImage(Options->Image, Options->Repo, Options->Tag); } catch (const DockerHTTPException& e) { @@ -1764,10 +1850,10 @@ try auto [repo, tagOrDigest] = wslutil::ParseImage(Image); EnforceRegistryAllowlist(repo); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); - auto requestContext = m_dockerClient->PushImage(repo, tagOrDigest, RegistryAuthenticationInformation); + auto requestContext = m_runtime.Docker().PushImage(repo, tagOrDigest, RegistryAuthenticationInformation); StreamImageOperation(*requestContext, Image, "Push", ProgressCallback); return S_OK; @@ -1785,8 +1871,8 @@ try *Output = nullptr; - auto lock = m_lock.lock_shared(); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + auto lock = AcquireVmLease(); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); *Output = wil::make_unique_ansistring(InspectImageLockHeld(ImageNameOrId).c_str()).release(); @@ -1799,7 +1885,7 @@ std::string WSLCSession::InspectImageLockHeld(const std::string& NameOrId) docker_schema::InspectImage dockerInspect; try { - dockerInspect = m_dockerClient->InspectImage(NameOrId); + dockerInspect = m_runtime.Docker().InspectImage(NameOrId); } catch (const DockerHTTPException& e) { @@ -1833,14 +1919,14 @@ try *IdentityToken = nullptr; - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); wil::unique_cotaskmem_ansistring token; try { - auto response = m_dockerClient->Authenticate(ServerAddress, Username, Password); + auto response = m_runtime.Docker().Authenticate(ServerAddress, Username, Password); token = wil::make_unique_ansistring(response.c_str()); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to authenticate with registry: %hs", ServerAddress); @@ -1865,13 +1951,13 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); - auto lock = m_lock.lock_shared(); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + auto lock = AcquireVmLease(); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); docker_schema::PruneImageResult pruneResult; try { - pruneResult = m_dockerClient->PruneImages(filters); + pruneResult = m_runtime.Docker().PruneImages(filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune images"); @@ -1926,7 +2012,7 @@ try "Invalid process flags: 0x%x", containerOptions->InitProcessOptions.Flags); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); auto result = wil::ResultFromException([&]() { CreateContainerImpl(containerOptions, Container); }); @@ -1947,10 +2033,10 @@ CATCH_RETURN(); void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptions, IWSLCContainer** Container) { - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_eventTracker); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasEvents()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes()); // Validate that name & images are valid. if (containerOptions->Name != nullptr && containerOptions->Name[0] != '\0') @@ -1997,14 +2083,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)); @@ -2037,7 +2123,7 @@ try ValidateName(Id, WSLC_MAX_CONTAINER_NAME_LENGTH); // Look for an exact ID match first. - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); std::lock_guard containersLock{m_containersLock}; // Purge containers that were auto-deleted via OnEvent (--rm). @@ -2052,7 +2138,7 @@ try try { - inspectResult = m_dockerClient->InspectContainer(Id); + inspectResult = m_runtime.Docker().InspectContainer(Id); } catch (DockerHTTPException& e) { @@ -2076,6 +2162,77 @@ try } CATCH_RETURN(); +namespace { + + // Activity token holds an activity reference to prevent idle VM teardown while client holds it. + // Implements IFastRundown so crashed clients reclaim stub promptly instead of slow default rundown. + class ContainerOperation + : public Microsoft::WRL::RuntimeClass, IUnknown, IFastRundown> + { + public: + // Adopts an activity reference from CreateActivityToken; callback releases it. + void Initialize(std::function&& onRelease) noexcept + { + m_onRelease = std::move(onRelease); + } + + ~ContainerOperation() override + { + if (m_onRelease) + { + m_onRelease(); + } + } + + private: + std::function m_onRelease; + }; + +} // namespace + +Microsoft::WRL::ComPtr WSLCSession::CreateActivityToken() +{ + // Record the in-flight activity up front so the VM cannot idle-terminate before the caller + // takes ownership of the returned token. + m_runtime.Idle().AddActivity(); + auto countCleanup = wil::scope_exit([this]() { m_runtime.Idle().ReleaseActivity(); }); + + auto operation = Microsoft::WRL::Make(); + THROW_IF_NULL_ALLOC(operation.Get()); + + // Capture shared idle state so token can outlive session and release activity without keeping session alive. + std::shared_ptr idleState = m_runtime.IdleStateShared(); + operation->Initialize([idleState = std::move(idleState)]() { idleState->ReleaseActivity(); }); + + // The token now owns the activity-count reference and will release it on destruction. + countCleanup.release(); + + Microsoft::WRL::ComPtr token; + THROW_IF_FAILED(operation.As(&token)); + return token; +} + +HRESULT WSLCSession::BeginContainerOperation(IUnknown** Operation) +try +{ + WSLCExecutionContext context(this); + + RETURN_HR_IF_NULL(E_POINTER, Operation); + *Operation = nullptr; + + // Do not start a new operation (which would hold the VM alive) once the session is terminating + // or has terminated. Mirrors the gate in EnsureVmRunning(). + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating.load() || m_sessionTerminatedEvent.is_signaled()); + + // Record the in-flight operation up front so the VM cannot idle-terminate before the client + // resolves the container and issues the operation (and streams any output). + auto token = CreateActivityToken(); + + RETURN_IF_FAILED(token.CopyTo(Operation)); + return S_OK; +} +CATCH_RETURN(); + HRESULT WSLCSession::ListContainers( const WSLCListContainersOptions* Options, WSLCContainerEntry** Containers, ULONG* Count, WSLCContainerPortMapping** Ports, ULONG* PortsCount) try @@ -2110,13 +2267,13 @@ try filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Options->Filters, Options->FiltersCount); } - auto lock = m_lock.lock_shared(); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + auto lock = AcquireVmLease(); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); std::vector dockerContainers; try { - dockerContainers = m_dockerClient->ListContainers(all, limit, filters); + dockerContainers = m_runtime.Docker().ListContainers(all, limit, filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list containers"); @@ -2189,8 +2346,8 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); - auto lock = m_lock.lock_shared(); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + auto lock = AcquireVmLease(); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); std::lock_guard containersLock{m_containersLock}; @@ -2198,7 +2355,7 @@ try try { - pruneResult = m_dockerClient->PruneContainers(filters); + pruneResult = m_runtime.Docker().PruneContainers(filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune containers"); @@ -2260,10 +2417,18 @@ try *Errno = -1; // Make sure not to return 0 if something fails. } - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + auto runtime = m_runtime.Acquire(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); + + auto process = runtime.Vm().CreateLinuxProcess(Executable, *Options, TtyRows, TtyColumns, Errno); + + // The VmLease above is released when this call returns, but the process keeps running in the + // VM and the client holds the returned proxy. A root-namespace process is not tracked as a + // container, so attach an activity token bound to the process's lifetime; this keeps the VM + // alive for as long as the client holds the process, preventing the idle worker from tearing + // the VM down and killing the process out from under the client. + process->SetKeepAliveToken(CreateActivityToken()); - auto process = m_virtualMachine->CreateLinuxProcess(Executable, *Options, TtyRows, TtyColumns, Errno); THROW_IF_FAILED(process.CopyTo(Process)); return S_OK; @@ -2274,7 +2439,7 @@ void WSLCSession::Ext4Format(const std::string& Device) { constexpr auto mkfsPath = "/usr/sbin/mkfs.ext4"; ServiceProcessLauncher launcher(mkfsPath, {mkfsPath, Device}); - auto result = launcher.Launch(*m_virtualMachine).WaitAndCaptureOutput(); + auto result = launcher.Launch(m_runtime.Vm()).WaitAndCaptureOutput(); THROW_HR_IF_MSG(E_FAIL, result.Code != 0, "%hs", launcher.FormatResult(result).c_str()); } @@ -2286,17 +2451,17 @@ try THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Path), !std::filesystem::path(Path).is_absolute()); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); // Attach the disk to the VM (AttachDisk() performs the access check for the VHD file). - auto [lun, device] = m_virtualMachine->AttachDisk(Path, false); + auto [lun, device] = m_runtime.Vm().AttachDisk(Path, false); // N.B. DetachDisk calls sync() before detaching. - auto detachDisk = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, lun]() { m_virtualMachine->DetachDisk(lun); }); + auto detachDisk = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, lun]() { m_runtime.Vm().DetachDisk(lun); }); // Format it to ext4. - m_virtualMachine->Ext4Format(device); + m_runtime.Vm().Ext4Format(device); return S_OK; } @@ -2314,15 +2479,15 @@ try auto driverOpts = wslutil::ParseKeyValuePairs(Options->DriverOpts, Options->DriverOptsCount); auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCVolumeMetadataLabel); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes()); if (Options->Name != nullptr && Options->Name[0] != '\0') { ValidateName(Options->Name, WSLC_MAX_VOLUME_NAME_LENGTH); } - *VolumeInfo = m_volumes->CreateVolume(Options->Name, Options->Driver, std::move(driverOpts), std::move(labels)); + *VolumeInfo = m_runtime.Volumes().CreateVolume(Options->Name, Options->Driver, std::move(driverOpts), std::move(labels)); return S_OK; } CATCH_RETURN(); @@ -2334,10 +2499,10 @@ try RETURN_HR_IF_NULL(E_POINTER, Name); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes()); - m_volumes->DeleteVolume(Name); + m_runtime.Volumes().DeleteVolume(Name); return S_OK; } CATCH_RETURN(); @@ -2355,10 +2520,10 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes()); - auto volumeList = m_volumes->ListVolumes(std::move(filters)); + auto volumeList = m_runtime.Volumes().ListVolumes(std::move(filters)); if (volumeList.empty()) { @@ -2387,10 +2552,10 @@ try std::string name = Name; ValidateName(name.c_str(), WSLC_MAX_VOLUME_NAME_LENGTH); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes()); - std::string json = m_volumes->InspectVolume(name); + std::string json = m_runtime.Volumes().InspectVolume(name); *Output = wil::make_unique_ansistring(json.c_str()).release(); return S_OK; @@ -2412,13 +2577,13 @@ try auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes()); WSLCVolumes::PruneVolumesResult pruneResult; try { - pruneResult = m_volumes->PruneVolumes(filters); + pruneResult = m_runtime.Volumes().PruneVolumes(filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune volumes"); @@ -2444,32 +2609,6 @@ try } CATCH_RETURN(); -int WSLCSession::StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs) -{ - auto signalResult = Process.Get().Signal(WSLCSignalSIGTERM); - if (FAILED(signalResult)) - { - LOG_HR_MSG(signalResult, "Failed to terminate process %i", Process.Get().GetPid()); - return -1; - } - - try - { - return Process.Wait(TerminateTimeoutMs); - } - catch (...) - { - LOG_CAUGHT_EXCEPTION(); - try - { - LOG_IF_FAILED(Process.Get().Signal(WSLCSignalSIGKILL)); - return Process.Wait(KillTimeoutMs); - } - CATCH_LOG(); - } - - return -1; -} // Network management. HRESULT WSLCSession::CreateNetwork(const WSLCNetworkOptions* Options, IWarningCallback* WarningCallback) @@ -2491,9 +2630,9 @@ try auto driverOpts = wslutil::ParseKeyValuePairs(Options->DriverOpts, Options->DriverOptsCount); auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCNetworkManagedLabel); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); std::lock_guard networksLock(m_networksLock); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), m_networks.contains(name)); @@ -2532,7 +2671,7 @@ try docker_schema::CreateNetworkResponse createResult; try { - createResult = m_dockerClient->CreateNetwork(request); + createResult = m_runtime.Docker().CreateNetwork(request); } catch (const DockerHTTPException& e) { @@ -2546,14 +2685,15 @@ try EMIT_USER_WARNING(wsl::shared::string::MultiByteToWide(createResult.Warning)); } - auto removeNetworkCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, &name]() { m_dockerClient->RemoveNetwork(name); }); + auto removeNetworkCleanup = + wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, &name]() { m_runtime.Docker().RemoveNetwork(name); }); // Inspect the newly created network to cache full properties (IPAM, Scope, etc.) // since CreateNetworkResponse only returns {Id, Warning}. docker_schema::Network full; try { - full = m_dockerClient->InspectNetwork(name); + full = m_runtime.Docker().InspectNetwork(name); } catch (const DockerHTTPException& e) { @@ -2600,9 +2740,9 @@ try std::string name = Name; ValidateName(name.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); std::lock_guard networksLock(m_networksLock); @@ -2611,7 +2751,7 @@ try try { - m_dockerClient->RemoveNetwork(name); + m_runtime.Docker().RemoveNetwork(name); } catch (const DockerHTTPException& e) { @@ -2640,7 +2780,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 +2819,7 @@ try std::string name = Name; ValidateName(name.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH); - auto lock = m_lock.lock_shared(); + auto lock = AcquireVmLease(); std::lock_guard networksLock(m_networksLock); auto it = m_networks.find(name); @@ -2734,16 +2874,16 @@ try // Scope the prune to WSLC-managed networks. filters["label"].push_back(WSLCNetworkManagedLabel); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); std::lock_guard networksLock(m_networksLock); docker_schema::PruneNetworkResult pruneResult; try { - pruneResult = m_dockerClient->PruneNetworks(filters); + pruneResult = m_runtime.Docker().PruneNetworks(filters); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune networks"); @@ -2814,10 +2954,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; @@ -2839,8 +2979,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()) { @@ -2860,101 +3000,20 @@ 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); - - std::lock_guard containersLock(m_containersLock); - std::lock_guard networksLock(m_networksLock); - - m_containers.clear(); - m_volumes.reset(); - m_networks.clear(); - - // Stop the IO relay. - // This stops: - // - container state monitoring. - // - container init process relays - // - execs relays - // - container logs relays - m_ioRelay.Stop(); - - { - std::lock_guard allocatedPortsLock(m_allocatedPortsLock); - m_allocatedPorts.clear(); - } - - m_eventTracker.reset(); - m_dockerClient.reset(); - - // Check if the VM has already exited (e.g., killed externally). - // If so, skip operations that require a live VM to avoid unnecessary waits. - // N.B. m_vmExitedEvent may be uninitialized if Terminate() is called from the - // Initialize() error path before GetTerminationEvent() succeeds. - if (m_vmExitedEvent && m_vmExitedEvent.is_signaled()) - { - WSL_LOG("SkippingGracefulShutdown_VmDead", TraceLoggingValue(m_id, "SessionId")); - - // The VM exited on its own, so it recorded the cause. - if (m_virtualMachine) - { - wil::unique_cotaskmem_string details; - LOG_IF_FAILED(m_virtualMachine->GetTerminationReason(&m_terminationReason, &details)); - m_terminationDetails = details ? details.get() : L""; - } - } - else - { - // The VM is still alive, so this is a graceful shutdown initiated by us. - m_terminationReason = WSLCVirtualMachineTerminationReasonShutdown; - - if (m_virtualMachine) - { - m_virtualMachine->OnSessionTerminated(); - - // Stop dockerd first, then containerd (dockerd is a client of containerd). - // N.B. dockerd waits a couple seconds if there are any outstanding HTTP request sockets opened. - if (m_dockerdProcess.has_value()) - { - auto dockerdExitCode = StopProcess(m_dockerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs); - WSL_LOG("DockerdExit", TraceLoggingValue(dockerdExitCode, "code")); - } - - if (m_containerdProcess.has_value()) - { - auto containerdExitCode = StopProcess(m_containerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs); - WSL_LOG("ContainerdExit", TraceLoggingValue(containerdExitCode, "code")); - } + m_runtime.Shutdown(sessionLock, m_terminationReason, m_terminationDetails); - // N.B. dockerd has exited by this point, so unmounting the VHD is safe since no container can be running. - if (m_storageMounted) - { - try - { - m_virtualMachine->Unmount(c_containerdStorage); - m_storageMounted = false; - } - CATCH_LOG(); - } - } - } - - m_dockerdProcess.reset(); - m_containerdProcess.reset(); - m_virtualMachine.reset(); - - // Delete the ephemeral swap VHD now that the VM is gone. - if (!m_swapVhdPath.empty()) + // 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) { - LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_swapVhdPath.c_str())); - m_swapVhdPath.clear(); + LOG_IF_FAILED(m_git->RevokeInterfaceFromGlobal(m_vmFactoryGitCookie)); + m_vmFactoryGitCookie = 0; } - m_sessionTerminatedEvent.SetEvent(); - return S_OK; } CATCH_RETURN(); @@ -3022,10 +3081,10 @@ try RETURN_HR_IF_NULL(E_POINTER, WindowsPath); RETURN_HR_IF_NULL(E_POINTER, LinuxPath); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); - return m_virtualMachine->MountWindowsFolder(WindowsPath, LinuxPath, ReadOnly); + return m_runtime.Vm().MountWindowsFolder(WindowsPath, LinuxPath, ReadOnly); } CATCH_RETURN(); @@ -3036,10 +3095,10 @@ try RETURN_HR_IF_NULL(E_POINTER, LinuxPath); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); - return m_virtualMachine->UnmountWindowsFolder(LinuxPath); + return m_runtime.Vm().UnmountWindowsFolder(LinuxPath); } CATCH_RETURN(); @@ -3048,36 +3107,37 @@ try { WSLCExecutionContext context(this); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); - std::lock_guard allocatedPortsLock(m_allocatedPortsLock); + std::lock_guard allocatedPortsLock(m_runtime.AllocatedPortsLock()); // Look for an existing allocation first. - auto it = m_allocatedPorts.find(LinuxPort); + auto& allocatedPorts = m_runtime.AllocatedPorts(); + auto it = allocatedPorts.find(LinuxPort); bool inserted = false; auto cleanup = wil::scope_exit([&]() { if (inserted) { - m_allocatedPorts.erase(it); + allocatedPorts.erase(it); } }); - if (it == m_allocatedPorts.end()) + if (it == allocatedPorts.end()) { // No existing port allocation, create a new one. - auto allocated = std::make_pair(m_virtualMachine->TryAllocatePort(LinuxPort, Family, IPPROTO_TCP), static_cast(0)); + auto allocated = std::make_pair(m_runtime.Vm().TryAllocatePort(LinuxPort, Family, IPPROTO_TCP), static_cast(0)); THROW_HR_IF(HRESULT_FROM_WIN32(WSAEADDRINUSE), allocated.first == nullptr); - it = m_allocatedPorts.emplace(LinuxPort, allocated).first; + it = allocatedPorts.emplace(LinuxPort, allocated).first; inserted = true; } auto mapping = VMPortMapping::LocalhostTcpMapping(Family, WindowsPort); mapping.AssignVmPort(it->second.first); - m_virtualMachine->MapPort(mapping); + m_runtime.Vm().MapPort(mapping); // Increase usage count. it->second.second++; @@ -3094,28 +3154,29 @@ try { WSLCExecutionContext context(this); - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); + auto lock = AcquireVmLease(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); - std::lock_guard allocatedPortsLock(m_allocatedPortsLock); + std::lock_guard allocatedPortsLock(m_runtime.AllocatedPortsLock()); - auto it = m_allocatedPorts.find(LinuxPort); - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == m_allocatedPorts.end()); + auto& allocatedPorts = m_runtime.AllocatedPorts(); + auto it = allocatedPorts.find(LinuxPort); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == allocatedPorts.end()); auto mapping = VMPortMapping::LocalhostTcpMapping(Family, WindowsPort); mapping.AssignVmPort(it->second.first); - mapping.Attach(m_virtualMachine.value()); + mapping.Attach(m_runtime.Vm()); auto cleanup = wil::scope_exit([&]() { mapping.Release(); }); - m_virtualMachine->UnmapPort(mapping); + m_runtime.Vm().UnmapPort(mapping); it->second.second--; // If usage count drops to 0, release the port allocation. if (it->second.second == 0) { - m_allocatedPorts.erase(it); + allocatedPorts.erase(it); } return S_OK; @@ -3408,7 +3469,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); @@ -3457,11 +3522,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) { @@ -3470,13 +3535,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); @@ -3497,10 +3562,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 aa86d47cb..8b2c7332f 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -18,12 +18,16 @@ Module Name: #include "WSLCCompat.h" #include "WSLCVirtualMachine.h" #include "WSLCContainer.h" +#include "WSLCIdleState.h" #include "WSLCVolumes.h" +#include "WSLCSessionRuntime.h" #include "WSLCNetworkMetadata.h" #include "DockerEventTracker.h" #include "DockerHTTPClient.h" #include "IORelay.h" +#include #include +#include #include namespace wsl::windows::service::wslc { @@ -71,13 +75,20 @@ class UserCOMCallback // // WSLCSession - Implements IWSLCSession for container management. // Runs in a per-user COM server process for security isolation. -// The SYSTEM service creates the VM and passes IWSLCVirtualMachine to Initialize(). +// The SYSTEM service passes an IWSLCVirtualMachineFactory to Initialize(); the VM is created +// lazily on first use and may be torn down when idle and recreated on demand. // class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession : public Microsoft::WRL::RuntimeClass, IWSLCSession, IWSLCCompatSession, IFastRundown, ISupportErrorInfo> { + // WSLCContainer::Delete acquires a VmLease to keep the VM alive (and block idle + // teardown) for the duration of a container deletion. + friend class WSLCContainer; + public: - WSLCSession() = default; + WSLCSession() : m_runtime(*this) + { + } ~WSLCSession(); @@ -143,6 +154,7 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession // Container management. IFACEMETHOD(CreateContainer)(_In_ const WSLCContainerOptions* Options, _In_opt_ IWarningCallback* WarningCallback, _Out_ IWSLCContainer** Container) override; IFACEMETHOD(OpenContainer)(_In_ LPCSTR Id, _In_ IWSLCContainer** Container) override; + IFACEMETHOD(BeginContainerOperation)(_Outptr_ IUnknown** Operation) override; IFACEMETHOD(ListContainers)( _In_opt_ const WSLCListContainersOptions* Options, _Out_ WSLCContainerEntry** Containers, @@ -259,33 +271,57 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession bool WaitForEventOrSessionTerminating(HANDLE Event, std::chrono::milliseconds Timeout) const; + // Shared idle-termination state. Exposed so VM-scoped objects (e.g. running containers via + // WSLCContainerImpl's ActivityRef) can hold an activity reference for their lifetime without + // keeping the session object itself alive. + std::shared_ptr IdleStateShared() const noexcept + { + return m_runtime.IdleStateShared(); + } + + WSLCSessionRuntime& Runtime() noexcept + { + return m_runtime; + } + + const WSLCSessionRuntime& Runtime() const noexcept + { + return m_runtime; + } + + // Creates an opaque activity token that holds a reference on this session's activity count for + // its lifetime, deferring idle teardown of the VM until every outstanding token is released. + // Used both for transient client operations (BeginContainerOperation) and to keep the VM alive + // for the lifetime of a process whose wrapper a client may keep (root-namespace and exec'd + // processes). + Microsoft::WRL::ComPtr CreateActivityToken(); + private: ULONG m_id = 0; + void PersistSettings(const WSLCSessionInitSettings& Settings, PSID UserSid); + + using VmLease = WSLCSessionRuntime::VmLease; + [[nodiscard]] VmLease AcquireVmLease(); __requires_lock_held(m_userHandlesLock) void CancelUserHandleIO(); __requires_lock_held(m_userCOMCallbacksLock) void CancelUserCOMCallbacks(); - _Requires_shared_lock_held_(m_lock) void CreateContainerImpl(const WSLCContainerOptions* Options, IWSLCContainer** Container); void ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID UserSid); void Ext4Format(const std::string& Device); - _Requires_shared_lock_held_(m_lock) std::string InspectImageLockHeld(const std::string& Id); void OnContainerDeleted(const WSLCContainerImpl* Container); void OnCrashDumpWritten(const std::wstring& DumpPath, const std::string& ProcessName, ULONG Pid, ULONG Signal, ULONGLONG Timestamp); - _Requires_shared_lock_held_(m_lock) void OnImageCreated(const std::string& ImageNameOrId) noexcept; - _Requires_shared_lock_held_(m_lock) void OnImageDeleted(const std::string& ImageId) noexcept; void OnProcessLog(const gsl::span& Data, PCSTR Source); void OnContainerdExited(); void OnDockerdExited(); - void OnVmExited(); ServiceRunningProcess StartProcess( const std::string& Executable, const std::vector& Args, PCSTR LogSource, std::function&& ExitCallback); void InstallTrustedRootCertificates(); @@ -300,35 +336,41 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession void SaveImageImpl(std::pair& RequestCodePair, WSLCHandle OutputHandle, HANDLE CancelEvent); void StreamImageOperation(DockerHTTPClient::HTTPRequestContext& requestContext, LPCSTR Image, LPCSTR OperationName, IProgressCallback* ProgressCallback); - std::optional m_dockerClient; - std::optional m_virtualMachine; - std::optional m_eventTracker; - wil::unique_event m_dockerdReadyEvent{wil::EventOptions::ManualReset}; + // The VM factory is a cross-process proxy supplied by the SYSTEM service at Initialize() time + // but first used later (on demand) from a different thread/apartment. A directly stored proxy + // would fail with RPC_E_WRONG_THREAD, so it is parked in the process Global Interface Table and + // re-fetched (re-marshalled into the calling apartment) each time a VM is created. + wil::com_ptr m_git; + DWORD m_vmFactoryGitCookie{}; + + WSLCSessionRuntime m_runtime; std::wstring m_displayName; std::wstring m_creatorProcessName; std::filesystem::path m_storageVhdPath; - std::filesystem::path m_swapVhdPath; - bool m_storageMounted = false; - // N.B. m_lock must be acquired before acquiring m_containersLock or m_networksLock. - // These locks protect m_containers without requiring an exclusive m_lock. + // N.B. 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; - IORelay m_ioRelay; - std::optional m_containerdProcess; - std::optional m_dockerdProcess; + + // Persisted settings required to (re)create the VM on demand. The string fields point + // into the owned storage members below (or m_displayName) so they remain valid for the + // lifetime of the session. + WSLCSessionInitSettings m_settings{}; + std::optional m_settingsCreatorProcessName; + std::optional m_settingsStoragePath; + std::optional m_settingsRootVhdTypeOverride; + std::vector m_userSid; + WSLCFeatureFlags m_featureFlags{}; std::function m_destructionCallback; std::atomic m_terminating{false}; @@ -347,14 +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 000000000..0f50c7ceb --- /dev/null +++ b/src/windows/wslcsession/WSLCSessionRuntime.cpp @@ -0,0 +1,660 @@ +/*++ + +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. + if (m_storageMounted) + { + try + { + m_virtualMachine->Unmount(c_containerdStorage); + } + CATCH_LOG(); + } + } + + m_dockerdProcess.reset(); + m_containerdProcess.reset(); + m_virtualMachine.reset(); + m_storageMounted = false; + + // Destroy the relay unless we're on its own thread (~IORelay joins the thread, which would + // deadlock). On unexpected-VM-exit path (runs on relay thread), leave it for ~WSLCSession. + if (!m_ioRelay || !m_ioRelay->IsRelayThread()) + { + m_ioRelay.reset(); + m_vmExitedEvent.reset(); + } + + // Delete the ephemeral swap VHD now that the VM is gone. + if (!m_swapVhdPath.empty()) + { + LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_swapVhdPath.c_str())); + m_swapVhdPath.clear(); + } +} + +void WSLCSessionRuntime::OnIdleTimer() +try +{ + // Idle teardown releases cross-process COM proxies (the VM and its VM-scoped state), so this + // threadpool callback must join the process MTA; otherwise those Release/calls fail with + // RPC_E_WRONG_THREAD. The function-try-block keeps this (and everything below) under CATCH_LOG: + // the threadpool callback that invokes us is noexcept, so an escaping throw would terminate. + const auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED); + + if (m_terminating->load() || !IdleTerminationEnabled()) + { + return; + } + + // Non-blocking acquire: a blocking exclusive would queue behind in-flight operations, and + // SRW locks favor waiting writers, stalling all new ops. If the lock is held, an operation + // is in flight; it holds an activity reference and will re-arm the timer (via the 1->0 + // transition) when it releases, so there is nothing to do here. + auto lock = m_lock.try_lock_exclusive(); + if (!lock) + { + return; + } + + // Re-check every teardown precondition under the lock. The activity count is the single + // source of truth for "the VM is needed"; a 0->1 transition since the timer fired (cancel + // raced the callback) is caught here. + if (m_terminating->load() || m_vmState.load() != VmState::Running || m_idleState->ActivityCount() != 0) + { + return; + } + + // Claim the stop. If we lose, OnVmExited() owns a spontaneous-exit teardown and is spinning for + // this lock, so release it and let that run instead of joining the relay ourselves. + if (!TryClaimExpectedStop()) + { + return; + } + + // Restore Active on completion (or early exit) so the next StartVmLockHeld starts clean; only + // clear our own claim. + auto dispositionCleanup = wil::scope_exit([this]() { + auto stopRequested = VmExitDisposition::StopRequested; + m_vmExitDisposition.compare_exchange_strong(stopRequested, VmExitDisposition::Active); + }); + + 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 000000000..2cfd9e896 --- /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 diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 94fb0ed26..c8f315a8b 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,13 +11491,18 @@ class WSLCTests VERIFY_SUCCEEDED(sessionManager2->CreateSession(&settings2, WSLCSessionFlagsNone, warningCallback.Get(), &session2)); wsl::windows::common::security::ConfigureForCOMImpersonation(session2.get()); - // Verify the warning matches the expected localized message for the corrupt container. + // The VM (and container recovery) starts lazily on the first operation. Trigger it so + // recovery runs. + VERIFY_IS_TRUE(WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "exit 0"}).Launch(*session2).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( + 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()); } @@ -11555,12 +11564,17 @@ class WSLCTests VERIFY_SUCCEEDED(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, warningCallback.Get(), &session)); wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); - // Verify the warning matches the expected localized message for the missing volume. + // The VM (and volume recovery) starts lazily on the first operation. Trigger it so + // 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 = + 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")); @@ -11620,10 +11634,16 @@ 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. + 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/WSLCE2ESessionEnterTests.cpp b/test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp index c04c4b767..c512452cf 100644 --- a/test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp @@ -109,9 +109,14 @@ class WSLCE2ESessionEnterTests WSLC_TEST_METHOD(WSLCE2E_SessionEnter_StoragePathNotFound) { auto result = RunWslc(L"system session enter does-not-exist"); - const auto expectedPath = std::filesystem::absolute(L"does-not-exist").wstring(); + + // The CLI resolves the storage argument to an absolute path (see EnterSession task) and the + // service validates it eagerly at session creation, reporting the friendly "No WSLC session + // found in ''" message rather than a bare system error. + const auto storagePath = std::filesystem::absolute(L"does-not-exist").wstring(); result.Verify({ - .Stderr = std::format(L"No WSLC session found in '{}'\r\nError code: ERROR_PATH_NOT_FOUND\r\n", expectedPath), + .Stderr = wsl::shared::Localization::MessageWslcSessionStorageNotFound(storagePath) + + L"\r\nError code: ERROR_PATH_NOT_FOUND\r\n", .ExitCode = 1, }); } diff --git a/test/windows/wslc/e2e/WSLCE2EWarningTests.cpp b/test/windows/wslc/e2e/WSLCE2EWarningTests.cpp index 81afbef07..a488dcddc 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); + } } };