| 1 | /*++ |
| 2 | |
| 3 | Copyright (c) Microsoft. All rights reserved. |
| 4 | |
| 5 | Module Name: |
| 6 | |
| 7 | WSLCSessionRuntime.h |
| 8 | |
| 9 | Abstract: |
| 10 | |
| 11 | Contains the definition for WSLCSessionRuntime. |
| 12 | |
| 13 | --*/ |
| 14 | |
| 15 | #pragma once |
| 16 | |
| 17 | #include "wslc.h" |
| 18 | #include "WSLCVirtualMachine.h" |
| 19 | #include "WSLCVolumes.h" |
| 20 | #include "WSLCIdleState.h" |
| 21 | #include "DockerEventTracker.h" |
| 22 | #include "DockerHTTPClient.h" |
| 23 | #include "IORelay.h" |
| 24 | #include "ServiceProcessLauncher.h" |
| 25 | #include <atomic> |
| 26 | #include <chrono> |
| 27 | #include <filesystem> |
| 28 | #include <functional> |
| 29 | #include <map> |
| 30 | #include <memory> |
| 31 | #include <mutex> |
| 32 | #include <optional> |
| 33 | #include <string> |
| 34 | |
| 35 | namespace wsl::windows::service::wslc { |
| 36 | |
| 37 | class WSLCSession; |
| 38 | |
| 39 | class WSLCSessionRuntime |
| 40 | { |
| 41 | public: |
| 42 | enum class VmState |
| 43 | { |
| 44 | None, |
| 45 | Starting, |
| 46 | Running, |
| 47 | Stopping, |
| 48 | }; |
| 49 | |
| 50 | enum class VmExitDisposition |
| 51 | { |
| 52 | Active, |
| 53 | StopRequested, |
| 54 | ExitClaimed, |
| 55 | }; |
| 56 | |
| 57 | struct RuntimeHooks |
| 58 | { |
| 59 | std::function<void()> BringUp; |
| 60 | std::function<void()> RecoverState; |
| 61 | // Invoked while tearing down the VM, with the VM-scoped state still alive. The argument is |
| 62 | // true only for a permanent session shutdown (not an idle teardown): on idle teardown the |
| 63 | // container wrappers must be kept alive so client COM references stay valid and are reused |
| 64 | // when the VM restarts. |
| 65 | std::function<void(bool permanent)> TearDownSessionState; |
| 66 | std::function<void()> OnSpontaneousExit; |
| 67 | WSLCVirtualMachine::TOnCrashDump OnCrashDump; |
| 68 | |
| 69 | // Fired when a VM has started (best-effort). Invoked without the runtime lock held so the |
| 70 | // handler may call back into the session (e.g. to run setup in the VM). Fired every time a |
| 71 | // VM is (re)created for the session. |
| 72 | std::function<void()> OnVmStarted; |
| 73 | |
| 74 | // Fired when a VM is about to be torn down (best-effort), while the VM is still alive. Invoked |
| 75 | // without the runtime lock held so the handler may call back into the session or into a plugin |
| 76 | // that acquires a VM lease. Fires exactly once per OnVmStarted -- on both idle and permanent |
| 77 | // teardown -- and the teardown always follows: once this is raised the VM is committed to |
| 78 | // stopping, and any lease that is not part of this callback waits for the next VM. On a |
| 79 | // permanent teardown the session is terminating, so a callback that resolves this session |
| 80 | // (e.g. to create a process) fails cleanly instead of restarting the VM. |
| 81 | std::function<void()> OnVmStopping; |
| 82 | }; |
| 83 | |
| 84 | struct SessionContext |
| 85 | { |
| 86 | ULONG Id{}; |
| 87 | std::wstring DisplayName; |
| 88 | const std::atomic<bool>* Terminating{}; |
| 89 | wil::shared_event SessionTerminatingEvent; |
| 90 | wil::shared_event SessionTerminatedEvent; |
| 91 | }; |
| 92 | |
| 93 | // Whether a lease may bring a VM up, or must be served by whatever VM is already running. |
| 94 | // |
| 95 | // Acquire is correct for every ordinary caller: it starts the VM if there is none, and because an |
| 96 | // announced stop always happens, a VM with one pending is unusable even though it is still |
| 97 | // running -- the lease waits for the teardown and is then served by a fresh VM. |
| 98 | // |
| 99 | // ExistingOnly is for plugins. A plugin call is a side effect of the session's own activity, never |
| 100 | // a reason to create a VM, so it neither starts one nor waits for a teardown: it is served by the |
| 101 | // running VM, including one committed to stopping, and fails with WSLC_E_VM_NOT_RUNNING when there |
| 102 | // is none. Waiting is not an option for the calls that matter -- a plugin reentering from its |
| 103 | // OnWslcVmStopping handler is the reason the teardown is blocked, so it would deadlock against |
| 104 | // itself. |
| 105 | enum class VmLeasePolicy |
| 106 | { |
| 107 | Acquire, |
| 108 | ExistingOnly, |
| 109 | }; |
| 110 | |
| 111 | class VmLease |
| 112 | { |
| 113 | public: |
| 114 | VmLease() = default; |
| 115 | explicit VmLease(WSLCSessionRuntime& Runtime, VmLeasePolicy Policy = VmLeasePolicy::Acquire); |
| 116 | VmLease(VmLease&& Other) noexcept; |
| 117 | VmLease& operator=(VmLease&& Other) noexcept; |
| 118 | ~VmLease(); |
| 119 | |
| 120 | VmLease(const VmLease&) = delete; |
| 121 | VmLease& operator=(const VmLease&) = delete; |
| 122 | |
| 123 | private: |
| 124 | WSLCSessionRuntime* m_runtime{}; |
| 125 | wil::rwlock_release_shared_scope_exit m_lock; |
| 126 | }; |
| 127 | |
| 128 | class LockedRuntime |
| 129 | { |
| 130 | public: |
| 131 | LockedRuntime() = default; |
| 132 | explicit LockedRuntime(WSLCSessionRuntime& Runtime, VmLeasePolicy Policy = VmLeasePolicy::Acquire); |
| 133 | |
| 134 | WSLCVirtualMachine& Vm(); |
| 135 | IORelay* Relay(); |
| 136 | DockerHTTPClient& Docker(); |
| 137 | |
| 138 | private: |
| 139 | WSLCSessionRuntime* m_runtime{}; |
| 140 | VmLease m_lease; |
| 141 | }; |
| 142 | |
| 143 | explicit WSLCSessionRuntime(WSLCSession& Session) noexcept; |
| 144 | |
| 145 | void Initialize( |
| 146 | DWORD vmFactoryGitCookie, |
| 147 | wil::com_ptr<IGlobalInterfaceTable> git, |
| 148 | const WSLCSessionInitSettings* settings, |
| 149 | std::chrono::milliseconds idleGrace, |
| 150 | SessionContext sessionContext, |
| 151 | RuntimeHooks hooks); |
| 152 | |
| 153 | WSLCVirtualMachine& Vm(); |
| 154 | bool HasVm() const noexcept; |
| 155 | IORelay* Relay(); |
| 156 | bool HasRelay() const noexcept; |
| 157 | DockerHTTPClient& Docker(); |
| 158 | bool HasDocker() const noexcept; |
| 159 | DockerEventTracker& Events(); |
| 160 | bool HasEvents() const noexcept; |
| 161 | WSLCVolumes& Volumes(); |
| 162 | bool HasVolumes() const noexcept; |
| 163 | [[nodiscard]] wil::rwlock_release_exclusive_scope_exit TryLockExclusive() noexcept; |
| 164 | IdleState& Idle() noexcept; |
| 165 | std::shared_ptr<IdleState> IdleStateShared() const noexcept; |
| 166 | VmState State() const noexcept; |
| 167 | VmExitDisposition ExitDisposition() const noexcept; |
| 168 | bool VmExited() const noexcept; |
| 169 | void ResetDockerdReady() noexcept; |
| 170 | void OnProcessLog(const gsl::span<char>& buffer, PCSTR source) noexcept; |
| 171 | void SetContainerdProcess(ServiceRunningProcess&& process); |
| 172 | void SetDockerdProcess(ServiceRunningProcess&& process); |
| 173 | |
| 174 | void SetSwapVhdPath(std::filesystem::path path); |
| 175 | void SetStorageMounted(bool value) noexcept; |
| 176 | |
| 177 | std::mutex& AllocatedPortsLock() noexcept; |
| 178 | std::map<uint16_t, std::pair<std::shared_ptr<VmPortAllocation>, size_t>>& AllocatedPorts() noexcept; |
| 179 | |
| 180 | [[nodiscard]] bool TryClaimExpectedStop() noexcept; |
| 181 | [[nodiscard]] bool TryClaimSpontaneousExit() noexcept; |
| 182 | |
| 183 | _Requires_exclusive_lock_held_(m_lock) |
| 184 | void StartVmLockHeld(); |
| 185 | _Requires_exclusive_lock_held_(m_lock) |
| 186 | void StopVmLockHeld(); |
| 187 | _Requires_exclusive_lock_held_(m_lock) |
| 188 | void TearDownVmLockHeld(bool CaptureTerminationReason = false); |
| 189 | void EnsureVmRunning(); |
| 190 | void OnIdleTimer(); |
| 191 | void OnVmExited(); |
| 192 | void InitializeDockerRuntime(const std::filesystem::path& storagePath); |
| 193 | [[nodiscard]] VmLease AcquireVmLease(VmLeasePolicy Policy = VmLeasePolicy::Acquire); |
| 194 | [[nodiscard]] LockedRuntime Acquire(VmLeasePolicy Policy = VmLeasePolicy::Acquire); |
| 195 | |
| 196 | [[nodiscard]] bool TriggerIdleTerminationForTest(); |
| 197 | |
| 198 | // runtimeLock is an exclusive hold on m_lock (this runtime's lock), which is dropped and reacquired |
| 199 | // internally so the OnVmStopping notification can fire without it and TearDownVmLockHeld runs with it. |
| 200 | void Shutdown(wil::rwlock_release_exclusive_scope_exit& runtimeLock, WSLCVirtualMachineTerminationReason& terminationReason, std::wstring& terminationDetails); |
| 201 | |
| 202 | private: |
| 203 | bool IdleTerminationEnabled() const noexcept; |
| 204 | int StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs); |
| 205 | |
| 206 | // Fires the OnVmStarted hook for the VM instance identified by 'Generation'. Must be called |
| 207 | // without the runtime lock held (the handler may call back into the session), with an activity |
| 208 | // reference held so idle teardown cannot race the VM down before the notification is delivered. |
| 209 | void NotifyVmStarted(uint64_t Generation); |
| 210 | |
| 211 | // Fires the OnVmStopping hook, if OnVmStarted was delivered for 'Generation' and that instance is |
| 212 | // still the one running. Must be called without the runtime lock held: the handler may call into a |
| 213 | // plugin that acquires a VM lease (which takes m_lock), so firing under the lock would deadlock. |
| 214 | // Called while the VM is still running so the handler can still operate on it, and only after |
| 215 | // BeginVmStopLockHeld has committed the VM to stopping, so the announcement is always true. |
| 216 | void NotifyVmStopping(uint64_t Generation); |
| 217 | |
| 218 | // Commits the current VM to stopping. Ordinary leases arriving from here until EndVmStop() release |
| 219 | // the shared lock and wait for the teardown rather than being served by a VM that is going away. |
| 220 | _Requires_exclusive_lock_held_(m_lock) |
| 221 | void BeginVmStopLockHeld() noexcept; |
| 222 | |
| 223 | // Releases waiting leases, which then start (or wait for) the next VM. Safe to call when no stop |
| 224 | // is pending, so it can be run unconditionally from a scope_exit. |
| 225 | void EndVmStop() noexcept; |
| 226 | |
| 227 | WSLCSession* m_session{}; |
| 228 | RuntimeHooks m_hooks; |
| 229 | |
| 230 | ULONG m_id{}; |
| 231 | std::wstring m_displayName; |
| 232 | bool m_initialized{}; |
| 233 | const std::atomic<bool>* m_terminating{}; |
| 234 | wil::shared_event m_sessionTerminatingEvent; |
| 235 | wil::shared_event m_sessionTerminatedEvent; |
| 236 | |
| 237 | DWORD m_vmFactoryGitCookie{}; |
| 238 | wil::com_ptr<IGlobalInterfaceTable> m_git; |
| 239 | const WSLCSessionInitSettings* m_settings{}; |
| 240 | |
| 241 | std::optional<WSLCVirtualMachine> m_virtualMachine; |
| 242 | // Lock-free mirror of m_virtualMachine.has_value(): written under the runtime lock immediately |
| 243 | // after the VM object is constructed and immediately before it is destroyed, read without the |
| 244 | // lock by container teardown (~WSLCContainerImpl runs without a VM lease). Reading the optional |
| 245 | // itself there would race with the reset() performed by an idle teardown on another thread. |
| 246 | std::atomic<bool> m_hasVm{false}; |
| 247 | std::optional<IORelay> m_ioRelay; |
| 248 | std::optional<DockerEventTracker> m_eventTracker; |
| 249 | std::optional<DockerHTTPClient> m_dockerClient; |
| 250 | std::optional<WSLCVolumes> m_volumes; |
| 251 | std::optional<ServiceRunningProcess> m_containerdProcess; |
| 252 | std::optional<ServiceRunningProcess> m_dockerdProcess; |
| 253 | wil::unique_event m_vmExitedEvent; |
| 254 | // Lock-free mirror of "the current VM instance has exited": written under the runtime lock when a |
| 255 | // VM starts (false) or is observed dead during teardown (true), read without the lock by container |
| 256 | // teardown to skip VM-dependent cleanup. Avoids racing on m_vmExitedEvent, which is reset/replaced |
| 257 | // under the lock. |
| 258 | std::atomic<bool> m_vmExited{false}; |
| 259 | wil::unique_event m_dockerdReadyEvent{wil::EventOptions::ManualReset}; |
| 260 | |
| 261 | std::filesystem::path m_swapVhdPath; |
| 262 | bool m_storageMounted{false}; |
| 263 | std::mutex m_allocatedPortsLock; |
| 264 | std::map<uint16_t, std::pair<std::shared_ptr<VmPortAllocation>, size_t>> m_allocatedPorts; |
| 265 | |
| 266 | wil::srwlock m_lock; |
| 267 | std::atomic<VmState> m_vmState{VmState::None}; |
| 268 | std::atomic<VmExitDisposition> m_vmExitDisposition{VmExitDisposition::Active}; |
| 269 | |
| 270 | // Identifies the current VM instance. Bumped under m_lock by StartVmLockHeld, so a notification |
| 271 | // whose delivery had to drop the runtime lock can still tell whether it is describing the VM it |
| 272 | // was raised for, or one that has since been torn down and replaced. |
| 273 | std::atomic<uint64_t> m_vmGeneration{0}; |
| 274 | |
| 275 | // The VM instance whose OnVmStarted has been delivered and not yet retired (0 = none). Set by |
| 276 | // NotifyVmStarted, retired by NotifyVmStopping as it announces the stop, and also cleared by |
| 277 | // TearDownVmLockHeld to cover a VM that goes away without a stop ever being announced. Retiring |
| 278 | // it on the announcement is what makes OnVmStopping exactly-once: the stop is committed before it |
| 279 | // is announced, so the instance can never come back and a second stop for the same generation |
| 280 | // (Terminate() racing an idle teardown) finds nothing to retire and stays silent. |
| 281 | // |
| 282 | // The generation check and the hook invocation both happen under m_notifyLock, so a start and a |
| 283 | // stop racing on different threads cannot interleave (which would otherwise let a stale |
| 284 | // OnVmStarted be delivered after the OnVmStopping it should have preceded), and a notification |
| 285 | // that lost such a race is dropped rather than misattributed to whichever VM is running by the |
| 286 | // time it is delivered. Recursive because the handler may reentrantly restart the VM, re-entering |
| 287 | // these notifications on the same thread. |
| 288 | std::recursive_mutex m_notifyLock; |
| 289 | std::atomic<uint64_t> m_notifiedGeneration{0}; |
| 290 | |
| 291 | // Published under m_lock once a stop is decided and cleared when the teardown has finished. While |
| 292 | // it is set the VM is going away no matter what, so an ordinary lease must not be served by it; |
| 293 | // it waits on m_vmStopCompleteEvent (holding no lock, or the teardown could never reacquire the |
| 294 | // exclusive lock) and is then served by the next VM. Manual-reset and initially signaled so a |
| 295 | // lease that never sees a stop pending never blocks. |
| 296 | std::atomic<bool> m_vmStopPending{false}; |
| 297 | wil::unique_event m_vmStopCompleteEvent{wil::EventOptions::ManualReset | wil::EventOptions::Signaled}; |
| 298 | |
| 299 | std::shared_ptr<IdleState> m_idleState{std::make_shared<IdleState>()}; |
| 300 | |
| 301 | WSLCVirtualMachineTerminationReason m_lastTerminationReason{WSLCVirtualMachineTerminationReasonUnknown}; |
| 302 | std::wstring m_lastTerminationDetails; |
| 303 | }; |
| 304 | |
| 305 | } // namespace wsl::windows::service::wslc |