master
h 305 lines 13 KB
Raw
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