wslc: idle-terminate inactive per-user session VMs (#41077)

* wslc: idle-terminate per-user container VMs and lazily restart Per-user WSLC container session VMs now idle-terminate when no container is in a non-terminal (Created/Running) state, freeing host memory, and lazily restart on the next operation that needs the VM. - Centralize VM lifecycle in WSLCSession via TearDownVmLockHeld / StartVmLockHeld and an atomic VmExitDisposition (Active / StopRequested / ExitClaimed) to arbitrate expected stops vs. spontaneous VM exits without a polling thread. - Gate VM-requiring entrypoints behind AcquireVmLease(), which brings the VM up on demand and keeps it alive for the operation's duration. - Add IWSLCSession::BeginContainerOperation so a CLI command can hold the VM alive across resolve + operate + streamed output. - Preserve the session WarningCallback for the lifetime of the session so warnings emitted by the lazy VM start (e.g. resource recovery) are still delivered to the CLI invocation. - Remove the dtor lock in HcsVirtualMachine; OnExit/OnCrash are lock-free. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wslc: don't pin the session VM for merely-created containers Only Running containers now hold an activity reference that keeps the per-user session VM alive. Previously a container in either Created or Running state held the reference, so a `create`d-but-never-started container pinned the VM indefinitely and defeated idle termination. A created container's metadata persists on the containerd VHD across VM teardown and is rebuilt by RecoverExistingContainers on the next VM-requiring operation, so create -> idle-terminate -> start later works; the 30s grace period covers the common create-then-start gap. Also fix m_stateChangedAt recovery for created containers: docker inspect reports FinishedAt as the zero date ("0001-01-01T00:00:00Z") for a never-started container, which parsed to year 1 and rendered as "created 2026 years ago". Use the container's Created time for the Created state. This recovery path was previously unreachable, since created containers never got torn down. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wslc: fix stale Created/Running comments on the activity hold Addresses review feedback: WSLCContainer.h still described the activity hold as held while Created/Running, but it now only pins the VM while Running. Update the two header comments to match the implementation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wslc: address idle-termination review feedback - Make the VM idle grace period configurable via settings.yaml (session.idleTimeout, default 30s) instead of a hardcoded constant. - Assert UserSid is non-null in PersistSettings rather than tolerating a null SID. - Drop the session warning-callback GIT fallback; warnings emitted outside a callback-bearing operation are logged and event-logged only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wslc: update recovery warning tests for log-only behavior Recovery warnings emitted during lazy VM start run outside the user's current command, so they are now logged (and written to the event log) instead of being routed back to the session-creation warning callback. Update the three WarningCallback*Recovery unit tests and the e2e test to assert the warning is no longer delivered to the session callback / printed on stderr. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wslc: address review feedback on lazy VM lifecycle - BeginContainerOperation: reject new operations once the session is terminating/terminated, mirroring EnsureVmRunning's gate, so a started operation cannot pin a VM that is being torn down. - TearDownVmLockHeld: reset m_storageMounted after unmounting so the flag does not stay stale across an idle teardown. - CLI Session model: stop retaining the IWarningCallback for the session lifetime. Recovery warnings from lazy VM start are logged rather than delivered to the session callback, so the stashed callback (and its now-misleading comments) is dead state. The callback is still passed to CreateSession, where it is consumed during initialization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wslc: use non-throwing filesystem::exists for storage VHD probe Probe the storage VHD once with the std::error_code overload so an access-denied/transient I/O error surfaces as a clear Win32 error via WIL instead of a generic filesystem_error-to-HRESULT conversion, and avoid the duplicate exists() check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wslc: extract WSLCSessionRuntime from WSLCSession Move the VM instance and its lifecycle state (VM, IO relay, docker client, event tracker, volumes, containerd/dockerd processes, exit-disposition and vm-state atomics, session lock, idle state, published-port bookkeeping, and ephemeral swap/storage state) out of WSLCSession into a new WSLCSessionRuntime. WSLCSession retains identity and orchestration and drives the runtime through BringUp/RecoverState/TearDownSessionState/OnSpontaneousExit/OnCrashDump hooks plus a SessionContext carrying the lifecycle primitives the runtime observes, so the runtime no longer reaches into WSLCSession private members. VM state is reached through the runtime, with Acquire()/LockedRuntime as the ergonomic lease accessor. Concurrency invariants are preserved: the exit-disposition claim protocol, the relay-thread teardown guard, lock ordering, GIT re-fetch per VM creation, and release-lock-before-Disarm in shutdown. The VM-exit monitor is armed between bring-up and state recovery to match the pre-extraction ordering. This is an isolated refactor commit on top of the idle-terminate work so it can be reverted independently. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix formatting * wslc: skip containerd VHD unmount when storage was never mounted Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wslc: publish termination reason before signaling the terminated event Copy the termination reason/details into the session-visible fields before SetEvent() so a waiter woken by the terminated event cannot observe the default Unknown reason. Restores the pre-refactor ordering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wslc: add idle-termination test hook and pass runtime into container factories Add a test-only TriggerIdleTermination COM method to force VM idle teardown on demand, with three TAEF tests that hammer the VM idle-termination race paths. Refactor WSLCContainer construction to take WSLCSessionRuntime& instead of five individual runtime members. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wslc: force-delete recovery test container on all exit paths The TriggerIdleTerminationRecoversRunningContainer test left the persisted container in the shared default session on early-exit or if the recovered handle's best-effort delete failed, making later ListContainers-based tests order-dependent. Add a scope_exit that force-deletes the container by name regardless of exit path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wslc: list WSLCIdleState.h in wslcsession HEADERS Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wslc: clang-format Open() call site Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wslc: correct idle grace-period comment to reference IdleTimeoutSec Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wslc: avoid reserved identifier in Fork structured binding Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add WSLC VM start/stop plugin hooks Session-level WSLC plugin hooks (OnSessionCreated/OnSessionStopping) assumed session == running VM. With idle termination (PR #40781) the VM is created lazily, torn down when idle, and transparently recreated while the session persists, so plugins had no notification of actual VM lifecycle. Add VM-level hooks OnWslcVmStarted/OnWslcVmStopping that fire on every VM (re)start and teardown, in addition to the once-per-session hooks. Both are best-effort (errors logged and ignored). OnWslcVmStarted fires after releasing the runtime exclusive lock so a plugin may reentrantly call back into the session (e.g. WSLCCreateProcess) without deadlocking; OnWslcVmStopping fires under the lock during teardown, gated on a fire-once flag so a failed bring-up emits no spurious stopping. Adds a PluginTests::WslcVmRestart case that drives first start, forced idle teardown (TriggerIdleTermination), and lazy restart, asserting the hook sequence and proving reentrancy is deadlock-free. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f1c36ca0-19d0-46a7-82b3-10f1e1ee3e4c * Pair OnVmStopping only when OnVmStarted fired Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wslc: address PR feedback and harden idle-terminate VM lifecycle - Session-scope DockerEventTracker; rebind per VM start, preserve subscriptions - Fire OnVmStopping with m_lock dropped to avoid plugin-reentrancy deadlock - Keep containers alive on idle teardown; clear only on permanent shutdown - Redesign OnIdleTimer to commit teardown unconditionally after notifying, matching the test-trigger path; removes phantom OnVmStopping->OnVmStarted and the concurrent-Terminate is_signaled crash - Dedup containerd storage mount point into WSLCSessionDefaults.h - Mark WslcVmStarted/WslcVmStopping hooks as introduced in 2.9.5 * wslc: fix reentrant deadlock in VM start/stop notifications NotifyVmStarted/NotifyVmStopping invoked the plugin hook while holding m_notifyLock. During idle teardown a plugin may reentrantly restart the VM (WSLCCreateProcess), re-entering these notifications and self-deadlocking on the non-recursive mutex. Flip the pairing state under the lock, copy the hook, then invoke it after releasing the lock. Also clarify Shutdown's lock parameter (runtimeLock is the exclusive hold on m_lock) per review feedback. * test: exercise reentrant mount from the WSLC OnVmStarted plugin hook The VM-restart plugin test already validates a reentrant process launch and mount+unmount from OnVmStopping. Extend OnVmStarted to also mount+unmount so both lifecycle notifications validate reentrant mount management does not deadlock, and assert the new log lines. * wslc: hold a VmLease across container archive upload/download UploadArchive and DownloadArchive streamed data without holding a VmLease, so idle teardown could race and tear down the docker client mid-transfer. Acquire a VmLease and wrap in try/CATCH_RETURN to match Export/Logs and the other VM-dependent container operations. * test: fail fast instead of hanging on idle-termination deadlock detection TriggerIdleTerminationConcurrentWithOperations bounded the worker join with a std::async future, but on timeout the failed VERIFY would unwind and block in the future destructor until the (deadlocked) task completed -- hanging the test host. Fail fast with a dump on timeout so a real deadlock fails cleanly. * wslc: fix idle-teardown activity race and stopping-only hook pairing OnIdleTimer dropped the runtime lock to fire OnVmStopping, then unconditionally tore the VM down after reacquiring. A lease that started in that window found the VM still Running, so it did not restart and could leave a long-lived activity token (e.g. a process keep-alive), and the teardown would then kill it. Re-check the activity count after reacquiring the lock and, if non-zero, abandon the stop and re-pair the notification with a fresh OnVmStarted. Also set m_vmStartNotified whenever the VM starts rather than only when an OnVmStarted hook is installed, so a hooks user that sets only OnVmStopping still receives paired stop notifications. * wslc: skip guest volume unmount when the VM has already exited ReleaseRuntimeResources unconditionally called UnmountWindowsFolder via Vm() during container teardown. After an unexpected VM exit the guest is already gone, so each call only blocks on the RPC timeout and emits a spurious unmount-failed warning. A dead VM has already dropped every guest mount, so mark the mounts inactive locally instead. Add WSLCSessionRuntime::VmExited() (mirrors TearDownVmLockHeld's VM-dead check) so the container can detect this via the runtime it already references. * wslc: track VM-exited with an atomic to avoid a lock-free event race VmExited() read m_vmExitedEvent without the runtime lock, but StartVmLockHeld and TearDownVmLockHeld reset/replace that event under the lock, so a container teardown running on another thread could observe the handle mid-reset and fail-fast. Mirror the state in a std::atomic<bool>: cleared when a VM instance starts and latched when the guest is observed dead at the top of teardown (before session-state cleanup, so ReleaseRuntimeResources sees it). VmExited() now returns the atomic. * wslc: close two VM-lifecycle races found in multi-model review OnIdleTimer's abort path re-paired OnVmStarted without checking whether the VM died while m_lock was dropped for OnVmStopping. OnVmExited() declines that exit (the expected-stop claim is held) and the exit handle is one-shot, so the session was left with a dead VM marked Running until a later idle cycle. Tear the VM down in that case so a waiting lease restarts a fresh instance, matching the commit path. EnsureVmRunning's running fast path returned before the terminating gate, so a reentrant plugin lease during Shutdown's OnVmStopping (lock dropped) could run work against a VM being permanently torn down. Enforce the gate before the fast path, as its comment already documents. * wslc: narrow the WSLCSession/WSLCSessionRuntime seam The runtime extraction exposed mutable internals directly, letting WSLCSession drive them from outside. Replace those handouts with intent-revealing methods so the runtime owns its invariants: - Remove dead accessors StateAtomic() and VmExitedEvent() (no callers). - Replace ExitDispositionAtomic().load() call sites with the existing ExitDisposition() value getter and drop the atomic-ref accessor. - Replace the raw srwlock handout Lock() with TryLockExclusive(), used only by the Terminate/Shutdown handoff. - Replace DockerdReadyEvent() with Reset/Is/SignalDockerdReady(); replace the ContainerdProcess()/DockerdProcess() ref assignments with setters; replace SwapVhdPath() with SetSwapVhdPath(). No behavior change. Idle, VM restart, VM-kill, and plugin tests pass. * wslc: mark per-operation activity leases [[maybe_unused]] The BeginContainerOperation() lease is held only for its scope lifetime to keep the session VM alive during the operation. Annotate it so its intent is explicit and a future edit doesn't drop it to a discarded temporary. * wslc: fix idle-timer crash race and notification ordering race - OnIdleTimer: release the expected-stop disposition claim before the second lock drop for NotifyVmStarted(), so a VM crash in that window is claimable by OnVmExited() as a normal spontaneous exit instead of being silently dropped while m_vmState stays Running. - NotifyVmStarted/NotifyVmStopping: hold m_notifyLock (now recursive) across both the pairing-state flip and the hook invocation, so a start and a stop racing on different threads cannot interleave and deliver a stale OnVmStarted after its OnVmStopping. * wslc: address idle runtime review feedback Copilot-Session: 65bff365-20af-4701-8edd-09641ba75747 * wslc: report never-started sessions as idle Copilot-Session: 65bff365-20af-4701-8edd-09641ba75747 * wslc: fix session host crash releasing containers after idle teardown A graceful idle teardown releases the VM without the exit event ever being signaled, leaving the runtime in a state where VmExited() is false and there is no VM object. ReleaseRuntimeResources() only checked VmExited(), so the unmount path called Vm() and hit the empty optional. Terminating a session in that state clears m_containers, so this ran from ~WSLCContainerImpl and took down the per-user session host (STATUS_ASSERTION_FAILURE in debug, std::terminate in release) on a mainline flow: run a container, idle out, stop the session. Treat "no VM" like "VM exited", and wrap the container destructor body in CATCH_LOG so no future VM-dependent call can terminate the host from a destructor. An inner try is used rather than a function-try-block, which would implicitly rethrow. Also move Shutdown()'s idle-timer disarm into a scope_exit so it runs on every exit path. IdleState outlives the runtime via activity token copies, so an exception escaping the teardown left a late 1->0 transition able to re-arm a timer bound to a destroyed runtime. Adds SessionTerminationAfterIdleTerminationWithContainer, which fails without the fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6fa3a33-6af7-4323-be66-2d59dcca8d33 * wslc: gate VM leases across the OnVmStopping notification window Idle teardown drops the runtime lock to fire OnVmStopping so a plugin handler can take a VM lease without deadlocking. That window also let an unrelated thread lease the VM, which forced the teardown to be abandoned and a second OnVmStarted to be re-paired onto the very same VM instance -- plugins observed a stop/start pair the VM never actually performed. Publish a stop-pending state before dropping the lock instead. EnsureVmRunning holds off every other thread until the teardown completes, so each waiter then starts, and is notified about, a genuinely new VM. The notifying thread is exempt, since its reentrant leases are why the lock is dropped at all. This makes the notification unconditional and removes the cancel/re-pair path from both OnIdleTimer and the test hook. Also drop the SKIP_TEST_SERVER calls added by this change: the WSLC tests are disabled on server either way. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6fa3a33-6af7-4323-be66-2d59dcca8d33 * Revert "wslc: gate VM leases across the OnVmStopping notification window" This reverts the runtime portion of 2ebee52a. The gate cannot work: it exempts the notifying thread by OS thread id, but a plugin OnVmStopping handler re-enters the session host over COM (wslservice.exe -> IWSLCPluginNotifier -> WSLCCreateProcess), so the reentrant lease lands on an RPC worker thread and never matches the exemption. It then waits on m_vmStopComplete, which is only signalled after NotifyVmStopping returns -- a hard cycle that hangs the session host, the plugin and the service permanently. This is a documented, tested contract: WslPluginApi.h states a callback needing the VM transparently restarts it during idle teardown, and PluginTests::WslcVmRestart expects the reentrant WSLCCreateProcess to succeed. That test hangs with the gate and passes without it. The gate was also not airtight: AddActivity takes no runtime lock, so a lease could pass the check before the stop was published, then acquire the shared lock as soon as it was dropped for the notification. With the post-notification activity re-check removed, teardown could destroy a VM under a freshly created keep-alive token. Keeps the SKIP_TEST_SERVER removal, which was unrelated and correct. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6fa3a33-6af7-4323-be66-2d59dcca8d33 * wslc: commit to an announced VM stop before notifying plugins OnWslcVmStopping has to fire while the VM is still alive -- its purpose is to warn a plugin that the VM it is using is going away, while it can still act on it. Delivering it means dropping the runtime lock, because a plugin handler may call back into the session, and that opens a window in which new work can arrive. Previously that work could keep the VM alive, so an announced stop could silently not happen and the plugin was left holding a warning that never came true. Decide the stop under the exclusive lock and publish it before announcing it, so the notification states a fact rather than a forecast: - The last silent back-out is the ActivityCount() check immediately before BeginVmStopLockHeld(). From there the teardown runs unconditionally. - A lease arriving during the window releases its shared lock, waits on m_vmStopCompleteEvent and retries, so it is served by a fresh VM rather than keeping a promised-dead one alive. Releasing the lock before waiting is required: the teardown must be able to reacquire it exclusively. - EndVmStop() is a scope_exit declared after the lock guard, so reverse-order destruction runs it while the lock is still held and a waiter can only wake into a fully torn-down, published state. - Calls the stopping handler makes itself must not wait for the teardown they are holding up. wslservice marks the thread running the callout and tags those calls with FromVmLifecycleCallback, so the session serves them from the VM that is going away. A plugin calling from a thread of its own is untagged and simply waits for the next VM. - NotifyVmStopping retires the notified generation as it announces the stop, so a Terminate() racing an idle teardown cannot deliver OnWslcVmStopping twice. Work a plugin leaves running when its handler returns now dies with the VM, which is exactly what the handler was just told would happen. Documented in WslPluginApi.h along with the rule that the handler must not block waiting on another thread calling into the session. Also publish VM presence in an atomic, so ~WSLCContainerImpl -- which deliberately holds no VM lease -- can query it without racing the teardown's reset(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6fa3a33-6af7-4323-be66-2d59dcca8d33 * wslc: fix spurious OnVmStopping for a session that never started a VM NotifyVmStopping retires the notified generation with a compare-exchange against the generation being stopped. Both m_vmGeneration and m_notifiedGeneration start at 0, so a session torn down without ever starting a VM -- bring-up is lazy, a VM is only created on the first operation that needs one -- reached Shutdown with generation 0 and the CAS matched, delivering an OnWslcVmStopping that no OnWslcVmStarted ever paired with. Treat 0 as the 'nothing announced' sentinel it already is everywhere else. Covered by a new WslcVmNeverStarted plugin test. Also from review: - Don't attach a process keep-alive activity token for a call made from the OnVmStopping handler. That VM is committed to stopping, so the token cannot do its job, but it would keep counting activity for as long as the plugin held the proxy and block idle termination of every later VM in the session. - Deliver the notification under CATCH_LOG at all three sites. The stop is already published and its generation retired by then, so a throwing handler must not be able to skip the teardown and leave waiters to be served by the VM they were promised would die. - Correct three comments: TearDownVmLockHeld's claim that the notify paths never run mid-teardown, WslPluginApi.h's claim that blocked callers are always served by a next VM (there is none when the session itself is terminating), and the IDL's 'must be set only by the service' for FromVmLifecycleCallback, which the session cannot enforce across the process boundary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6fa3a33-6af7-4323-be66-2d59dcca8d33 * wslc: close the exit event handle and tighten the recovery-warning assert IWSLCProcess::GetExitEvent marshals its handle as [out, system_handle(sh_event)], so the caller owns the duplicate. The stop-window thread waited on it and dropped it -- a leak, and the test plugin is reference code plugin authors copy. WSLCE2E_Warning_ContainerRecoveryNotPrintedOnStderr guarded its only assertion on Stderr.has_value(), so the test would pass vacuously if stderr capture broke -- exactly when it would stop proving anything. No stderr at all already satisfies 'the warning was not printed', so assert against an empty string instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6fa3a33-6af7-4323-be66-2d59dcca8d33 * wslc: assert stderr capture in the recovery-warning test and close the leaked exit event RunWslc always populates Stderr, so guarding the search on has_value() -- or searching value_or({}) -- lets the test pass without proving anything if capture ever breaks. Assert has_value() first, matching the other wslc e2e tests. Also close the duplicated exit event at session teardown when the stop-window thread did not get far enough to claim it. The leaked process wrapper is left alone deliberately: it holds a COM proxy marshalled to the OnWslcVmStopping callback's thread, so releasing it from the session-stopping thread risks the same RPC_E_WRONG_THREAD hazard that forced the exit event to be cached as a plain handle, and it is one wrapper for the lifetime of a test process. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6fa3a33-6af7-4323-be66-2d59dcca8d33 * wslc: serialize test plugin logging and roll back port reservations on failure Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6fa3a33-6af7-4323-be66-2d59dcca8d33 * wslc: reject plugin calls when the session has no running VM Plugin-originated mount/exec calls used to acquire a VM lease like any other client, so a plugin could bring a VM up as a side effect of a session-level callback, or block on a teardown it was itself holding up. Rename the IWSLCSession flag from FromVmLifecycleCallback to AcquireVmLease, which names what it does rather than when the service sets it, and invert its meaning: the service now passes FALSE for every plugin call. Such a call is served by whatever VM is already running, including one committed to stopping, and fails with the new WSLC_E_VM_NOT_RUNNING when there is none. The thread-local tagging that used to distinguish a call made from inside OnWslcVmStopping is no longer needed and is removed. Also drop the WSLCContainerImpl accessors that only forwarded to the runtime, and use wil::unique_handle for the test plugin's duplicated exit event. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6fa3a33-6af7-4323-be66-2d59dcca8d33 * wslc: surface WSLC_E_VM_NOT_RUNNING in the plugin header and refresh stale comments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6fa3a33-6af7-4323-be66-2d59dcca8d33 * wslc: hold a container operation token across copy to/from container Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6fa3a33-6af7-4323-be66-2d59dcca8d33 * wslc: include <set> in WSLCVirtualMachine.h Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e6fa3a33-6af7-4323-be66-2d59dcca8d33 --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f1c36ca0-19d0-46a7-82b3-10f1e1ee3e4c Copilot-Session: 65bff365-20af-4701-8edd-09641ba75747 Copilot-Session: e6fa3a33-6af7-4323-be66-2d59dcca8d33

Ben Hillis committed Aug 4, 2026 at 20:11 UTC 0e8adfb17b5a15bdaaaffaa0857a95a62b102302
41 files changed +3423 -660
doc/docs/api-reference/c/error-codes.md
+2
@@ -17,6 +17,7 @@
17 #define WSLC_E_REGISTRY_BLOCKED_BY_POLICY MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 13) /* 0x8004060D */
18 #define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */
19 #define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */
20 +#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */
21 ```
22
23 | Symbol | Hex Value |
@@ -37,5 +38,6 @@
38 | `WSLC_E_REGISTRY_BLOCKED_BY_POLICY` | `0x8004060D` |
39 | `WSLC_E_VOLUME_NOT_AVAILABLE` | `0x8004060E` |
40 | `WSLC_E_SESSION_NOT_FOUND` | `0x8004060F` |
41 +| `WSLC_E_VM_NOT_RUNNING` | `0x80040610` |
42
43 ---
src/windows/WslcSDK/wslcsdk.h
+1
@@ -43,6 +43,7 @@ EXTERN_C_START
43 #define WSLC_E_REGISTRY_BLOCKED_BY_POLICY MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 13) /* 0x8004060D */
44 #define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */
45 #define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */
46 +#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */
47
48 // Session values
49 #define WSLC_SESSION_OPTIONS_SIZE 72
src/windows/common/WSLCProcessLauncher.cpp
+2 -1
@@ -180,7 +180,8 @@ std::tuple<HRESULT, std::optional<ClientRunningWSLCProcess>, int> WSLCProcessLau
180
181 wil::com_ptr<IWSLCProcess> process;
182 int error = -1;
183 - auto result = Session.CreateRootNamespaceProcess(m_executable.c_str(), &options, m_rows, m_columns, &process, &error);
183 + auto result =
184 + Session.CreateRootNamespaceProcess(m_executable.c_str(), &options, m_rows, m_columns, /* AcquireVmLease */ TRUE, &process, &error);
185 if (FAILED(result))
186 {
187 return std::make_tuple(result, std::optional<ClientRunningWSLCProcess>(), error);
src/windows/common/WSLCSessionDefaults.h
+1
@@ -22,5 +22,6 @@ inline constexpr const wchar_t DefaultAdminSessionName[] = L"wslc-cli-admin";
22 inline constexpr const wchar_t DefaultStorageSubPath[] = L"wslc\\sessions";
23 inline constexpr const wchar_t DefaultStorageVhdName[] = L"storage.vhdx";
24 inline constexpr uint32_t DefaultBootTimeoutMs = 30000;
25 +inline constexpr const char ContainerdStorageMountPoint[] = "/var/lib/docker";
26
27 } // namespace wsl::windows::wslc
src/windows/common/WSLCUserSettings.cpp
+8
@@ -57,6 +57,9 @@ static constexpr std::string_view s_DefaultSettingsTemplate =
57 " # used without an explicit address (default: 127.0.0.1)\n"
58 " # defaultBindingAddress: default\n"
59 "\n"
60 + " # Seconds an idle session VM stays running before it is torn down (default: 30)\n"
61 + " # idleTimeout: default\n"
62 + "\n"
63 "# Credential storage backend: \"wincred\" or \"file\" (default: wincred)\n"
64 "# credentialStore: wincred\n";
65
@@ -163,6 +166,11 @@ namespace details {
166 return value;
167 }
168
169 + WSLC_VALIDATE_SETTING(SessionIdleTimeout)
170 + {
171 + return value > 0 ? std::optional{value} : std::nullopt;
172 + }
173 +
174 WSLC_VALIDATE_SETTING(CredentialStore)
175 {
176 if (value == "wincred")
src/windows/common/WSLCUserSettings.h
+2
@@ -45,6 +45,7 @@ enum class Setting : size_t
45 SessionPortRelay,
46 SessionDefaultBindingAddress,
47 SessionStoragePath,
48 + SessionIdleTimeout,
49
50 Max
51 };
@@ -101,6 +102,7 @@ namespace details {
102 DEFINE_SETTING_MAPPING(SessionPortRelay, std::string, PortRelayType, PortRelayType::VirtioNet, "experimental.portRelay")
103 DEFINE_SETTING_MAPPING(SessionDefaultBindingAddress, std::string, std::string, std::string{}, "session.defaultBindingAddress")
104 DEFINE_SETTING_MAPPING(SessionStoragePath, std::string, std::string, std::string{}, "session.storagePath")
105 + DEFINE_SETTING_MAPPING(SessionIdleTimeout, uint32_t, uint32_t, 30, "session.idleTimeout")
106
107 #undef DEFINE_SETTING_MAPPING
108 // clang-format on
src/windows/common/wslutil.cpp
+1
@@ -162,6 +162,7 @@ static const std::map<HRESULT, LPCWSTR> g_commonErrors{
162 X(WSLC_E_INVALID_SESSION_NAME),
163 X(WSLC_E_NETWORK_NOT_FOUND),
164 X(WSLC_E_SESSION_NOT_FOUND),
165 + X(WSLC_E_VM_NOT_RUNNING),
166 X(WSLC_E_WU_SEARCH_FAILED),
167 X_WIN32(RPC_S_SERVER_UNAVAILABLE),
168 X_WIN32(ERROR_ELEVATION_REQUIRED),
src/windows/inc/WslPluginApi.h
+33
@@ -26,6 +26,12 @@ extern "C" {
26 #define WSLPLUGINAPI_ENTRYPOINTV1 WSLPluginAPIV1_EntryPoint
27 #define WSL_E_PLUGIN_REQUIRES_UPDATE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x032A)
28
29 +// Returned by the WSLC plugin API calls below when the session has no running VM.
30 +// N.B. This value is also defined in wslc.idl; the two definitions must stay in sync.
31 +#ifndef WSLC_E_VM_NOT_RUNNING
32 +#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x0610)
33 +#endif
34 +
35 #define WSL_PLUGIN_REQUIRE_VERSION(_Major, _Minor, _Revision, Api) \
36 if (Api->Version.Major < (_Major) || (Api->Version.Major == (_Major) && Api->Version.Minor < (_Minor)) || \
37 (Api->Version.Major == (_Major) && Api->Version.Minor == (_Minor) && Api->Version.Revision < (_Revision))) \
@@ -141,9 +147,34 @@ typedef HRESULT (*WSLPluginAPI_ImageCreated)(const struct WSLCSessionInformation
147 // Called when an image is deleted. 'ImageId' is the deleted image identifier. Errors are ignored.
148 typedef HRESULT (*WSLPluginAPI_ImageDeleted)(const struct WSLCSessionInformation* Session, LPCSTR ImageId);
149
150 +// Called when the VM backing a WSLC session has started. Unlike OnSessionCreated (which fires once
151 +// per session), this fires every time a VM is created for the session: on the first operation that
152 +// needs a VM, and again each time the VM is recreated after being idle-terminated. Errors are logged
153 +// but ignored (they do not abort VM startup or the triggering operation).
154 +typedef HRESULT (*WSLPluginAPI_OnWslcVmStarted)(const struct WSLCSessionInformation* Session);
155 +
156 +// Called when the VM backing a WSLC session is about to stop (idle teardown, explicit termination,
157 +// or unexpected exit). Fires exactly once per OnWslcVmStarted. Errors are logged but ignored.
158 +//
159 +// The VM is still alive for the duration of this call, so a callback may run last-minute work in it
160 +// (e.g. WSLCCreateProcess) to react to the VM going away. During a permanent session termination
161 +// such a call fails cleanly, because the session itself is being torn down.
162 +//
163 +// The stop is guaranteed: the VM is torn down as soon as this call returns, and nothing the callback
164 +// does can keep it alive. Any work the callback leaves running in the VM -- a process it did not wait
165 +// for, for example -- dies with it. Calls made by other threads while this callback is running are
166 +// served by the same stopping VM, on the same terms; once the teardown starts they fail with
167 +// WSLC_E_VM_NOT_RUNNING rather than waiting for or creating another VM.
168 +typedef HRESULT (*WSLPluginAPI_OnWslcVmStopping)(const struct WSLCSessionInformation* Session);
169 +
170 //
171 // WSLC plugin API calls.
172 //
173 +// These operate on the VM that is currently backing the session; they never create one. A call made
174 +// while the session has no running VM fails with WSLC_E_VM_NOT_RUNNING, so a plugin that needs a VM
175 +// should do its work from OnWslcVmStarted (or before OnWslcVmStopping returns) rather than from a
176 +// session-level callback.
177 +//
178
179 // Mount a Windows folder into the WSLC session VM at the given 'Mountpoint' path. If the 'Mountpoint' doesn't exist, it will be created.
180 typedef HRESULT (*WSLCPluginAPI_MountFolder)(WSLCSessionId Session, LPCWSTR WindowsPath, LPCSTR Mountpoint, BOOL ReadOnly);
@@ -220,6 +251,8 @@ struct WSLPluginHooksV1
251 WSLPluginAPI_ContainerStopping ContainerStopping;
252 WSLPluginAPI_ImageCreated ImageCreated;
253 WSLPluginAPI_ImageDeleted ImageDeleted;
254 + WSLPluginAPI_OnWslcVmStarted WslcVmStarted; // Introduced in 2.9.5
255 + WSLPluginAPI_OnWslcVmStopping WslcVmStopping; // Introduced in 2.9.5
256 };
257
258 struct WSLPluginAPIV1
src/windows/service/exe/HcsVirtualMachine.cpp
+3 -1
@@ -348,7 +348,9 @@ HcsVirtualMachine::HcsVirtualMachine(_In_ const WSLCSessionSettings* Settings)
348
349 HcsVirtualMachine::~HcsVirtualMachine()
350 {
351 - std::lock_guard lock(m_lock);
351 + // Do not hold m_lock: waiting on m_vmExitEvent and closing the compute system below both block
352 + // on in-flight HCS exit/crash callbacks, which may themselves need m_lock. OnExit() is lock-free,
353 + // and closing the compute system drains all callbacks, so the rest of teardown needs no lock.
354
355 // Wait up to 5 seconds for the VM to terminate gracefully.
356 bool forceTerminate = false;
src/windows/service/exe/PluginManager.cpp
+77 -9
@@ -28,6 +28,31 @@ constexpr auto c_pluginPath = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Lx
28 constexpr WSLVersion Version = {wsl::shared::VersionMajor, wsl::shared::VersionMinor, wsl::shared::VersionRevision};
29
30 thread_local std::optional<std::wstring> g_pluginErrorMessage;
31 +thread_local bool g_inWslcPluginNotification = false;
32 +
33 +// Plugin-originated calls into the WSLC plugin API never acquire a VM lease: a plugin is a side
34 +// effect of the session's own activity, never a reason to bring a VM up. The call is served by
35 +// whatever VM is already running -- including one committed to stopping, which is what lets a plugin
36 +// do last-minute work from its OnWslcVmStopping handler without deadlocking against the teardown it
37 +// is blocking -- and is rejected with WSLC_E_VM_NOT_RUNNING when there is no VM.
38 +constexpr BOOL c_pluginAcquireVmLease = FALSE;
39 +
40 +class WslcPluginNotificationContext
41 +{
42 +public:
43 + WslcPluginNotificationContext() : m_previous(std::exchange(g_inWslcPluginNotification, true))
44 + {
45 + }
46 +
47 + ~WslcPluginNotificationContext()
48 + {
49 + g_inWslcPluginNotification = m_previous;
50 + }
51 +
52 +private:
53 + ExecutionContext m_executionContext{Context::Plugin};
54 + bool m_previous;
55 +};
56
57 extern "C" {
58 HRESULT MountFolder(WSLSessionId Session, LPCWSTR WindowsPath, LPCWSTR LinuxPath, BOOL ReadOnly, LPCWSTR Name)
@@ -127,7 +152,7 @@ try
152 RETURN_HR_IF(E_POINTER, WindowsPath == nullptr || Mountpoint == nullptr);
153
154 auto session = ResolveWslcSession(Session);
130 - auto result = session->MountWindowsFolder(WindowsPath, Mountpoint, ReadOnly);
155 + auto result = session->MountWindowsFolder(WindowsPath, Mountpoint, ReadOnly, c_pluginAcquireVmLease);
156
157 WSL_LOG(
158 "WslcPluginMountFolderCall",
@@ -149,7 +174,7 @@ try
174
175 auto session = ResolveWslcSession(Session);
176
152 - auto result = session->UnmountWindowsFolder(Mountpoint);
177 + auto result = session->UnmountWindowsFolder(Mountpoint, c_pluginAcquireVmLease);
178
179 WSL_LOG(
180 "WslcPluginUnmountFolderCall",
@@ -197,7 +222,7 @@ try
222
223 wil::com_ptr<IWSLCProcess> process;
224 int errnoValue = 0;
200 - auto result = session->CreateRootNamespaceProcess(Executable, &options, 0, 0, &process, &errnoValue);
225 + auto result = session->CreateRootNamespaceProcess(Executable, &options, 0, 0, c_pluginAcquireVmLease, &process, &errnoValue);
226
227 if (Errno != nullptr)
228 {
@@ -565,9 +590,14 @@ void PluginManager::ThrowIfFatalPluginError() const
590 }
591 }
592
593 +bool PluginManager::IsInWslcNotification() noexcept
594 +{
595 + return g_inWslcPluginNotification;
596 +}
597 +
598 void PluginManager::OnWslcSessionCreated(const WSLCSessionInformation* Session)
599 {
570 - ExecutionContext context(Context::Plugin);
600 + WslcPluginNotificationContext context;
601
602 for (const auto& e : m_plugins)
603 {
@@ -588,7 +618,7 @@ void PluginManager::OnWslcSessionCreated(const WSLCSessionInformation* Session)
618
619 void PluginManager::OnWslcSessionStopping(const WSLCSessionInformation* Session) const
620 {
591 - ExecutionContext context(Context::Plugin);
621 + WslcPluginNotificationContext context;
622
623 for (const auto& e : m_plugins)
624 {
@@ -609,7 +639,7 @@ void PluginManager::OnWslcSessionStopping(const WSLCSessionInformation* Session)
639 HRESULT PluginManager::OnWslcContainerStarted(const WSLCSessionInformation* Session, LPCSTR InspectJson) const
640 try
641 {
612 - ExecutionContext context(Context::Plugin);
642 + WslcPluginNotificationContext context;
643
644 for (const auto& e : m_plugins)
645 {
@@ -632,7 +662,7 @@ CATCH_RETURN()
662
663 void PluginManager::OnWslcContainerStopping(const WSLCSessionInformation* Session, LPCSTR ContainerId) const
664 {
635 - ExecutionContext context(Context::Plugin);
665 + WslcPluginNotificationContext context;
666
667 for (const auto& e : m_plugins)
668 {
@@ -654,7 +684,7 @@ void PluginManager::OnWslcContainerStopping(const WSLCSessionInformation* Sessio
684
685 void PluginManager::OnWslcImageCreated(const WSLCSessionInformation* Session, LPCSTR InspectJson) const
686 {
657 - ExecutionContext context(Context::Plugin);
687 + WslcPluginNotificationContext context;
688
689 for (const auto& e : m_plugins)
690 {
@@ -674,7 +704,7 @@ void PluginManager::OnWslcImageCreated(const WSLCSessionInformation* Session, LP
704
705 void PluginManager::OnWslcImageDeleted(const WSLCSessionInformation* Session, LPCSTR ImageId) const
706 {
677 - ExecutionContext context(Context::Plugin);
707 + WslcPluginNotificationContext context;
708
709 for (const auto& e : m_plugins)
710 {
@@ -691,3 +721,41 @@ void PluginManager::OnWslcImageDeleted(const WSLCSessionInformation* Session, LP
721 }
722 }
723 }
724 +
725 +void PluginManager::OnWslcVmStarted(const WSLCSessionInformation* Session) const
726 +{
727 + WslcPluginNotificationContext context;
728 +
729 + for (const auto& e : m_plugins)
730 + {
731 + if (e.hooks.WslcVmStarted != nullptr)
732 + {
733 + const auto result = e.hooks.WslcVmStarted(Session);
734 + WSL_LOG(
735 + "PluginOnWslcVmStartedCall",
736 + TraceLoggingValue(e.name.c_str(), "Plugin"),
737 + TraceLoggingValue(Session->SessionId, "SessionId"),
738 + TraceLoggingValue(result, "Result"));
739 + LOG_IF_FAILED_MSG(result, "Error thrown from plugin: '%ls'", e.name.c_str());
740 + }
741 + }
742 +}
743 +
744 +void PluginManager::OnWslcVmStopping(const WSLCSessionInformation* Session) const
745 +{
746 + WslcPluginNotificationContext context;
747 +
748 + for (const auto& e : m_plugins)
749 + {
750 + if (e.hooks.WslcVmStopping != nullptr)
751 + {
752 + const auto result = e.hooks.WslcVmStopping(Session);
753 + WSL_LOG(
754 + "PluginOnWslcVmStoppingCall",
755 + TraceLoggingValue(e.name.c_str(), "Plugin"),
756 + TraceLoggingValue(Session->SessionId, "SessionId"),
757 + TraceLoggingValue(result, "Result"));
758 + LOG_IF_FAILED_MSG(result, "Error thrown from plugin: '%ls'", e.name.c_str());
759 + }
760 + }
761 +}
src/windows/service/exe/PluginManager.h
+4
@@ -52,6 +52,10 @@ public:
52 void OnWslcContainerStopping(const WSLCSessionInformation* Session, LPCSTR ContainerId) const;
53 void OnWslcImageCreated(const WSLCSessionInformation* Session, LPCSTR InspectJson) const;
54 void OnWslcImageDeleted(const WSLCSessionInformation* Session, LPCSTR ImageId) const;
55 + void OnWslcVmStarted(const WSLCSessionInformation* Session) const;
56 + void OnWslcVmStopping(const WSLCSessionInformation* Session) const;
57 +
58 + static bool IsInWslcNotification() noexcept;
59
60 void ThrowIfFatalPluginError() const;
61
src/windows/service/exe/WSLCPluginNotifier.cpp
+20
@@ -64,3 +64,23 @@ try
64 return S_OK;
65 }
66 CATCH_RETURN();
67 +
68 +HRESULT WSLCPluginNotifier::OnVmStarted()
69 +try
70 +{
71 + COMServiceExecutionContext context;
72 +
73 + m_plugins.OnWslcVmStarted(&m_sessionInfo);
74 + return S_OK;
75 +}
76 +CATCH_RETURN();
77 +
78 +HRESULT WSLCPluginNotifier::OnVmStopping()
79 +try
80 +{
81 + COMServiceExecutionContext context;
82 +
83 + m_plugins.OnWslcVmStopping(&m_sessionInfo);
84 + return S_OK;
85 +}
86 +CATCH_RETURN();
src/windows/service/exe/WSLCPluginNotifier.h
+2
@@ -34,6 +34,8 @@ public:
34 IFACEMETHOD(OnContainerStopping)(_In_ LPCSTR ContainerId) override;
35 IFACEMETHOD(OnImageCreated)(_In_ LPCSTR InspectJson) override;
36 IFACEMETHOD(OnImageDeleted)(_In_ LPCSTR ImageId) override;
37 + IFACEMETHOD(OnVmStarted)() override;
38 + IFACEMETHOD(OnVmStopping)() override;
39
40 private:
41 wsl::windows::service::PluginManager& m_plugins;
src/windows/service/exe/WSLCSessionManager.cpp
+13 -10
@@ -121,6 +121,7 @@ private:
121 Settings.MemoryMb = memoryMb > 0 ? memoryMb : SessionSettings::DefaultMemoryMb();
122 Settings.MaximumStorageSizeMb = userSettings.Get<settings::Setting::SessionStorageSizeMb>();
123 Settings.BootTimeoutMs = wsl::windows::wslc::DefaultBootTimeoutMs;
124 + Settings.IdleTimeoutSec = userSettings.Get<settings::Setting::SessionIdleTimeout>();
125 Settings.NetworkingMode = userSettings.Get<settings::Setting::SessionNetworkingMode>();
126
127 // TODO: Add a config setting to opt-out of GPU support.
@@ -285,8 +286,7 @@ void WSLCSessionManagerImpl::CreateSession(
286 g_pluginManager, sessionId, creatorPid, std::wstring(resolvedDisplayName), wil::shared_handle(sharedToken), std::vector<BYTE>(storedSid));
287
288 // Create the VM factory in the SYSTEM service (privileged). The per-user session
288 - // uses it to create the VM. Funneling VM creation through a factory lets the session
289 - // own when VMs are created, rather than having one handed to it up front.
289 + // uses it to create VMs on demand and recreate them after idle-termination.
290 auto vmFactory = Microsoft::WRL::Make<WSLCVirtualMachineFactory>(Settings);
291
292 // 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(
473 sessionSettings.RootVhdTypeOverride = Settings->RootVhdTypeOverride;
474 sessionSettings.StorageFlags = Settings->StorageFlags;
475 sessionSettings.SwapSizeMb = Settings->MemoryMb;
476 + sessionSettings.IdleTimeoutSec = Settings->IdleTimeoutSec;
477 return sessionSettings;
478 }
479
@@ -688,15 +689,17 @@ wil::com_ptr<IWSLCSession> WSLCSessionManagerImpl::FindSession(ULONG Id)
689 {
690 wil::com_ptr<IWSLCSession> result;
691
691 - ForEachSession<HRESULT>([&](SessionEntry& entry, const wil::com_ptr<IWSLCSession>& session) noexcept -> std::optional<HRESULT> {
692 - if (entry.SessionId != Id)
693 - {
694 - return std::nullopt;
695 - }
692 + ForEachSession<HRESULT>(
693 + [&](SessionEntry& entry, const wil::com_ptr<IWSLCSession>& session) noexcept -> std::optional<HRESULT> {
694 + if (entry.SessionId != Id)
695 + {
696 + return std::nullopt;
697 + }
698
697 - result = session;
698 - return S_OK;
699 - });
699 + result = session;
700 + return S_OK;
701 + },
702 + PluginManager::IsInWslcNotification());
703
704 THROW_HR_IF_MSG(WSLC_E_SESSION_NOT_FOUND, !result, "WSLC session %lu not found", Id);
705 return result;
src/windows/service/exe/WSLCSessionManager.h
+8 -1
@@ -108,7 +108,7 @@ private:
108 // Iterates over all sessions, cleaning up released sessions.
109 // The routine receives a SessionEntry& and can return an optional<T> to stop iteration.
110 template <typename T>
111 - inline auto ForEachSession(const auto& Routine)
111 + inline auto ForEachSession(const auto& Routine, bool DeferSessionCleanup = false)
112 {
113 std::lock_guard lock(m_wslcSessionsLock);
114
@@ -129,6 +129,13 @@ private:
129 wil::com_ptr<IWSLCSession> lockedSession;
130 if (FAILED_LOG(entry.Ref->OpenSession(&lockedSession)))
131 {
132 + // FindSession is used by plugin callbacks into the API. Defer cleanup in that path so
133 + // OnWslcSessionStopping is not nested inside another plugin notification.
134 + if (DeferSessionCleanup)
135 + {
136 + return false; // Keep in tracking; clean up on a later pass.
137 + }
138 +
139 // Session is gone: notify plugins (if not already), then drop persistent reference if any.
140 NotifySessionStoppingLockHeld(entry);
141
src/windows/service/inc/wslc.idl
+39 -4
@@ -125,6 +125,15 @@ interface IWSLCPluginNotifier : IUnknown
125
126 // Called when an image is deleted. 'ImageId' is the image identifier. Errors are logged but ignored.
127 HRESULT OnImageDeleted([in] LPCSTR ImageId);
128 +
129 + // Called when the VM backing the session has started (first start or recreation after idle
130 + // teardown). Errors are logged but ignored.
131 + HRESULT OnVmStarted();
132 +
133 + // Called when the VM backing the session is about to stop. The VM is still running, and is
134 + // committed to stopping: session calls made from this callback are served by it, while any other
135 + // caller waits for the teardown and is served by the next VM. Errors are logged but ignored.
136 + HRESULT OnVmStopping();
137 };
138
139 typedef struct _WSLCImageInformation
@@ -491,6 +500,7 @@ typedef struct _WSLCSessionSettings {
500 WSLCFeatureFlags FeatureFlags;
501 WSLCHandle DmesgOutput;
502 WSLCSessionStorageFlags StorageFlags;
503 + ULONG IdleTimeoutSec;
504
505 // Below options are used for debugging purposes only.
506 [unique] LPCWSTR RootVhdOverride;
@@ -633,6 +643,7 @@ typedef struct _WSLCSessionInitSettings
643 WSLCNetworkingMode NetworkingMode;
644 WSLCFeatureFlags FeatureFlags;
645 [unique] LPCSTR RootVhdTypeOverride;
646 + ULONG IdleTimeoutSec;
647 } WSLCSessionInitSettings;
648
649 [
@@ -675,7 +686,13 @@ interface IWSLCSession : IUnknown
686 HRESULT PruneContainers([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out] WSLCPruneContainersResults* Result);
687
688 // Create a process at the VM level. This is meant for debugging.
678 - HRESULT CreateRootNamespaceProcess([in, ref] LPCSTR Executable, [in, ref] const WSLCProcessOptions* Options, [in] ULONG TtyRows, [in] ULONG TtyColumns, [out] IWSLCProcess** Process, [out] int* Errno);
689 + // 'AcquireVmLease' controls whether the call may bring a VM up. TRUE is the normal client
690 + // behaviour: start the VM if there is none, and wait out an announced stop so the call is served
691 + // by a fresh VM. FALSE is what the service passes for plugin-originated calls: the call is served
692 + // by whatever VM is already running -- including one committed to stopping, which is what lets a
693 + // plugin do last-minute work from its OnVmStopping handler without deadlocking against the
694 + // teardown it is blocking -- and fails with WSLC_E_VM_NOT_RUNNING when there is no VM.
695 + HRESULT CreateRootNamespaceProcess([in, ref] LPCSTR Executable, [in, ref] const WSLCProcessOptions* Options, [in] ULONG TtyRows, [in] ULONG TtyColumns, [in] BOOL AcquireVmLease, [out] IWSLCProcess** Process, [out] int* Errno);
696
697 // TODO: an OpenProcess() method can be added later if needed.
698
@@ -685,9 +702,10 @@ interface IWSLCSession : IUnknown
702 // Terminate the VM and containers.
703 HRESULT Terminate();
704
688 - // Used only for testing. TODO: Think about moving them to a dedicated testing-only interface.
689 - HRESULT MountWindowsFolder([in, ref] LPCWSTR WindowsPath, [in, ref] LPCSTR LinuxPath, [in] BOOL ReadOnly);
690 - HRESULT UnmountWindowsFolder([in, ref] LPCSTR LinuxPath);
705 + // Used only for testing (and by the plugin API). TODO: Think about moving them to a dedicated
706 + // testing-only interface. See CreateRootNamespaceProcess for 'AcquireVmLease'.
707 + HRESULT MountWindowsFolder([in, ref] LPCWSTR WindowsPath, [in, ref] LPCSTR LinuxPath, [in] BOOL ReadOnly, [in] BOOL AcquireVmLease);
708 + HRESULT UnmountWindowsFolder([in, ref] LPCSTR LinuxPath, [in] BOOL AcquireVmLease);
709 HRESULT MapVmPort([in] int Family, [in] unsigned short WindowsPort, [in] unsigned short LinuxPort);
710 HRESULT UnmapVmPort([in] int Family, [in] unsigned short WindowsPort, [in] unsigned short LinuxPort);
711
@@ -720,6 +738,20 @@ interface IWSLCSession : IUnknown
738 HRESULT PruneNetworks([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out, size_is(, *NetworksCount)] WSLCNetworkName** Networks, [out] ULONG* NetworksCount);
739
740 HRESULT RegisterCrashDumpCallback([in] ICrashDumpCallback* Callback, [out] IUnknown** Subscription);
741 +
742 + // Used only for testing. Synchronously runs the idle-termination teardown path. The production
743 + // activity-count and persistent-storage guards are honored, so an active container keeps the VM
744 + // alive and a tmpfs-backed session is never torn down.
745 + // WasAlreadyIdle is TRUE when the VM was not running.
746 + HRESULT TriggerIdleTermination([out] BOOL* WasAlreadyIdle);
747 +
748 + // Keeps the VM alive for the duration of a client-side container operation. The CLI performs
749 + // each mutation as two round-trips (OpenContainer followed by the operation) and may stream
750 + // output afterwards. With on-demand VM idle-termination the VM could otherwise tear down
751 + // between those calls, disconnecting the container wrapper and failing the second call with
752 + // RPC_E_DISCONNECTED. The client holds the returned token for the whole operation; releasing
753 + // it (or the client exiting) lets the VM idle-terminate again.
754 + HRESULT BeginContainerOperation([out] IUnknown** Operation);
755 }
756
757 //
@@ -807,3 +839,6 @@ cpp_quote("#define WSLC_E_CONTAINER_DISABLED MAKE_HRESULT(SEVERITY_ERROR, FACILI
839 cpp_quote("#define WSLC_E_REGISTRY_BLOCKED_BY_POLICY MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 13) /* 0x8004060D */")
840 cpp_quote("#define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */")
841 cpp_quote("#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 15) /* 0x8004060F */")
842 +// N.B. WSLC_E_VM_NOT_RUNNING is part of the plugin API contract and is also defined in WslPluginApi.h.
843 +// The two definitions must stay in sync.
844 +cpp_quote("#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */")
src/windows/wslc/services/ContainerService.cpp
+15
@@ -327,6 +327,7 @@ std::wstring ContainerService::FormatRelativeTime(ULONGLONG timestamp)
327
328 int ContainerService::Attach(Reporter& reporter, Session& session, const std::string& id)
329 {
330 + [[maybe_unused]] auto operation = session.BeginContainerOperation();
331 wil::com_ptr<IWSLCContainer> container;
332 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
333
@@ -487,6 +488,7 @@ CreateContainerResult ContainerService::Create(Reporter& reporter, Session& sess
488
489 int ContainerService::Start(Reporter& reporter, Session& session, const std::string& id, bool attach)
490 {
491 + [[maybe_unused]] auto operation = session.BeginContainerOperation();
492 wil::com_ptr<IWSLCContainer> container;
493 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
494 WSLCContainerStartFlags flags = attach ? WSLCContainerStartFlagsAttach : WSLCContainerStartFlagsNone;
@@ -517,6 +519,7 @@ int ContainerService::Start(Reporter& reporter, Session& session, const std::str
519
520 void ContainerService::Stop(Session& session, const std::string& id, StopContainerOptions options)
521 {
522 + [[maybe_unused]] auto operation = session.BeginContainerOperation();
523 wil::com_ptr<IWSLCContainer> container;
524 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
525 THROW_IF_FAILED_EXCEPT(container->Stop(options.Signal, options.Timeout), WSLC_E_CONTAINER_NOT_RUNNING);
@@ -524,6 +527,7 @@ void ContainerService::Stop(Session& session, const std::string& id, StopContain
527
528 void ContainerService::Kill(Session& session, const std::string& id, WSLCSignal signal)
529 {
530 + [[maybe_unused]] auto operation = session.BeginContainerOperation();
531 wil::com_ptr<IWSLCContainer> container;
532 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
533 THROW_IF_FAILED(container->Kill(signal));
@@ -531,6 +535,7 @@ void ContainerService::Kill(Session& session, const std::string& id, WSLCSignal
535
536 void ContainerService::Delete(Session& session, const std::string& id, bool force)
537 {
538 + [[maybe_unused]] auto operation = session.BeginContainerOperation();
539 wil::com_ptr<IWSLCContainer> container;
540 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
541 THROW_IF_FAILED(container->Delete(force ? WSLCDeleteFlagsForce : WSLCDeleteFlagsNone));
@@ -585,6 +590,7 @@ std::vector<ContainerInformation> ContainerService::List(
590
591 int ContainerService::Exec(Reporter& reporter, Session& session, const std::string& id, ContainerOptions options)
592 {
593 + [[maybe_unused]] auto operation = session.BeginContainerOperation();
594 wil::com_ptr<IWSLCContainer> container;
595 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
596
@@ -616,6 +622,7 @@ int ContainerService::Exec(Reporter& reporter, Session& session, const std::stri
622
623 InspectContainer ContainerService::Inspect(Session& session, const std::string& id)
624 {
625 + [[maybe_unused]] auto operation = session.BeginContainerOperation();
626 wil::com_ptr<IWSLCContainer> container;
627 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
628 wil::unique_cotaskmem_ansistring output;
@@ -634,6 +641,8 @@ void ContainerService::Export(Session& session, const std::string& id, const std
641
642 void ContainerService::Export(Session& session, const std::string& id, HANDLE outputHandle)
643 {
644 + [[maybe_unused]] auto operation = session.BeginContainerOperation();
645 +
646 wil::com_ptr<IWSLCContainer> container;
647 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
648
@@ -645,6 +654,8 @@ void ContainerService::Export(Session& session, const std::string& id, HANDLE ou
654
655 void ContainerService::CopyToContainer(Session& session, const std::string& id, const std::string& destPath, HANDLE inputHandle, ULONGLONG contentSize)
656 {
657 + [[maybe_unused]] auto operation = session.BeginContainerOperation();
658 +
659 wil::com_ptr<IWSLCContainer> container;
660 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
661
@@ -653,6 +664,8 @@ void ContainerService::CopyToContainer(Session& session, const std::string& id,
664
665 void ContainerService::CopyFromContainer(Session& session, const std::string& id, const std::string& srcPath, HANDLE outputHandle)
666 {
667 + [[maybe_unused]] auto operation = session.BeginContainerOperation();
668 +
669 wil::com_ptr<IWSLCContainer> container;
670 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
671
@@ -661,6 +674,7 @@ void ContainerService::CopyFromContainer(Session& session, const std::string& id
674
675 void ContainerService::Logs(Session& session, const std::string& id, bool follow, bool timestamps, ULONGLONG since, ULONGLONG until, ULONGLONG tail)
676 {
677 + [[maybe_unused]] auto operation = session.BeginContainerOperation();
678 wil::com_ptr<IWSLCContainer> container;
679 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
680
@@ -692,6 +706,7 @@ void ContainerService::Logs(Session& session, const std::string& id, bool follow
706
707 wsl::windows::common::docker_schema::ContainerStats ContainerService::Stats(Session& session, const std::string& id)
708 {
709 + [[maybe_unused]] auto operation = session.BeginContainerOperation();
710 wil::com_ptr<IWSLCContainer> container;
711 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
712 wil::unique_cotaskmem_ansistring output;
src/windows/wslc/services/SessionModel.h
+10
@@ -31,6 +31,16 @@ struct Session
31 return m_session.get();
32 }
33
34 + // Acquires an activity token that keeps the VM alive for the duration of a client-side
35 + // container operation (resolve + operate, plus any streamed output). Hold the returned
36 + // pointer for the whole operation; releasing it lets the VM idle-terminate again.
37 + [[nodiscard]] wil::com_ptr<IUnknown> BeginContainerOperation() const
38 + {
39 + wil::com_ptr<IUnknown> operation;
40 + THROW_IF_FAILED(m_session->BeginContainerOperation(&operation));
41 + return operation;
42 + }
43 +
44 private:
45 wil::com_ptr<IWSLCSession> m_session;
46 };
src/windows/wslc/services/SessionService.cpp
+3 -1
@@ -57,10 +57,12 @@ Session SessionService::OpenOrCreateDefaultSession(Reporter& reporter)
57 WarningCallback warningCallback(reporter);
58 auto manager = CreateSessionManager();
59
60 - // Null Settings = default session with server-determined name and settings.
60 + // Null Settings = default session with server-determined name and settings. The warning callback
61 + // is consumed during CreateSession (session initialization); it is not retained afterwards.
62 wil::com_ptr<IWSLCSession> session;
63 THROW_IF_FAILED(manager->CreateSession(nullptr, WSLCSessionFlagsNone, &warningCallback, &session));
64 wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
65 +
66 return Session(std::move(session));
67 }
68
src/windows/wslcsession/CMakeLists.txt
+3
@@ -9,6 +9,7 @@ set(SOURCES
9
10 # Session and container implementation
11 WSLCSession.cpp
12 + WSLCSessionRuntime.cpp
13 WSLCContainer.cpp
14 WSLCVirtualMachine.cpp
15
@@ -44,6 +45,8 @@ set(HEADERS
45 WSLCProcessControl.h
46 WSLCProcessIO.h
47 WSLCSession.h
48 + WSLCSessionRuntime.h
49 + WSLCIdleState.h
50 WSLCSessionFactory.h
51 WSLCSessionReference.h
52 WSLCVirtualMachine.h
src/windows/wslcsession/DockerEventTracker.cpp
+5 -1
@@ -61,7 +61,11 @@ DockerEventTracker::EventTrackingReference::~EventTrackingReference() noexcept
61 Reset();
62 }
63
64 -DockerEventTracker::DockerEventTracker(DockerHTTPClient& dockerClient, WSLCSession& session, IORelay& relay) : m_session(session)
64 +DockerEventTracker::DockerEventTracker(WSLCSession& session) : m_session(session)
65 +{
66 +}
67 +
68 +void DockerEventTracker::Connect(DockerHTTPClient& dockerClient, IORelay& relay)
69 {
70 auto onChunk = [this](const gsl::span<char>& buffer) {
71 if (!buffer.empty()) // docker inserts empty lines between events, skip those.
src/windows/wslcsession/DockerEventTracker.h
+6 -1
@@ -64,9 +64,14 @@ public:
64 using ContainerStateChangeCallback = std::function<void(ContainerEvent, std::optional<int>, std::uint64_t)>;
65 using VolumeEventCallback = std::function<void(const std::string&, VolumeEvent, std::uint64_t)>;
66
67 - DockerEventTracker(DockerHTTPClient& dockerClient, WSLCSession& session, IORelay& relay);
67 + explicit DockerEventTracker(WSLCSession& session);
68 ~DockerEventTracker();
69
70 + // Binds the tracker to a VM's docker client and IO relay. Called on every VM start. Existing
71 + // container/volume registrations are preserved across (re)connects so callers do not re-register
72 + // when the VM is idle-terminated and later restarted.
73 + void Connect(DockerHTTPClient& dockerClient, IORelay& relay);
74 +
75 EventTrackingReference RegisterContainerStateUpdates(const std::string& ContainerId, ContainerStateChangeCallback&& Callback) noexcept;
76 EventTrackingReference RegisterExecStateUpdates(const std::string& ContainerId, const std::string& ExecId, ContainerStateChangeCallback&& Callback) noexcept;
77 EventTrackingReference RegisterVolumeUpdates(VolumeEventCallback&& Callback) noexcept;
src/windows/wslcsession/IORelay.cpp
+9
@@ -68,11 +68,20 @@ void IORelay::Stop()
68 }
69 }
70
71 +bool IORelay::IsRelayThread() const noexcept
72 +{
73 + return m_thread.get_id() == std::this_thread::get_id();
74 +}
75 +
76 void IORelay::Run()
77 try
78 {
79 common::wslutil::SetThreadDescription(L"IORelay");
80
81 + // Handle callbacks dispatched from this thread (e.g. unexpected VM exit) can tear the VM down,
82 + // releasing cross-process COM proxies, so join the process MTA to avoid RPC_E_WRONG_THREAD.
83 + const auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED);
84 +
85 windows::common::io::MultiHandleWait io;
86
87 // N.B. All the IO must happen on the thread.
src/windows/wslcsession/IORelay.h
+6
@@ -30,6 +30,12 @@ public:
30
31 void Stop();
32
33 + // Returns true if the calling thread is the IORelay's own worker thread (i.e. the call
34 + // is being made from a handle callback). Destroying the IORelay from this thread would
35 + // join the thread with itself and call std::terminate(), so callers that may run on the
36 + // relay thread must check this before destroying the object.
37 + bool IsRelayThread() const noexcept;
38 +
39 private:
40 void Start();
41 void Run();
src/windows/wslcsession/WSLCContainer.cpp
+232 -99
@@ -36,8 +36,10 @@ using wsl::windows::common::io::OverlappedIOHandle;
36 using wsl::windows::common::io::ReadHandle;
37 using wsl::windows::common::io::RelayHandle;
38 using wsl::windows::service::wslc::ContainerPortMapping;
39 +using wsl::windows::service::wslc::DockerEventTracker;
40 using wsl::windows::service::wslc::DockerHTTPClient;
41 using wsl::windows::service::wslc::DockerHTTPException;
42 +using wsl::windows::service::wslc::IORelay;
43 using wsl::windows::service::wslc::IWSLCVolume;
44 using wsl::windows::service::wslc::NetworkEntry;
45 using wsl::windows::service::wslc::RelayedProcessIO;
@@ -53,6 +55,7 @@ using wsl::windows::service::wslc::WSLCPortMapping;
55 using wsl::windows::service::wslc::WSLCSession;
56 using wsl::windows::service::wslc::WSLCVirtualMachine;
57 using wsl::windows::service::wslc::WSLCVolumeMount;
58 +using wsl::windows::service::wslc::WSLCVolumes;
59
60 using namespace wsl::windows::common::io;
61 using namespace wsl::windows::common::docker_schema;
@@ -587,7 +590,7 @@ WSLCPortMapping ContainerPortMapping::Serialize() const
590
591 WSLCContainerImpl::WSLCContainerImpl(
592 WSLCSession& wslcSession,
590 - WSLCVirtualMachine& virtualMachine,
593 + WSLCSessionRuntime& runtime,
594 IWSLCPluginNotifier* pluginNotifier,
595 std::string&& Id,
596 std::string&& Name,
@@ -595,85 +598,90 @@ WSLCContainerImpl::WSLCContainerImpl(
598 std::string NetworkMode,
599 std::vector<WSLCVolumeMount>&& volumes,
600 std::vector<std::string>&& namedVolumes,
598 - WSLCVolumes& Volumes,
601 std::vector<ContainerPortMapping>&& ports,
602 std::map<std::string, std::string>&& labels,
603 std::function<void(const WSLCContainerImpl*)>&& onDeleted,
602 - DockerEventTracker& EventTracker,
603 - DockerHTTPClient& DockerClient,
604 - IORelay& Relay,
604 WSLCContainerState InitialState,
605 std::uint64_t CreatedAt,
606 WSLCProcessFlags InitProcessFlags,
607 WSLCContainerFlags ContainerFlags) :
608 m_wslcSession(wslcSession),
609 m_pluginNotifier(pluginNotifier),
611 - m_virtualMachine(virtualMachine),
610 + m_runtime(runtime),
611 m_name(std::move(Name)),
612 m_image(std::move(Image)),
613 m_networkMode(std::move(NetworkMode)),
614 m_id(std::move(Id)),
615 m_mountedVolumes(std::move(volumes)),
616 m_namedVolumes(std::move(namedVolumes)),
618 - m_volumes(Volumes),
617 m_mappedPorts(std::move(ports)),
618 m_labels(std::move(labels)),
619 m_comWrapper(wil::MakeOrThrow<WSLCContainer>(wslcSession, std::move(onDeleted))),
622 - m_dockerClient(DockerClient),
623 - m_eventTracker(EventTracker),
624 - m_ioRelay(Relay),
625 - m_containerEvents(EventTracker.RegisterContainerStateUpdates(
620 + m_containerEvents(runtime.Events().RegisterContainerStateUpdates(
621 m_id, std::bind(&WSLCContainerImpl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))),
622 m_state(InitialState),
623 m_createdAt(CreatedAt),
624 m_initProcessFlags(InitProcessFlags),
625 m_containerFlags(ContainerFlags)
626 {
627 + // Acquire the activity hold up front for a container recovered in the running state, so it keeps
628 + // the VM alive even before any client opens its wrapper. A merely-created (never-started)
629 + // container does not pin the VM: its metadata survives teardown and the VM restarts on next use.
630 + if (m_state == WslcContainerStateRunning)
631 + {
632 + m_activityHold = ActivityRef(m_wslcSession.Runtime().IdleStateShared());
633 + }
634 }
635
636 WSLCContainerImpl::~WSLCContainerImpl()
637 {
636 - WSL_LOG(
637 - "~WSLCContainerImpl",
638 - TraceLoggingValue(m_name.c_str(), "Name"),
639 - TraceLoggingValue(m_id.c_str(), "Id"),
640 - TraceLoggingValue((int)m_state, "State"));
638 + // Destructors are implicitly noexcept, so any escaping exception terminates the session host.
639 + // Everything below touches VM-scoped state that may already be gone.
640 + try
641 + {
642 + WSL_LOG(
643 + "~WSLCContainerImpl",
644 + TraceLoggingValue(m_name.c_str(), "Name"),
645 + TraceLoggingValue(m_id.c_str(), "Id"),
646 + TraceLoggingValue((int)m_state, "State"));
647
642 - // Snapshot and clear process references under the lock.
643 - // Callbacks are then invoked without holding m_lock.
644 - decltype(m_processes) processes;
645 - decltype(m_initProcessControl) initProcessControl = nullptr;
648 + // Snapshot and clear process references under the lock.
649 + // Callbacks are then invoked without holding m_lock.
650 + decltype(m_processes) processes;
651 + decltype(m_initProcessControl) initProcessControl = nullptr;
652
647 - {
648 - auto lock = m_lock.lock_exclusive();
649 - std::lock_guard processesLock{m_processesLock};
650 - initProcessControl = std::exchange(m_initProcessControl, nullptr);
651 - processes = std::exchange(m_processes, {});
652 - }
653 + {
654 + auto lock = m_lock.lock_exclusive();
655 + std::lock_guard processesLock{m_processesLock};
656 + initProcessControl = std::exchange(m_initProcessControl, nullptr);
657 + processes = std::exchange(m_processes, {});
658 + }
659
654 - if (initProcessControl)
655 - {
656 - initProcessControl->OnContainerReleased();
657 - }
660 + if (initProcessControl)
661 + {
662 + initProcessControl->OnContainerReleased();
663 + }
664
659 - for (auto& process : processes)
660 - {
661 - if (auto control = process.lock())
665 + for (auto& process : processes)
666 {
663 - control->OnContainerReleased();
667 + if (auto control = process.lock())
668 + {
669 + control->OnContainerReleased();
670 + }
671 }
665 - }
672
667 - m_containerEvents.Reset();
673 + m_containerEvents.Reset();
674
669 - // Release resources under m_lock, but extract the COM wrapper so Disconnect()
670 - // can be called without holding m_lock. Calling Disconnect() under m_lock can
671 - // deadlock if an in-flight COM caller is waiting for m_lock.
672 - unique_com_disconnect wrapper;
673 - {
674 - auto lock = m_lock.lock_exclusive();
675 - wrapper = ReleaseResources();
675 + // Release resources under m_lock, but extract the COM wrapper so Disconnect()
676 + // can be called without holding m_lock. Calling Disconnect() under m_lock can
677 + // deadlock if an in-flight COM caller is waiting for m_lock.
678 + unique_com_disconnect wrapper;
679 + {
680 + auto lock = m_lock.lock_exclusive();
681 + wrapper = ReleaseResources();
682 + }
683 }
684 + CATCH_LOG()
685 }
686
687 void WSLCContainerImpl::Initialize()
@@ -758,7 +766,7 @@ void WSLCContainerImpl::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle*
766
767 try
768 {
761 - ioHandle = m_dockerClient.AttachContainer(m_id, DetachKeys == nullptr ? std::nullopt : std::optional<std::string>(DetachKeys));
769 + ioHandle = m_runtime.Docker().AttachContainer(m_id, DetachKeys == nullptr ? std::nullopt : std::optional<std::string>(DetachKeys));
770 }
771 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to attach to container '%hs'", m_id.c_str());
772
@@ -789,7 +797,7 @@ void WSLCContainerImpl::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle*
797 handles.emplace_back(std::make_unique<DockerIORelayHandle>(
798 std::move(ioHandle), std::move(stdoutWrite), std::move(stderrWrite), DockerIORelayHandle::Format::Raw));
799
792 - m_ioRelay.AddHandles(std::move(handles));
800 + m_runtime.Relay()->AddHandles(std::move(handles));
801
802 *Stdin = common::wslutil::ToCOMOutputHandle(reinterpret_cast<HANDLE>(stdinWrite.get()), GENERIC_WRITE | SYNCHRONIZE, WSLCHandleTypePipe);
803
@@ -836,11 +844,11 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
844 if (WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty))
845 {
846 io = std::make_unique<TTYProcessIO>(TypedHandle{
839 - wil::unique_handle{(HANDLE)m_dockerClient.AttachContainer(m_id, detachKeys).release()}, WSLCHandleTypeSocket});
847 + wil::unique_handle{(HANDLE)m_runtime.Docker().AttachContainer(m_id, detachKeys).release()}, WSLCHandleTypeSocket});
848 }
849 else
850 {
843 - wil::unique_handle stream{reinterpret_cast<HANDLE>(m_dockerClient.AttachContainer(m_id, detachKeys).release())};
851 + wil::unique_handle stream{reinterpret_cast<HANDLE>(m_runtime.Docker().AttachContainer(m_id, detachKeys).release())};
852 io = CreateRelayedProcessIO(std::move(stream), m_initProcessFlags);
853 }
854 }
@@ -851,7 +859,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
859 THROW_DOCKER_USER_ERROR_MSG(e, "Failed to attach to container '%hs' during start", m_id.c_str());
860 }
861
854 - auto control = std::make_unique<DockerContainerProcessControl>(*this, m_dockerClient);
862 + auto control = std::make_unique<DockerContainerProcessControl>(*this, m_runtime.Docker());
863
864 std::lock_guard processesLock{m_processesLock};
865 m_initProcessControl = control.get();
@@ -867,7 +875,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
875 std::vector<std::string> unavailableVolumes;
876 for (const auto& volumeName : m_namedVolumes)
877 {
870 - const auto [code, message] = m_volumes.GetVolumeStatus(volumeName);
878 + const auto [code, message] = m_runtime.Volumes().GetVolumeStatus(volumeName);
879 if (FAILED(code))
880 {
881 EMIT_USER_WARNING(Localization::MessageWslcVolumeNotAvailableReason(volumeName, message));
@@ -880,7 +888,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
888 Localization::MessageWslcVolumeNotAvailable(wsl::shared::string::Join(unavailableVolumes, ',')),
889 !unavailableVolumes.empty());
890
883 - auto volumeCleanup = MountVolumes(m_mountedVolumes, m_virtualMachine);
891 + auto volumeCleanup = MountVolumes(m_mountedVolumes, m_runtime.Vm());
892
893 auto portCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { UnmapPorts(); });
894 MapPorts();
@@ -890,7 +898,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
898
899 try
900 {
893 - m_dockerClient.StartContainer(m_id, detachKeys);
901 + m_runtime.Docker().StartContainer(m_id, detachKeys);
902 }
903 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to start container '%hs'", m_id.c_str());
904
@@ -898,7 +906,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
906 {
907 try
908 {
901 - m_dockerClient.ResizeContainerTty(m_id, StartOptions->TtyRows, StartOptions->TtyColumns);
909 + m_runtime.Docker().ResizeContainerTty(m_id, StartOptions->TtyRows, StartOptions->TtyColumns);
910 }
911 CATCH_LOG();
912 }
@@ -913,7 +921,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
921 LOG_HR_MSG(pluginResult, "Plugin rejected start of container '%hs' (0x%x)", m_id.c_str(), pluginResult);
922 try
923 {
916 - m_dockerClient.StopContainer(m_id.c_str(), {}, {});
924 + m_runtime.Docker().StopContainer(m_id.c_str(), {}, {});
925 }
926 catch (...)
927 {
@@ -1027,7 +1035,7 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill)
1035 {
1036 if (Kill)
1037 {
1030 - m_dockerClient.SignalContainer(m_id, SignalArg);
1038 + m_runtime.Docker().SignalContainer(m_id, SignalArg);
1039
1040 if (!waitForStop)
1041 {
@@ -1042,7 +1050,7 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill)
1050 TimeoutArg = TimeoutSeconds;
1051 }
1052
1045 - m_dockerClient.StopContainer(m_id, SignalArg, TimeoutArg);
1053 + m_runtime.Docker().StopContainer(m_id, SignalArg, TimeoutArg);
1054 }
1055 }
1056 catch (const DockerHTTPException& e)
@@ -1112,6 +1120,43 @@ __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::
1120 return comWrapper;
1121 }
1122
1123 +void WSLCContainerImpl::RecoverPorts(const common::docker_schema::ContainerInfo& dockerContainer)
1124 +{
1125 + auto lock = m_lock.lock_exclusive();
1126 +
1127 + // Re-register VM-scoped port reservations against the restarted VM using the numbers recorded at
1128 + // create time, restoring bridge-mode forwarding when the stopped container starts again.
1129 + const bool allocateVmPorts = NetworkModeAllocatesVmPorts(m_networkMode);
1130 + if (!allocateVmPorts)
1131 + {
1132 + return;
1133 + }
1134 +
1135 + auto metadataIt = dockerContainer.Labels.find(WSLCContainerMetadataLabel);
1136 + if (metadataIt == dockerContainer.Labels.end())
1137 + {
1138 + return;
1139 + }
1140 +
1141 + auto metadata = ParseContainerMetadata(metadataIt->second.c_str());
1142 +
1143 + std::vector<ContainerPortMapping> ports;
1144 + ports.reserve(metadata.Ports.size());
1145 + for (const auto& e : metadata.Ports)
1146 + {
1147 + auto& inserted = ports.emplace_back(ContainerPortMapping{VMPortMapping::FromContainerMetaData(e), e.ContainerPort});
1148 +
1149 + auto allocation = m_runtime.Vm().TryAllocatePort(e.VmPort, e.Family, e.Protocol);
1150 +
1151 + THROW_HR_IF_MSG(
1152 + HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), !allocation, "Port %hu is in use, cannot recover container %hs", e.VmPort, m_id.c_str());
1153 +
1154 + inserted.VmMapping.AssignVmPort(allocation);
1155 + }
1156 +
1157 + m_mappedPorts = std::move(ports);
1158 +}
1159 +
1160 void WSLCContainerImpl::Delete(WSLCDeleteFlags Flags)
1161 {
1162 // N.B. wrapper must be destroyed after m_lock is released, since its destructor calls Disconnect().
@@ -1144,7 +1189,7 @@ __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::
1189
1190 try
1191 {
1147 - m_dockerClient.DeleteContainer(m_id, WI_IsFlagSet(Flags, WSLCDeleteFlagsForce), WI_IsFlagSet(Flags, WSLCDeleteFlagsDeleteVolumes));
1192 + m_runtime.Docker().DeleteContainer(m_id, WI_IsFlagSet(Flags, WSLCDeleteFlagsForce), WI_IsFlagSet(Flags, WSLCDeleteFlagsDeleteVolumes));
1193 }
1194 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to delete container '%hs'", m_id.c_str());
1195
@@ -1160,7 +1205,7 @@ void WSLCContainerImpl::Export(WSLCHandle OutHandle) const
1205 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_IS_RUNNING, Localization::MessageWslcContainerIsRunning(m_id), m_state == WslcContainerStateRunning);
1206
1207 std::pair<uint32_t, wil::unique_socket> SocketCodePair;
1163 - SocketCodePair = m_dockerClient.ExportContainer(m_id);
1208 + SocketCodePair = m_runtime.Docker().ExportContainer(m_id);
1209
1210 auto userHandle = m_wslcSession.OpenUserHandle(OutHandle);
1211
@@ -1209,7 +1254,7 @@ void WSLCContainerImpl::UploadArchive(WSLCHandle TarHandle, LPCSTR DestPath, ULO
1254 contentLength = ContentSize;
1255 }
1256
1212 - auto requestContext = m_dockerClient.PutArchive(m_id, DestPath, contentLength);
1257 + auto requestContext = m_runtime.Docker().PutArchive(m_id, DestPath, contentLength);
1258
1259 auto userHandle = m_wslcSession.OpenUserHandle(TarHandle);
1260
@@ -1265,7 +1310,7 @@ void WSLCContainerImpl::DownloadArchive(LPCSTR SrcPath, WSLCHandle OutHandle) co
1310 {
1311 auto lock = m_lock.lock_shared();
1312
1268 - auto [statusCode, socket, isChunked] = m_dockerClient.GetArchive(m_id, SrcPath);
1313 + auto [statusCode, socket, isChunked] = m_runtime.Docker().GetArchive(m_id, SrcPath);
1314
1315 auto userHandle = m_wslcSession.OpenUserHandle(OutHandle);
1316
@@ -1387,12 +1432,12 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, const WSLCProces
1432
1433 try
1434 {
1390 - auto result = m_dockerClient.CreateExec(m_id, request);
1435 + auto result = m_runtime.Docker().CreateExec(m_id, request);
1436
1437 // N.B. There's no way to delete a created exec instance, it is removed when the container is deleted.
1438
1439 wil::unique_handle stream{
1395 - (HANDLE)m_dockerClient
1440 + (HANDLE)m_runtime.Docker()
1441 .StartExec(result.Id, common::docker_schema::StartExec{.Tty = request.Tty, .ConsoleSize = request.ConsoleSize})
1442 .release()};
1443
@@ -1406,7 +1451,7 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, const WSLCProces
1451 io = CreateRelayedProcessIO(std::move(stream), Options->Flags);
1452 }
1453
1409 - auto control = std::make_shared<DockerExecProcessControl>(*this, result.Id, m_dockerClient, m_eventTracker);
1454 + auto control = std::make_shared<DockerExecProcessControl>(*this, result.Id, m_runtime.Docker(), m_runtime.Events());
1455
1456 {
1457 std::lock_guard processesLock{m_processesLock};
@@ -1429,7 +1474,7 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, const WSLCProces
1474
1475 do
1476 {
1432 - auto state = m_dockerClient.InspectExec(result.Id);
1477 + auto state = m_runtime.Docker().InspectExec(result.Id);
1478 if (state.Running && state.Pid > 0)
1479 {
1480 control->SetPid(state.Pid);
@@ -1452,6 +1497,12 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, const WSLCProces
1497 } while (!control->GetExitEvent().wait(100));
1498
1499 auto process = wil::MakeOrThrow<WSLCProcess>(std::move(control), std::move(io), Options->Flags);
1500 +
1501 + // The exec'd process wrapper is handed to the client and is not retained internally, so its
1502 + // lifetime tracks the client's proxy. Bind a keep-alive token to it so the idle worker does
1503 + // not tear the VM down (killing the process) while the client still holds the proxy.
1504 + process->SetKeepAliveToken(m_wslcSession.CreateActivityToken());
1505 +
1506 THROW_IF_FAILED(process.CopyTo(__uuidof(IWSLCProcess), (void**)Process));
1507 }
1508 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to exec process in container %hs", m_id.c_str());
@@ -1604,15 +1655,15 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1655 const WSLCContainerOptions& containerOptions,
1656 const std::string& containerName,
1657 WSLCSession& wslcSession,
1607 - WSLCVirtualMachine& virtualMachine,
1658 + WSLCSessionRuntime& runtime,
1659 IWSLCPluginNotifier* pluginNotifier,
1660 const std::unordered_map<std::string, NetworkEntry>& sessionNetworks,
1610 - WSLCVolumes& volumesManager,
1611 - std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
1612 - DockerEventTracker& EventTracker,
1613 - DockerHTTPClient& DockerClient,
1614 - IORelay& IoRelay)
1661 + std::function<void(const WSLCContainerImpl*)>&& OnDeleted)
1662 {
1663 + auto& virtualMachine = runtime.Vm();
1664 + auto& DockerClient = runtime.Docker();
1665 + auto& EventTracker = runtime.Events();
1666 +
1667 common::docker_schema::CreateContainer request;
1668 request.Image = containerOptions.Image;
1669
@@ -2057,7 +2108,7 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2108
2109 auto container = std::make_shared<WSLCContainerImpl>(
2110 wslcSession,
2060 - virtualMachine,
2111 + runtime,
2112 pluginNotifier,
2113 std::move(result.Id),
2114 CleanContainerName(inspectData.Name),
@@ -2065,13 +2116,9 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2116 std::move(networkMode),
2117 std::move(volumes),
2118 std::move(namedVolumes),
2068 - volumesManager,
2119 std::move(mappedPorts),
2120 std::move(labels),
2121 std::move(OnDeleted),
2072 - EventTracker,
2073 - DockerClient,
2074 - IoRelay,
2122 WslcContainerStateCreated,
2123 ParseDockerTimestamp(inspectData.Created),
2124 containerOptions.InitProcessOptions.Flags,
@@ -2086,14 +2133,13 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2133 std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
2134 const common::docker_schema::ContainerInfo& dockerContainer,
2135 WSLCSession& wslcSession,
2089 - WSLCVirtualMachine& virtualMachine,
2136 + WSLCSessionRuntime& runtime,
2137 IWSLCPluginNotifier* pluginNotifier,
2091 - WSLCVolumes& volumes,
2092 - std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
2093 - DockerEventTracker& EventTracker,
2094 - DockerHTTPClient& DockerClient,
2095 - IORelay& ioRelay)
2138 + std::function<void(const WSLCContainerImpl*)>&& OnDeleted)
2139 {
2140 + auto& virtualMachine = runtime.Vm();
2141 + auto& DockerClient = runtime.Docker();
2142 +
2143 // Extract container name from Docker's names list.
2144 std::string name = ExtractContainerName(dockerContainer.Names, dockerContainer.Id);
2145
@@ -2151,7 +2197,7 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
2197
2198 auto container = std::make_shared<WSLCContainerImpl>(
2199 wslcSession,
2154 - virtualMachine,
2200 + runtime,
2201 pluginNotifier,
2202 std::string(dockerContainer.Id),
2203 std::move(name),
@@ -2159,13 +2205,9 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
2205 std::move(networkMode),
2206 std::move(metadata.Volumes),
2207 std::move(namedVolumes),
2162 - volumes,
2208 std::move(ports),
2209 std::move(labels),
2210 std::move(OnDeleted),
2166 - EventTracker,
2167 - DockerClient,
2168 - ioRelay,
2211 DockerStateToWSLCState(dockerContainer.State),
2212 static_cast<std::uint64_t>(dockerContainer.Created),
2213 metadata.InitProcessFlags,
@@ -2178,11 +2220,21 @@ std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
2220 {
2221 auto inspectData = DockerClient.InspectContainer(dockerContainer.Id);
2222 auto state = DockerStateToWSLCState(dockerContainer.State);
2181 - const auto& timestamp = (state == WslcContainerStateRunning) ? inspectData.State.StartedAt : inspectData.State.FinishedAt;
2223
2183 - if (!timestamp.empty())
2224 + if (state == WslcContainerStateCreated)
2225 {
2185 - container->m_stateChangedAt = ParseDockerTimestamp(timestamp);
2226 + // A created-but-never-started container has no StartedAt/FinishedAt; its state last
2227 + // changed when it was created.
2228 + container->m_stateChangedAt = static_cast<std::uint64_t>(dockerContainer.Created);
2229 + }
2230 + else
2231 + {
2232 + const auto& timestamp = (state == WslcContainerStateRunning) ? inspectData.State.StartedAt : inspectData.State.FinishedAt;
2233 +
2234 + if (!timestamp.empty())
2235 + {
2236 + container->m_stateChangedAt = ParseDockerTimestamp(timestamp);
2237 + }
2238 }
2239 }
2240 catch (...)
@@ -2214,7 +2266,7 @@ void WSLCContainerImpl::Inspect(LPSTR* Output) const
2266 std::string WSLCContainerImpl::InspectLockHeld() const
2267 {
2268 // Get Docker inspect data
2217 - auto dockerInspect = m_dockerClient.InspectContainer(m_id);
2269 + auto dockerInspect = m_runtime.Docker().InspectContainer(m_id);
2270
2271 // Convert to WSLC schema
2272 auto wslcInspect = BuildInspectContainer(dockerInspect);
@@ -2230,7 +2282,7 @@ void WSLCContainerImpl::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle
2282 wil::unique_socket socket;
2283 try
2284 {
2233 - socket = m_dockerClient.ContainerLogs(m_id, Flags, Since, Until, Tail);
2285 + socket = m_runtime.Docker().ContainerLogs(m_id, Flags, Since, Until, Tail);
2286 }
2287 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to get logs from '%hs'", m_id.c_str());
2288
@@ -2240,7 +2292,7 @@ void WSLCContainerImpl::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle
2292 auto [ttyRead, ttyWrite] = common::wslutil::OpenAnonymousPipe(0, true, true);
2293
2294 auto handle = std::make_unique<RelayHandle<HTTPChunkBasedReadHandle>>(std::move(socket), std::move(ttyWrite));
2243 - m_ioRelay.AddHandle(std::move(handle));
2295 + m_runtime.Relay()->AddHandle(std::move(handle));
2296
2297 *Stdout = common::wslutil::ToCOMOutputHandle(ttyRead.get(), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe);
2298 }
@@ -2253,7 +2305,7 @@ void WSLCContainerImpl::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle
2305 auto handle = std::make_unique<DockerIORelayHandle>(
2306 std::move(socket), std::move(stdoutWrite), std::move(stderrWrite), DockerIORelayHandle::Format::HttpChunked);
2307
2256 - m_ioRelay.AddHandle(std::move(handle));
2308 + m_runtime.Relay()->AddHandle(std::move(handle));
2309
2310 *Stdout = common::wslutil::ToCOMOutputHandle(stdoutRead.get(), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe);
2311 *Stderr = common::wslutil::ToCOMOutputHandle(stderrRead.get(), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe);
@@ -2266,7 +2318,7 @@ void WSLCContainerImpl::Stats(LPSTR* Output) const
2318
2319 try
2320 {
2269 - auto stats = m_dockerClient.ContainerStats(m_id);
2321 + auto stats = m_runtime.Docker().ContainerStats(m_id);
2322
2323 // Always inject the authoritative id and name from this instance.
2324 // The response may omit them or use inconsistent casing.
@@ -2313,7 +2365,7 @@ std::unique_ptr<RelayedProcessIO> WSLCContainerImpl::CreateRelayedProcessIO(wil:
2365 ioHandles.emplace_back(std::make_unique<DockerIORelayHandle>(
2366 std::move(stream), std::move(stdoutWrite), std::move(stderrWrite), common::io::DockerIORelayHandle::Format::Raw));
2367
2316 - m_ioRelay.AddHandles(std::move(ioHandles));
2368 + m_runtime.Relay()->AddHandles(std::move(ioHandles));
2369
2370 return std::make_unique<RelayedProcessIO>(std::move(fds));
2371 }
@@ -2338,7 +2390,7 @@ void WSLCContainerImpl::MapPorts()
2390 else
2391 {
2392 auto allocatedPort =
2341 - m_virtualMachine.TryAllocatePort(e.ContainerPort, e.VmMapping.BindAddress.si_family, e.VmMapping.Protocol);
2393 + m_runtime.Vm().TryAllocatePort(e.ContainerPort, e.VmMapping.BindAddress.si_family, e.VmMapping.Protocol);
2394
2395 THROW_HR_WITH_USER_ERROR_IF(
2396 HRESULT_FROM_WIN32(WSAEADDRINUSE), wsl::shared::Localization::MessageWslcPortInUse(FormatPortEndpoint(e), m_id), !allocatedPort);
@@ -2351,7 +2403,7 @@ void WSLCContainerImpl::MapPorts()
2403
2404 try
2405 {
2354 - m_virtualMachine.MapPort(e.VmMapping);
2406 + m_runtime.Vm().MapPort(e.VmMapping);
2407 }
2408 catch (...)
2409 {
@@ -2413,7 +2465,25 @@ __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::ReleaseRuntimeRes
2465
2466 // Release runtime resources (port relays, volume mounts) that were set up at Start().
2467 UnmapPorts();
2416 - UnmountVolumes(m_mountedVolumes, m_virtualMachine);
2468 +
2469 + // A VM that already exited (crash / external kill) has dropped every guest mount, so calling
2470 + // UnmountWindowsFolder would only block on the RPC timeout and emit spurious unmount-failed
2471 + // warnings. Mark the mounts inactive without touching the dead VM.
2472 + //
2473 + // The same applies when there is no VM at all: after a graceful idle teardown the VM object is
2474 + // released without the exit event ever being signaled, so VmExited() is false while HasVm() is
2475 + // false too. m_runtime.Vm() would throw on that state, and this runs from ~WSLCContainerImpl.
2476 + if (m_runtime.VmExited() || !m_runtime.HasVm())
2477 + {
2478 + for (auto& volume : m_mountedVolumes)
2479 + {
2480 + volume.Mounted = false;
2481 + }
2482 + }
2483 + else
2484 + {
2485 + UnmountVolumes(m_mountedVolumes, m_runtime.Vm());
2486 + }
2487 }
2488
2489 __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::ReleaseResources()
@@ -2459,6 +2529,25 @@ __requires_lock_held(m_lock) void WSLCContainerImpl::Transition(WSLCContainerSta
2529
2530 m_state = State;
2531 m_stateChangedAt = stateChangedAt.value_or(static_cast<std::uint64_t>(std::time(nullptr)));
2532 +
2533 + // Keep the VM alive while this container is Running and release the hold once it leaves that
2534 + // state, even when no client holds the wrapper (e.g. a detached `run -d` container). Dropping
2535 + // the hold on the transition out of Running is what lets an otherwise-idle VM be torn down; a
2536 + // Created or Exited container does not pin the VM, since its metadata survives teardown.
2537 + UpdateActivityHoldLockHeld();
2538 +}
2539 +
2540 +__requires_lock_held(m_lock) void WSLCContainerImpl::UpdateActivityHoldLockHeld() noexcept
2541 +{
2542 + const bool active = (m_state == WslcContainerStateRunning);
2543 + if (active && !m_activityHold)
2544 + {
2545 + m_activityHold = ActivityRef(m_wslcSession.Runtime().IdleStateShared());
2546 + }
2547 + else if (!active && m_activityHold)
2548 + {
2549 + m_activityHold.reset();
2550 + }
2551 }
2552
2553 WSLCContainer::WSLCContainer(WSLCSession& session, std::function<void(const WSLCContainerImpl*)>&& OnDeleted) :
@@ -2467,6 +2556,7 @@ WSLCContainer::WSLCContainer(WSLCSession& session, std::function<void(const WSLC
2556 }
2557
2558 HRESULT WSLCContainer::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* Stdout, WSLCHandle* Stderr)
2559 +try
2560 {
2561 WSLCExecutionContext context(&m_session);
2562
@@ -2478,8 +2568,10 @@ HRESULT WSLCContainer::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle*
2568 *Stdout = {};
2569 *Stderr = {};
2570
2571 + auto vmLease = m_session.Runtime().AcquireVmLease();
2572 return CallImpl(&WSLCContainerImpl::Attach, DetachKeys, Stdin, Stdout, Stderr);
2573 }
2574 +CATCH_RETURN();
2575
2576 HRESULT WSLCContainer::GetState(WSLCContainerState* Result)
2577 {
@@ -2537,6 +2629,7 @@ HRESULT WSLCContainer::GetInitProcess(IWSLCProcess** Process)
2629 }
2630
2631 HRESULT WSLCContainer::Exec(const WSLCProcessOptions* Options, const WSLCProcessStartOptions* StartOptions, IWSLCProcess** Process)
2632 +try
2633 {
2634 WSLCExecutionContext context(&m_session);
2635
@@ -2545,22 +2638,35 @@ HRESULT WSLCContainer::Exec(const WSLCProcessOptions* Options, const WSLCProcess
2638 RETURN_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Options->Flags, ~WSLCProcessFlagsValid), "Invalid flags: 0x%x", Options->Flags);
2639
2640 *Process = nullptr;
2641 +
2642 + auto vmLease = m_session.Runtime().AcquireVmLease();
2643 return CallImpl(&WSLCContainerImpl::Exec, Options, StartOptions, Process);
2644 }
2645 +CATCH_RETURN();
2646
2647 HRESULT WSLCContainer::Stop(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds)
2648 +try
2649 {
2650 WSLCExecutionContext context(&m_session);
2651
2652 + // Hold a VM lease for the whole operation: --rm containers self-delete during Stop, which
2653 + // disconnects the wrapper and drops activity. Without the lease, the idle worker can fire
2654 + // during the post-stop destroy wait (up to 60s) and tear the VM down mid-call.
2655 + auto vmLease = m_session.Runtime().AcquireVmLease();
2656 return CallImpl(&WSLCContainerImpl::Stop, Signal, TimeoutSeconds, false);
2657 }
2658 +CATCH_RETURN();
2659
2660 HRESULT WSLCContainer::Kill(_In_ WSLCSignal Signal)
2661 +try
2662 {
2663 WSLCExecutionContext context(&m_session);
2664
2665 + // Hold a VM lease for the same reason as Stop(): --rm can self-delete and drop activity.
2666 + auto vmLease = m_session.Runtime().AcquireVmLease();
2667 return CallImpl(&WSLCContainerImpl::Stop, Signal, {}, true);
2668 }
2669 +CATCH_RETURN();
2670
2671 HRESULT WSLCContainer::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, IWarningCallback* WarningCallback)
2672 try
@@ -2569,11 +2675,13 @@ try
2675
2676 THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCContainerStartFlagsValid), "Invalid flags: 0x%x", Flags);
2677
2678 + auto vmLease = m_session.Runtime().AcquireVmLease();
2679 return CallImpl(&WSLCContainerImpl::Start, Flags, StartOptions);
2680 }
2681 CATCH_RETURN();
2682
2683 HRESULT WSLCContainer::Inspect(LPSTR* Output)
2684 +try
2685 {
2686 WSLCExecutionContext context(&m_session);
2687
@@ -2581,8 +2689,10 @@ HRESULT WSLCContainer::Inspect(LPSTR* Output)
2689
2690 *Output = nullptr;
2691
2692 + auto vmLease = m_session.Runtime().AcquireVmLease();
2693 return CallImpl(&WSLCContainerImpl::Inspect, Output);
2694 }
2695 +CATCH_RETURN();
2696
2697 HRESULT WSLCContainer::Stats(LPSTR* Output)
2698 try
@@ -2592,6 +2702,8 @@ try
2702 RETURN_HR_IF(E_POINTER, Output == nullptr);
2703
2704 *Output = nullptr;
2705 +
2706 + auto vmLease = m_session.Runtime().AcquireVmLease();
2707 return CallImpl(&WSLCContainerImpl::Stats, Output);
2708 }
2709 CATCH_RETURN();
@@ -2604,6 +2716,11 @@ try
2716 THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCDeleteFlagsValid), "Invalid flags: 0x%x", Flags);
2717
2718 // Special case for Delete(): If deletion is successful, notify the WSLCSession that the container has been deleted.
2719 + // Hold a VM lease across the whole operation: deleting a container makes it inactive and
2720 + // can trigger an idle teardown. Without the lease the idle worker could take the session
2721 + // lock exclusively and clear m_containers (destroying this container) concurrently, racing
2722 + // the delete and inverting the container->session lock order.
2723 + auto vmLease = m_session.Runtime().AcquireVmLease();
2724 auto [lock, impl] = LockImpl();
2725
2726 impl->Delete(Flags);
@@ -2629,29 +2746,40 @@ try
2746 CATCH_LOG();
2747
2748 HRESULT WSLCContainer::Export(WSLCHandle TarHandle)
2749 +try
2750 {
2751 WSLCExecutionContext context(&m_session);
2752
2753 + auto vmLease = m_session.Runtime().AcquireVmLease();
2754 return CallImpl(&WSLCContainerImpl::Export, TarHandle);
2755 }
2756 +CATCH_RETURN();
2757
2758 HRESULT WSLCContainer::UploadArchive(WSLCHandle TarHandle, LPCSTR DestPath, ULONGLONG ContentSize)
2759 +try
2760 {
2761 WSLCExecutionContext context(&m_session);
2762
2763 RETURN_HR_IF(E_POINTER, DestPath == nullptr);
2764 RETURN_HR_IF(E_INVALIDARG, DestPath[0] == '\0');
2765 +
2766 + auto vmLease = m_session.Runtime().AcquireVmLease();
2767 return CallImpl(&WSLCContainerImpl::UploadArchive, TarHandle, DestPath, ContentSize);
2768 }
2769 +CATCH_RETURN();
2770
2771 HRESULT WSLCContainer::DownloadArchive(LPCSTR SrcPath, WSLCHandle OutHandle)
2772 +try
2773 {
2774 WSLCExecutionContext context(&m_session);
2775
2776 RETURN_HR_IF(E_POINTER, SrcPath == nullptr);
2777 RETURN_HR_IF(E_INVALIDARG, SrcPath[0] == '\0');
2778 +
2779 + auto vmLease = m_session.Runtime().AcquireVmLease();
2780 return CallImpl(&WSLCContainerImpl::DownloadArchive, SrcPath, OutHandle);
2781 }
2782 +CATCH_RETURN();
2783
2784 HRESULT WSLCContainer::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail)
2785 try
@@ -2664,6 +2792,7 @@ try
2792 *Stdout = {};
2793 *Stderr = {};
2794
2795 + auto vmLease = m_session.Runtime().AcquireVmLease();
2796 return CallImpl(&WSLCContainerImpl::Logs, Flags, Stdout, Stderr, Since, Until, Tail);
2797 }
2798 CATCH_RETURN();
@@ -2778,7 +2907,7 @@ void WSLCContainerImpl::ConnectToNetwork(const WSLCNetworkConnectionOptions* Opt
2907
2908 try
2909 {
2781 - m_dockerClient.ConnectContainerToNetwork(Options->NetworkName, request);
2910 + m_runtime.Docker().ConnectContainerToNetwork(Options->NetworkName, request);
2911 }
2912 catch (const DockerHTTPException& e)
2913 {
@@ -2808,7 +2937,7 @@ void WSLCContainerImpl::DisconnectFromNetwork(LPCSTR NetworkName)
2937
2938 try
2939 {
2811 - m_dockerClient.DisconnectContainerFromNetwork(NetworkName, request);
2940 + m_runtime.Docker().DisconnectContainerFromNetwork(NetworkName, request);
2941 }
2942 catch (const DockerHTTPException& e)
2943 {
@@ -2839,6 +2968,8 @@ HRESULT WSLCContainer::ConnectToNetwork(const WSLCNetworkConnectionOptions* Opti
2968 try
2969 {
2970 COMServiceExecutionContext context;
2971 +
2972 + auto vmLease = m_session.Runtime().AcquireVmLease();
2973 return CallImpl(&WSLCContainerImpl::ConnectToNetwork, Options);
2974 }
2975 CATCH_RETURN();
@@ -2847,6 +2978,8 @@ HRESULT WSLCContainer::DisconnectFromNetwork(LPCSTR NetworkName)
2978 try
2979 {
2980 COMServiceExecutionContext context;
2981 +
2982 + auto vmLease = m_session.Runtime().AcquireVmLease();
2983 return CallImpl(&WSLCContainerImpl::DisconnectFromNetwork, NetworkName);
2984 }
2985 CATCH_RETURN();
src/windows/wslcsession/WSLCContainer.h
+24 -22
@@ -16,6 +16,7 @@ Abstract:
16
17 #include "ServiceProcessLauncher.h"
18 #include "WSLCSession.h"
19 +#include "WSLCIdleState.h"
20 #include "DockerEventTracker.h"
21 #include "DockerHTTPClient.h"
22 #include "WSLCProcessControl.h"
@@ -32,6 +33,7 @@ namespace wsl::windows::service::wslc {
33
34 class WSLCContainer;
35 class WSLCSession;
36 +class WSLCSessionRuntime;
37 class WSLCVolumes;
38
39 class unique_com_disconnect
@@ -72,7 +74,7 @@ public:
74
75 WSLCContainerImpl(
76 WSLCSession& wslcSession,
75 - WSLCVirtualMachine& virtualMachine,
77 + WSLCSessionRuntime& runtime,
78 IWSLCPluginNotifier* pluginNotifier,
79 std::string&& Id,
80 std::string&& Name,
@@ -80,13 +82,9 @@ public:
82 std::string NetworkMode,
83 std::vector<WSLCVolumeMount>&& volumes,
84 std::vector<std::string>&& namedVolumes,
83 - WSLCVolumes& Volumes,
85 std::vector<ContainerPortMapping>&& ports,
86 std::map<std::string, std::string>&& labels,
87 std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
87 - DockerEventTracker& EventTracker,
88 - DockerHTTPClient& DockerClient,
89 - IORelay& Relay,
88 WSLCContainerState InitialState,
89 std::uint64_t CreatedAt,
90 WSLCProcessFlags InitProcessFlags,
@@ -122,6 +120,9 @@ public:
120 WSLCContainerState State() const noexcept;
121 std::vector<WSLCPortMapping> GetPorts() const;
122
123 + // Re-registers a stopped container's VM-scoped port allocations against the restarted VM.
124 + void RecoverPorts(const common::docker_schema::ContainerInfo& dockerContainer);
125 +
126 __requires_lock_held(m_lock) void Transition(WSLCContainerState State, std::optional<std::uint64_t> stateChangedAt = std::nullopt) noexcept;
127
128 const std::string& ID() const noexcept;
@@ -137,25 +138,17 @@ public:
138 const WSLCContainerOptions& Options,
139 const std::string& Name,
140 WSLCSession& wslcSession,
140 - WSLCVirtualMachine& virtualMachine,
141 + WSLCSessionRuntime& runtime,
142 IWSLCPluginNotifier* pluginNotifier,
143 const std::unordered_map<std::string, NetworkEntry>& SessionNetworks,
143 - WSLCVolumes& Volumes,
144 - std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
145 - DockerEventTracker& EventTracker,
146 - DockerHTTPClient& DockerClient,
147 - IORelay& Relay);
144 + std::function<void(const WSLCContainerImpl*)>&& OnDeleted);
145
146 static std::shared_ptr<WSLCContainerImpl> Open(
147 const common::docker_schema::ContainerInfo& DockerContainer,
148 WSLCSession& wslcSession,
152 - WSLCVirtualMachine& virtualMachine,
149 + WSLCSessionRuntime& runtime,
150 IWSLCPluginNotifier* pluginNotifier,
154 - WSLCVolumes& Volumes,
155 - std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
156 - DockerEventTracker& EventTracker,
157 - DockerHTTPClient& DockerClient,
158 - IORelay& Relay);
151 + std::function<void(const WSLCContainerImpl*)>&& OnDeleted);
152
153 private:
154 __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect DeleteExclusiveLockHeld(WSLCDeleteFlags Flags);
@@ -180,6 +173,10 @@ private:
173 void MapPorts();
174 void UnmapPorts();
175
176 + // Acquires or releases the activity hold so it is held exactly while the container is Running,
177 + // keeping the session's VM alive across idle teardown.
178 + __requires_lock_held(m_lock) void UpdateActivityHoldLockHeld() noexcept;
179 +
180 __requires_shared_lock_held(m_lock) std::string InspectLockHeld() const;
181
182 mutable wil::srwlock m_lock;
@@ -205,25 +202,30 @@ private:
202 // Must be acquired before m_lock when both are needed.
203 std::mutex m_stopLock;
204
208 - DockerHTTPClient& m_dockerClient;
205 + // The container outlives any single VM: it survives idle-termination and is reused when the VM
206 + // restarts. VM-scoped resources (Vm(), Docker(), Volumes(), Events(), Relay()) are therefore
207 + // fetched from the (stable) runtime at each use rather than cached, since a cached reference
208 + // would dangle across a restart. They are only valid while a VM lease is held.
209 + WSLCSessionRuntime& m_runtime;
210 std::uint64_t m_stateChangedAt{static_cast<std::uint64_t>(std::time(nullptr))};
211 std::uint64_t m_createdAt{};
212 WSLCContainerState m_state = WslcContainerStateInvalid;
213 WSLCSession& m_wslcSession;
214 IWSLCPluginNotifier* m_pluginNotifier;
214 - WSLCVirtualMachine& m_virtualMachine;
215 std::vector<ContainerPortMapping> m_mappedPorts;
216 std::vector<WSLCVolumeMount> m_mountedVolumes;
217
218 std::vector<std::string> m_namedVolumes;
219 - WSLCVolumes& m_volumes;
219
220 std::map<std::string, std::string> m_labels;
221 Microsoft::WRL::ComPtr<WSLCContainer> m_comWrapper;
223 - DockerEventTracker& m_eventTracker;
222 DockerEventTracker::EventTrackingReference m_containerEvents;
225 - IORelay& m_ioRelay;
223 std::string m_networkMode;
224 +
225 + // Held (non-empty) exactly while the container is Running so the session's VM stays alive even
226 + // when no client holds the wrapper (e.g. a detached `run -d` container). Maintained by
227 + // UpdateActivityHoldLockHeld(); released automatically when the container is destroyed.
228 + ActivityRef m_activityHold;
229 };
230
231 class DECLSPEC_UUID("B1F1C4E3-C225-4CAE-AD8A-34C004DE1AE4") WSLCContainer
src/windows/wslcsession/WSLCIdleState.h new
+226
@@ -0,0 +1,226 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCIdleState.h
8 +
9 +Abstract:
10 +
11 + Shared idle-termination state for WSLC session VM lifecycle.
12 +
13 +--*/
14 +#pragma once
15 +
16 +#include <atomic>
17 +#include <chrono>
18 +#include <functional>
19 +#include <memory>
20 +#include <utility>
21 +#include <wil/resource.h>
22 +
23 +namespace wsl::windows::service::wslc {
24 +
25 +// Shared idle-termination state for a WSLC session.
26 +//
27 +// A single activity refcount is the only source of truth for "the VM is needed". Everything that
28 +// requires the VM holds a reference for as long as it needs it:
29 +// * in-flight operations (WSLCSession::VmLease),
30 +// * running/created containers themselves (WSLCContainerImpl's ActivityRef),
31 +// * client-held process wrappers (WSLCProcess keep-alive token),
32 +// * multi-round-trip CLI operations (WSLCSession::BeginContainerOperation).
33 +//
34 +// When the count drops to zero a threadpool timer is armed for the idle grace period; if it
35 +// elapses without new activity the session-supplied OnIdle callback tears the VM down. Any new
36 +// activity before it fires cancels the timer.
37 +//
38 +// Held via shared_ptr so activity holders (container/process wrappers, operation tokens) can
39 +// outlive the owning session and release activity without dereferencing it. The session clears the
40 +// callback and drains the timer in Disarm() during teardown, after which a late release simply
41 +// decrements the count and never re-enters the destroyed session.
42 +class IdleState
43 +{
44 +public:
45 + IdleState() = default;
46 +
47 + IdleState(const IdleState&) = delete;
48 + IdleState& operator=(const IdleState&) = delete;
49 +
50 + // Installs the idle-teardown callback and grace period and creates the timer. Called once by
51 + // the owning session after construction. OnIdle runs on a threadpool thread.
52 + void Initialize(std::chrono::milliseconds GracePeriod, std::function<void()> OnIdle)
53 + {
54 + auto lock = m_lock.lock_exclusive();
55 + m_gracePeriod = GracePeriod;
56 + m_onIdle = std::move(OnIdle);
57 + m_timer.reset(CreateThreadpoolTimer(&IdleState::TimerCallback, this, nullptr));
58 + THROW_LAST_ERROR_IF(!m_timer);
59 + }
60 +
61 + // Permanently disables idle teardown: clears the callback so no further arm has any effect, and
62 + // drains any pending/running timer callback. Must be called by the session (with its own lock
63 + // released) during teardown, before the session object is destroyed, so no callback can
64 + // reference it afterwards.
65 + void Disarm() noexcept
66 + {
67 + PTP_TIMER timer = nullptr;
68 + {
69 + auto lock = m_lock.lock_exclusive();
70 + m_onIdle = nullptr;
71 + timer = m_timer.get();
72 + if (timer != nullptr)
73 + {
74 + SetThreadpoolTimer(timer, nullptr, 0, 0);
75 + }
76 + }
77 +
78 + // Drain any in-flight callback outside the lock; it may take the session lock.
79 + if (timer != nullptr)
80 + {
81 + WaitForThreadpoolTimerCallbacks(timer, TRUE);
82 + }
83 + }
84 +
85 + // Records the start of an activity; cancels any pending idle teardown on the 0->1 transition.
86 + void AddActivity() noexcept
87 + {
88 + auto lock = m_lock.lock_exclusive();
89 + if (m_activityCount.fetch_add(1) == 0)
90 + {
91 + CancelLockHeld();
92 + }
93 + }
94 +
95 + // Records the end of an activity; arms the idle timer on the 1->0 transition.
96 + void ReleaseActivity() noexcept
97 + {
98 + auto lock = m_lock.lock_exclusive();
99 + const int previous = m_activityCount.fetch_sub(1);
100 + FAIL_FAST_IF(previous <= 0); // Underflow is a fatal bug, not a recoverable condition.
101 + if (previous == 1)
102 + {
103 + ArmLockHeld();
104 + }
105 + }
106 +
107 + int ActivityCount() const noexcept
108 + {
109 + return m_activityCount.load();
110 + }
111 +
112 +private:
113 + static void CALLBACK TimerCallback(PTP_CALLBACK_INSTANCE, PVOID Context, PTP_TIMER) noexcept
114 + try
115 + {
116 + auto* self = static_cast<IdleState*>(Context);
117 +
118 + std::function<void()> onIdle;
119 + {
120 + auto lock = self->m_lock.lock_exclusive();
121 +
122 + // Activity resumed (count != 0) or teardown raced us (callback cleared): nothing to do.
123 + if (self->m_activityCount.load() != 0 || !self->m_onIdle)
124 + {
125 + return;
126 + }
127 +
128 + // Copy and invoke outside the lock: OnIdle takes the session lock, and holding this
129 + // lock across that would invert the session-lock -> idle-lock ordering.
130 + onIdle = self->m_onIdle;
131 + }
132 +
133 + onIdle();
134 + }
135 + CATCH_LOG()
136 +
137 + void ArmLockHeld() noexcept
138 + {
139 + if (!m_timer || !m_onIdle)
140 + {
141 + return;
142 + }
143 +
144 + // Relative due time is expressed as a negative count of 100ns intervals.
145 + const int64_t relative = -static_cast<int64_t>(m_gracePeriod.count()) * 10000;
146 + FILETIME due{};
147 + due.dwLowDateTime = static_cast<DWORD>(relative & 0xFFFFFFFF);
148 + due.dwHighDateTime = static_cast<DWORD>((relative >> 32) & 0xFFFFFFFF);
149 + SetThreadpoolTimer(m_timer.get(), &due, 0, 0);
150 + }
151 +
152 + void CancelLockHeld() noexcept
153 + {
154 + if (m_timer)
155 + {
156 + SetThreadpoolTimer(m_timer.get(), nullptr, 0, 0);
157 + }
158 + }
159 +
160 + std::atomic<int> m_activityCount{0};
161 + wil::srwlock m_lock;
162 +
163 + _Guarded_by_(m_lock) std::function<void()> m_onIdle;
164 + _Guarded_by_(m_lock) std::chrono::milliseconds m_gracePeriod { 0 };
165 + _Guarded_by_(m_lock) wil::unique_threadpool_timer m_timer;
166 +};
167 +
168 +// RAII activity hold on an IdleState: increments on construction and decrements on destruction or
169 +// reset(). Movable, non-copyable. Used by running/created containers to keep the VM alive without
170 +// a client reference. Holds the IdleState via shared_ptr so it is safe even if it outlives the
171 +// owning session.
172 +class ActivityRef
173 +{
174 +public:
175 + ActivityRef() = default;
176 +
177 + explicit ActivityRef(std::shared_ptr<IdleState> State) noexcept : m_state(std::move(State))
178 + {
179 + if (m_state)
180 + {
181 + m_state->AddActivity();
182 + }
183 + }
184 +
185 + ActivityRef(ActivityRef&& Other) noexcept : m_state(std::exchange(Other.m_state, nullptr))
186 + {
187 + }
188 +
189 + ActivityRef& operator=(ActivityRef&& Other) noexcept
190 + {
191 + if (this != &Other)
192 + {
193 + reset();
194 + m_state = std::exchange(Other.m_state, nullptr);
195 + }
196 +
197 + return *this;
198 + }
199 +
200 + ActivityRef(const ActivityRef&) = delete;
201 + ActivityRef& operator=(const ActivityRef&) = delete;
202 +
203 + ~ActivityRef()
204 + {
205 + reset();
206 + }
207 +
208 + void reset() noexcept
209 + {
210 + if (m_state)
211 + {
212 + m_state->ReleaseActivity();
213 + m_state.reset();
214 + }
215 + }
216 +
217 + explicit operator bool() const noexcept
218 + {
219 + return m_state != nullptr;
220 + }
221 +
222 +private:
223 + std::shared_ptr<IdleState> m_state;
224 +};
225 +
226 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCProcess.h
+10
@@ -45,9 +45,19 @@ public:
45 HANDLE GetExitEvent();
46 int GetPid() const;
47
48 + // Attaches an opaque keep-alive token whose lifetime is bound to this process object. A
49 + // root-namespace process is not tracked as a container, so it relies on this token to hold an
50 + // activity reference on the owning session for as long as the client keeps the process alive,
51 + // preventing the idle worker from tearing the VM down (and killing the process) underneath it.
52 + void SetKeepAliveToken(Microsoft::WRL::ComPtr<IUnknown>&& Token) noexcept
53 + {
54 + m_keepAliveToken = std::move(Token);
55 + }
56 +
57 private:
58 WSLCProcessFlags m_flags;
59 std::shared_ptr<WSLCProcessControl> m_control;
60 std::unique_ptr<WSLCProcessIO> m_io;
61 + Microsoft::WRL::ComPtr<IUnknown> m_keepAliveToken;
62 };
63 } // namespace wsl::windows::service::wslc
\ No newline at end of file
src/windows/wslcsession/WSLCProcessControl.cpp
+9 -1
@@ -101,7 +101,15 @@ void DockerContainerProcessControl::OnContainerReleased() noexcept
101 // Signal the exit event to prevent callers from being blocked on it.
102 if (!m_exitEvent.is_signaled())
103 {
104 - m_exitedCode = 128 + WSLCSignalSIGKILL;
104 + // If the container already produced a real exit code (recorded by SetExitCode but not yet
105 + // signaled — e.g. an --rm container whose init-exit signal is deferred to the Destroy
106 + // event), preserve it. Only synthesize SIGKILL when the container is released without ever
107 + // having produced an exit code (an abrupt teardown of a still-running container).
108 + if (!m_exitedCode.has_value())
109 + {
110 + m_exitedCode = 128 + WSLCSignalSIGKILL;
111 + }
112 +
113 m_exitEvent.SetEvent();
114 }
115 }
src/windows/wslcsession/WSLCSession.cpp
+475 -384
@@ -36,15 +36,48 @@ using wsl::windows::service::wslc::WSLCExecutionContext;
36 using wsl::windows::service::wslc::WSLCSession;
37 using wsl::windows::service::wslc::WSLCVirtualMachine;
38
39 -constexpr auto c_containerdStorage = "/var/lib/docker";
39 +constexpr auto c_containerdStorage = wsl::windows::wslc::ContainerdStorageMountPoint;
40 constexpr auto c_containerdSocket = "/run/containerd/containerd.sock";
41 -constexpr auto c_dockerdReadyLogLine = "API listen on /var/run/docker.sock";
41 constexpr auto c_storageVhdFilename = wsl::windows::wslc::DefaultStorageVhdName;
42 constexpr DWORD c_processTerminateTimeoutMs = 30 * 1000;
43 constexpr DWORD c_processKillTimeoutMs = 10 * 1000;
44
45 +// Default grace period to keep an otherwise-idle VM running before tearing it down (used when the
46 +// session's IdleTimeoutSec setting is 0/unset). This avoids thrashing the VM (repeated
47 +// teardown/recreate) when containers are created and destroyed, or operations issued, in quick
48 +// succession. The clock restarts whenever the VM is observed to be non-idle, so a full grace period
49 +// of continuous idleness is required before teardown.
50 +constexpr auto c_vmIdleGracePeriod = std::chrono::seconds(30);
51 +
52 namespace {
53
54 +// Validates the target path for a NEW session (one with no existing storage VHD): if the path
55 +// already exists it must be an empty directory, so session storage is never mixed with unrelated
56 +// user files. A non-existent path is fine (it will be created). Enforced eagerly at session
57 +// creation and again when the storage VHD is lazily created.
58 +void ValidateNewSessionStorageDirectory(const std::filesystem::path& StoragePath)
59 +{
60 + // status's error_code distinguishes "doesn't exist yet" (OK, we'll create it) from other I/O errors.
61 + std::error_code ec;
62 + const auto status = std::filesystem::status(StoragePath, ec);
63 + if (ec && ec.value() != ERROR_FILE_NOT_FOUND && ec.value() != ERROR_PATH_NOT_FOUND)
64 + {
65 + THROW_IF_WIN32_ERROR_MSG(ec.value(), "status failed for %ls", StoragePath.c_str());
66 + }
67 +
68 + if (!std::filesystem::exists(status))
69 + {
70 + return;
71 + }
72 +
73 + THROW_HR_WITH_USER_ERROR_IF(
74 + E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeDirectory(StoragePath.c_str()), !std::filesystem::is_directory(status));
75 +
76 + const bool empty = std::filesystem::is_empty(StoragePath, ec);
77 + THROW_IF_WIN32_ERROR_MSG(ec.value(), "is_empty failed for %ls", StoragePath.c_str());
78 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeEmpty(StoragePath.c_str()), !empty);
79 +}
80 +
81 // Group policy: WSLContainerRegistryAllowlist restricts which container-image
82 // registries can be pulled from or pushed to. The check is enforced here at the
83 // service boundary so it covers ALL callers (wslc.exe CLI, the WslcSDK C API, and
@@ -331,7 +364,7 @@ HRESULT WSLCSession::Initialize(
364 try
365 {
366 RETURN_HR_IF(E_POINTER, Settings == nullptr || VmFactory == nullptr);
334 - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_INITIALIZED), m_virtualMachine.has_value());
367 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_INITIALIZED), m_vmFactoryGitCookie != 0);
368
369 THROW_HR_IF_MSG(
370 E_INVALIDARG, WI_IsAnyFlagSet(Settings->FeatureFlags, ~WSLCFeatureFlagsValid), "Invalid feature flags: 0x%x", Settings->FeatureFlags);
@@ -342,9 +375,35 @@ try
375 Settings->StorageFlags);
376
377 // Set up a warning context for the duration of initialization so that non-fatal
345 - // failures (e.g., container/volume/network recovery) are streamed to the CLI.
378 + // failures are streamed to the CLI.
379 WSLCExecutionContext warningContext(this, WarningCallback);
380
381 + // The VM (and storage VHD) is created lazily on the first operation. Validate the storage
382 + // configuration eagerly here so misconfiguration is reported at session creation rather than
383 + // surfacing later on the first VM-starting operation.
384 + if (Settings->StoragePath != nullptr)
385 + {
386 + const std::filesystem::path storagePath{Settings->StoragePath};
387 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Settings->StoragePath), !storagePath.is_absolute());
388 +
389 + const auto vhdPath = storagePath / c_storageVhdFilename;
390 + std::error_code existsError;
391 + const bool vhdExists = std::filesystem::exists(vhdPath, existsError);
392 + THROW_IF_WIN32_ERROR_MSG(existsError.value(), "exists failed for %ls", vhdPath.c_str());
393 +
394 + if (WI_IsFlagSet(Settings->StorageFlags, WSLCSessionStorageFlagsNoCreate))
395 + {
396 + // The storage VHD must already exist (ConfigureStorage will not create it).
397 + THROW_HR_WITH_USER_ERROR_IF(
398 + HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), Localization::MessageWslcSessionStorageNotFound(Settings->StoragePath), !vhdExists);
399 + }
400 + else if (!vhdExists)
401 + {
402 + // New session: the target path (if it exists) must be an empty directory.
403 + ValidateNewSessionStorageDirectory(storagePath);
404 + }
405 + }
406 +
407 // N.B. No locking is required because Initialize() is always called before the session is returned to the caller.
408 m_id = Settings->SessionId;
409 m_displayName = Settings->DisplayName ? Settings->DisplayName : L"";
@@ -352,8 +411,16 @@ try
411 m_featureFlags = Settings->FeatureFlags;
412 m_pluginNotifier = PluginNotifier;
413
355 - // Get user token for the current process
414 + // Park the VM factory in the Global Interface Table. It is supplied here (on the call that
415 + // creates the session) but used on demand from other threads/apartments; storing the raw
416 + // proxy and calling it later would raise RPC_E_WRONG_THREAD.
417 + m_git = wil::CoCreateInstance<IGlobalInterfaceTable>(CLSID_StdGlobalInterfaceTable, CLSCTX_INPROC_SERVER);
418 + THROW_IF_FAILED(m_git->RegisterInterfaceInGlobal(VmFactory, __uuidof(IWSLCVirtualMachineFactory), &m_vmFactoryGitCookie));
419 +
420 + // Persist a deep copy of the settings (and the creating user's SID) required to
421 + // (re)create the VM on demand.
422 const auto tokenInfo = wil::get_token_information<TOKEN_USER>(GetCurrentProcessToken());
423 + PersistSettings(*Settings, tokenInfo->User.Sid);
424
425 WSL_LOG(
426 "SessionInitialized",
@@ -361,62 +428,130 @@ try
428 TraceLoggingValue(m_displayName.c_str(), "DisplayName"),
429 TraceLoggingValue(m_creatorProcessName.c_str(), "CreatorProcess"));
430
364 - // Create the VM through the factory. The VM produces crash events; the session multiplexes
365 - // them out to any registered ICrashDumpCallback subscribers via OnCrashDumpWritten.
366 - wil::com_ptr<IWSLCVirtualMachine> vm;
367 - THROW_IF_FAILED(VmFactory->CreateVirtualMachine(&vm));
431 + const auto idleGracePeriod = m_settings.IdleTimeoutSec > 0 ? std::chrono::seconds(m_settings.IdleTimeoutSec) : c_vmIdleGracePeriod;
432
369 - m_virtualMachine.emplace(
370 - vm.get(),
371 - Settings,
372 - m_sessionTerminatingEvent.get(),
373 - std::bind(&WSLCSession::OnCrashDumpWritten, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5));
433 + WSLCSessionRuntime::RuntimeHooks hooks;
434 + hooks.BringUp = [this]() {
435 + // Configure storage.
436 + ConfigureStorage(m_settings, m_userSid.empty() ? nullptr : reinterpret_cast<PSID>(m_userSid.data()));
437
375 - // Make sure that everything is destroyed correctly if an exception is thrown.
376 - auto errorCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_FAILED(Terminate()); });
438 + // Mirror the host's trusted root CAs into the VM before dockerd starts.
439 + InstallTrustedRootCertificates();
440
378 - m_virtualMachine->Initialize();
441 + // Launch containerd first, then dockerd with the external containerd socket.
442 + StartContainerd();
443
380 - // Get an event from the service that is signaled when the VM exits.
381 - THROW_IF_FAILED(vm->GetTerminationEvent(&m_vmExitedEvent));
444 + // Reset the readiness event before (re)starting dockerd so a stale signal from a prior
445 + // VM instance is not observed.
446 + m_runtime.ResetDockerdReady();
447 + StartDockerd();
448
383 - // Configure storage.
384 - ConfigureStorage(*Settings, tokenInfo->User.Sid);
449 + m_runtime.InitializeDockerRuntime(m_storageVhdPath.parent_path());
450 + };
451
386 - // Mirror the host's trusted root CAs into the VM before dockerd starts.
387 - InstallTrustedRootCertificates();
452 + hooks.RecoverState = [this]() {
453 + RecoverExistingNetworks();
454 + RecoverExistingContainers();
455 + };
456
389 - // Launch containerd first
390 - StartContainerd();
457 + hooks.TearDownSessionState = [this](bool permanent) {
458 + std::lock_guard containersLock(m_containersLock);
459 + std::lock_guard networksLock(m_networksLock);
460
392 - // Launch dockerd with external containerd socket
393 - StartDockerd();
461 + // Network metadata is rebuilt from dockerd on every VM start, so it is always dropped.
462 + m_networks.clear();
463
395 - // Wait for dockerd to be ready before starting the event tracker.
396 - THROW_WIN32_IF_MSG(
397 - ERROR_TIMEOUT, !m_dockerdReadyEvent.wait(Settings->BootTimeoutMs), "Timed out waiting for dockerd to start");
464 + // Container wrappers are kept alive across idle teardown (only cleared on permanent shutdown)
465 + // so client COM references stay valid; RecoverState reattaches them to the restarted VM.
466 + if (permanent)
467 + {
468 + m_containers.clear();
469 + }
470 + };
471
399 - auto [_, __, channel] = m_virtualMachine->Fork(WSLC_FORK::Thread);
472 + hooks.OnSpontaneousExit = [this]() { LOG_IF_FAILED(Terminate()); };
473
401 - m_dockerClient.emplace(std::move(channel), m_virtualMachine->TerminatingEvent(), m_virtualMachine->VmId(), 10 * 1000);
474 + // Forward VM start/stop to plugins. Both are best-effort: errors are logged and ignored so a
475 + // misbehaving plugin cannot abort VM startup or the operation that triggered it.
476 + hooks.OnVmStarted = [this]() {
477 + if (m_pluginNotifier)
478 + {
479 + LOG_IF_FAILED(m_pluginNotifier->OnVmStarted());
480 + }
481 + };
482
403 - // Start the event tracker.
404 - m_eventTracker.emplace(m_dockerClient.value(), *this, m_ioRelay);
483 + hooks.OnVmStopping = [this]() {
484 + if (m_pluginNotifier)
485 + {
486 + LOG_IF_FAILED(m_pluginNotifier->OnVmStopping());
487 + }
488 + };
489
406 - m_volumes.emplace(m_dockerClient.value(), m_virtualMachine.value(), m_eventTracker.value(), m_storageVhdPath.parent_path());
490 + hooks.OnCrashDump = std::bind(
491 + &WSLCSession::OnCrashDumpWritten, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5);
492
408 - // Monitor for unexpected VM exit.
409 - m_ioRelay.AddHandle(std::make_unique<windows::common::io::EventHandle>(m_vmExitedEvent.get(), std::bind(&WSLCSession::OnVmExited, this)));
493 + WSLCSessionRuntime::SessionContext sessionContext;
494 + sessionContext.Id = m_id;
495 + sessionContext.DisplayName = m_displayName;
496 + sessionContext.Terminating = &m_terminating;
497 + sessionContext.SessionTerminatingEvent = m_sessionTerminatingEvent;
498 + sessionContext.SessionTerminatedEvent = m_sessionTerminatedEvent;
499
411 - // Recover any existing resources from storage.
412 - RecoverExistingNetworks();
413 - RecoverExistingContainers();
500 + m_runtime.Initialize(m_vmFactoryGitCookie, m_git, &m_settings, idleGracePeriod, std::move(sessionContext), std::move(hooks));
501
415 - errorCleanup.release();
502 return S_OK;
503 }
504 CATCH_RETURN()
505
506 +void WSLCSession::PersistSettings(const WSLCSessionInitSettings& Settings, PSID UserSid)
507 +{
508 + m_settings = Settings;
509 +
510 + // Repoint the string fields at storage owned by the session so they outlive the caller's buffers.
511 + m_settings.DisplayName = m_displayName.c_str();
512 +
513 + if (Settings.CreatorProcessName != nullptr)
514 + {
515 + m_settingsCreatorProcessName = Settings.CreatorProcessName;
516 + m_settings.CreatorProcessName = m_settingsCreatorProcessName->c_str();
517 + }
518 + else
519 + {
520 + m_settings.CreatorProcessName = nullptr;
521 + }
522 +
523 + if (Settings.StoragePath != nullptr)
524 + {
525 + m_settingsStoragePath = Settings.StoragePath;
526 + m_settings.StoragePath = m_settingsStoragePath->c_str();
527 + }
528 + else
529 + {
530 + m_settings.StoragePath = nullptr;
531 + }
532 +
533 + if (Settings.RootVhdTypeOverride != nullptr)
534 + {
535 + m_settingsRootVhdTypeOverride = Settings.RootVhdTypeOverride;
536 + m_settings.RootVhdTypeOverride = m_settingsRootVhdTypeOverride->c_str();
537 + }
538 + else
539 + {
540 + m_settings.RootVhdTypeOverride = nullptr;
541 + }
542 +
543 + THROW_HR_IF(E_UNEXPECTED, UserSid == nullptr);
544 +
545 + const auto length = GetLengthSid(UserSid);
546 + const auto* bytes = reinterpret_cast<const BYTE*>(UserSid);
547 + m_userSid.assign(bytes, bytes + length);
548 +}
549 +
550 +WSLCSession::VmLease WSLCSession::AcquireLease(WSLCSessionRuntime::VmLeasePolicy Policy)
551 +{
552 + return m_runtime.AcquireVmLease(Policy);
553 +}
554 +
555 WSLCSession::~WSLCSession()
556 {
557 WSL_LOG("SessionTerminated", TraceLoggingValue(m_id, "SessionId"), TraceLoggingValue(m_displayName.c_str(), "DisplayName"));
@@ -439,8 +574,8 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID
574 if (Settings.StoragePath == nullptr)
575 {
576 // If no storage path is specified, use a tmpfs for convenience.
442 - m_virtualMachine->Mount("", c_containerdStorage, "tmpfs", "", 0);
443 - m_storageMounted = true;
577 + m_runtime.Vm().Mount("", c_containerdStorage, "tmpfs", "", 0);
578 + m_runtime.SetStorageMounted(true);
579 return;
580 }
581
@@ -458,7 +593,7 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID
593 {
594 if (diskLun.has_value())
595 {
461 - m_virtualMachine->DetachDisk(diskLun.value());
596 + m_runtime.Vm().DetachDisk(diskLun.value());
597 }
598
599 LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_storageVhdPath.c_str()));
@@ -466,7 +601,7 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID
601 });
602
603 auto result =
469 - wil::ResultFromException([&]() { diskDevice = m_virtualMachine->AttachDisk(m_storageVhdPath.c_str(), false).second; });
604 + wil::ResultFromException([&]() { diskDevice = m_runtime.Vm().AttachDisk(m_storageVhdPath.c_str(), false).second; });
605
606 if (FAILED(result))
607 {
@@ -483,23 +618,7 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID
618 WI_IsFlagSet(Settings.StorageFlags, WSLCSessionStorageFlagsNoCreate));
619
620 // Reject any non-empty existing path so we don't mix user files with session storage.
486 - // status's error_code distinguishes "doesn't exist yet" (OK, we'll create it) from other I/O errors.
487 - std::error_code ec;
488 - const auto status = std::filesystem::status(storagePath, ec);
489 - if (ec && ec.value() != ERROR_FILE_NOT_FOUND && ec.value() != ERROR_PATH_NOT_FOUND)
490 - {
491 - THROW_IF_WIN32_ERROR_MSG(ec.value(), "status failed for %ls", storagePath.c_str());
492 - }
493 -
494 - if (std::filesystem::exists(status))
495 - {
496 - THROW_HR_WITH_USER_ERROR_IF(
497 - E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeDirectory(storagePath.c_str()), !std::filesystem::is_directory(status));
498 -
499 - const bool empty = std::filesystem::is_empty(storagePath, ec);
500 - THROW_IF_WIN32_ERROR_MSG(ec.value(), "is_empty failed for %ls", storagePath.c_str());
501 - THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcSessionStorageMustBeEmpty(storagePath.c_str()), !empty);
502 - }
621 + ValidateNewSessionStorageDirectory(storagePath);
622
623 // If the VHD wasn't found, create it.
624 WSL_LOG("CreateStorageVhd", TraceLoggingValue(m_storageVhdPath.c_str(), "StorageVhdPath"));
@@ -514,31 +633,32 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID
633 vhdCreated = true;
634
635 // Then attach the new disk.
517 - std::tie(diskLun, diskDevice) = m_virtualMachine->AttachDisk(m_storageVhdPath.c_str(), false);
636 + std::tie(diskLun, diskDevice) = m_runtime.Vm().AttachDisk(m_storageVhdPath.c_str(), false);
637
638 // Then format it.
520 - m_virtualMachine->Ext4Format(diskDevice);
639 + m_runtime.Vm().Ext4Format(diskDevice);
640 }
641
642 // Mount the device to /root.
524 - m_virtualMachine->Mount(diskDevice.c_str(), c_containerdStorage, "ext4", "", 0);
525 - m_storageMounted = true;
643 + m_runtime.Vm().Mount(diskDevice.c_str(), c_containerdStorage, "ext4", "", 0);
644 + m_runtime.SetStorageMounted(true);
645
646 // Configure swap on a separate ephemeral VHD.
647 if (Settings.SwapSizeMb > 0)
648 {
649 try
650 {
532 - m_swapVhdPath = storagePath / "swap.vhdx";
533 - DeleteFileW(m_swapVhdPath.c_str()); // Remove stale swap from prior run
534 - wsl::core::filesystem::CreateVhd(m_swapVhdPath.c_str(), static_cast<ULONGLONG>(Settings.SwapSizeMb) * _1MB, UserSid, false, false);
651 + std::filesystem::path swapVhdPath = storagePath / "swap.vhdx";
652 + m_runtime.SetSwapVhdPath(swapVhdPath);
653 + DeleteFileW(swapVhdPath.c_str()); // Remove stale swap from prior run
654 + wsl::core::filesystem::CreateVhd(swapVhdPath.c_str(), static_cast<ULONGLONG>(Settings.SwapSizeMb) * _1MB, UserSid, false, false);
655
536 - auto [_, swapDevice] = m_virtualMachine->AttachDisk(m_swapVhdPath.c_str(), false);
656 + auto [_, swapDevice] = m_runtime.Vm().AttachDisk(swapVhdPath.c_str(), false);
657
658 // Fire-and-forget: mkswap + swapon runs asynchronously since swap is best-effort.
659 auto cmd = std::format("/usr/sbin/mkswap {0} && /usr/sbin/swapon {0}", swapDevice);
660 ServiceProcessLauncher launcher("/bin/sh", {"/bin/sh", "-c", cmd});
541 - launcher.Launch(*m_virtualMachine);
661 + launcher.Launch(m_runtime.Vm());
662 }
663 catch (...)
664 {
@@ -572,7 +692,7 @@ CATCH_RETURN();
692
693 void WSLCSession::OnDockerdExited()
694 {
575 - if (!m_sessionTerminatingEvent.is_signaled())
695 + if (!m_sessionTerminatingEvent.is_signaled() && m_runtime.ExitDisposition() != WSLCSessionRuntime::VmExitDisposition::StopRequested)
696 {
697 WSL_LOG("UnexpectedDockerdExit", TraceLoggingValue(m_displayName.c_str(), "Name"));
698 }
@@ -580,63 +700,26 @@ void WSLCSession::OnDockerdExited()
700
701 void WSLCSession::OnContainerdExited()
702 {
583 - if (!m_sessionTerminatingEvent.is_signaled())
703 + if (!m_sessionTerminatingEvent.is_signaled() && m_runtime.ExitDisposition() != WSLCSessionRuntime::VmExitDisposition::StopRequested)
704 {
705 WSL_LOG("UnexpectedContainerdExit", TraceLoggingValue(m_displayName.c_str(), "Name"));
706 }
707 }
708
589 -void WSLCSession::OnVmExited()
590 -{
591 - WSL_LOG(
592 - "VmExited",
593 - TraceLoggingLevel(WINEVENT_LEVEL_WARNING),
594 - TraceLoggingValue(m_id, "SessionId"),
595 - TraceLoggingValue(m_displayName.c_str(), "Name"),
596 - TraceLoggingValue(!m_sessionTerminatingEvent.is_signaled(), "Unexpected"));
597 -
598 - LOG_IF_FAILED(Terminate());
599 -}
600 -
601 -void WSLCSession::OnProcessLog(const gsl::span<char>& Buffer, PCSTR Source)
602 -try
603 -{
604 - if (Buffer.empty())
605 - {
606 - return;
607 - }
608 -
609 - std::string entry = {Buffer.begin(), Buffer.end()};
610 - WSL_LOG(
611 - "ContainerdLog",
612 - TraceLoggingValue(Source, "Source"),
613 - TraceLoggingValue(entry.c_str(), "Content"),
614 - TraceLoggingValue(m_displayName.c_str(), "Name"));
615 -
616 - if (!m_dockerdReadyEvent.is_signaled())
617 - {
618 - if (entry.find(c_dockerdReadyLogLine) != std::string::npos)
619 - {
620 - m_dockerdReadyEvent.SetEvent();
621 - }
622 - }
623 -}
624 -CATCH_LOG();
625 -
709 ServiceRunningProcess WSLCSession::StartProcess(
710 const std::string& Executable, const std::vector<std::string>& Args, PCSTR LogSource, std::function<void()>&& ExitCallback)
711 {
712 ServiceProcessLauncher launcher{Executable, Args, {{"PATH=/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin"}}};
713
631 - auto process = launcher.Launch(*m_virtualMachine);
714 + auto process = launcher.Launch(m_runtime.Vm());
715
633 - m_ioRelay.AddHandle(std::make_unique<windows::common::io::LineBasedReadHandle>(
634 - process.GetStdHandle(1), [this, LogSource](const auto& data) { OnProcessLog(data, LogSource); }, false));
716 + m_runtime.Relay()->AddHandle(std::make_unique<windows::common::io::LineBasedReadHandle>(
717 + process.GetStdHandle(1), [this, LogSource](const auto& data) { m_runtime.OnProcessLog(data, LogSource); }, false));
718
636 - m_ioRelay.AddHandle(std::make_unique<windows::common::io::LineBasedReadHandle>(
637 - process.GetStdHandle(2), [this, LogSource](const auto& data) { OnProcessLog(data, LogSource); }, false));
719 + m_runtime.Relay()->AddHandle(std::make_unique<windows::common::io::LineBasedReadHandle>(
720 + process.GetStdHandle(2), [this, LogSource](const auto& data) { m_runtime.OnProcessLog(data, LogSource); }, false));
721
639 - m_ioRelay.AddHandle(std::make_unique<windows::common::io::EventHandle>(process.GetExitEvent(), std::move(ExitCallback)));
722 + m_runtime.Relay()->AddHandle(std::make_unique<windows::common::io::EventHandle>(process.GetExitEvent(), std::move(ExitCallback)));
723
724 return process;
725 }
@@ -654,7 +737,7 @@ void WSLCSession::StartContainerd()
737 args.emplace_back("debug");
738 }
739
657 - m_containerdProcess = StartProcess("/usr/bin/containerd", args, "containerd", std::bind(&WSLCSession::OnContainerdExited, this));
740 + m_runtime.SetContainerdProcess(StartProcess("/usr/bin/containerd", args, "containerd", std::bind(&WSLCSession::OnContainerdExited, this)));
741 WSL_LOG("ContainerdStarted");
742 }
743
@@ -667,7 +750,7 @@ void WSLCSession::StartDockerd()
750 args.emplace_back("--debug");
751 }
752
670 - m_dockerdProcess = StartProcess("/usr/bin/dockerd", args, "dockerd", std::bind(&WSLCSession::OnDockerdExited, this));
753 + m_runtime.SetDockerdProcess(StartProcess("/usr/bin/dockerd", args, "dockerd", std::bind(&WSLCSession::OnDockerdExited, this)));
754 WSL_LOG("DockerdStarted");
755 }
756
@@ -687,7 +770,7 @@ try
770 const auto script = std::format("cat > '{}'", c_certPath);
771
772 ServiceProcessLauncher launcher("/bin/sh", {"/bin/sh", "--norc", "-c", script}, {}, WSLCProcessFlagsStdin);
690 - auto process = launcher.Launch(*m_virtualMachine);
773 + auto process = launcher.Launch(m_runtime.Vm());
774
775 std::unique_ptr<OverlappedIOHandle> writeStdin(
776 new WriteHandle(process.GetStdHandle(WSLCFDStdin), std::vector<char>{pem.begin(), pem.end()}));
@@ -846,8 +929,8 @@ try
929 auto tagOrDigest = reference.TagOrDigest();
930 EnforceRegistryAllowlist(repo);
931
849 - auto lock = m_lock.lock_shared();
850 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
932 + auto runtime = m_runtime.Acquire();
933 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
934
935 if (!tagOrDigest.has_value())
936 {
@@ -861,7 +944,7 @@ try
944 registryAuth = std::string(RegistryAuthenticationInformation);
945 }
946
864 - auto requestContext = m_dockerClient->PullImage(repo.Name, tagOrDigest, registryAuth);
947 + auto requestContext = runtime.Docker().PullImage(repo.Name, tagOrDigest, registryAuth);
948 StreamImageOperation(*requestContext, Image, "Pull", ProgressCallback);
949
950 OnImageCreated(Image);
@@ -905,9 +988,9 @@ try
988 comCall = RegisterUserCOMCallback();
989 }
990
908 - auto lock = m_lock.lock_shared();
991 + auto runtime = m_runtime.Acquire();
992
910 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
993 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
994
995 // Track every Windows folder we mount into the VM during this build so a single scope_exit
996 // unmounts them all on success or on any throw partway through the loop below.
@@ -917,14 +1000,14 @@ try
1000 {
1001 // Best-effort but not silent: a failed unmount can leave a file-secret share mounted in the
1002 // guest, so log it. Never throw here.
920 - LOG_IF_FAILED(m_virtualMachine->UnmountWindowsFolder(path.c_str()));
1003 + LOG_IF_FAILED(runtime.Vm().UnmountWindowsFolder(path.c_str()));
1004 }
1005 });
1006 auto mountInVm = [&](LPCWSTR windowsPath, BOOL readOnly, std::string_view guestBase = "/mnt") -> std::string {
1007 GUID id{};
1008 THROW_IF_FAILED(CoCreateGuid(&id));
1009 auto vmPath = std::format("{}/{}", guestBase, wsl::shared::string::GuidToString<char>(id));
927 - THROW_IF_FAILED(m_virtualMachine->MountWindowsFolder(windowsPath, vmPath.c_str(), readOnly));
1010 + THROW_IF_FAILED(runtime.Vm().MountWindowsFolder(windowsPath, vmPath.c_str(), readOnly));
1011 mountedPaths.push_back(std::move(vmPath));
1012 return mountedPaths.back();
1013 };
@@ -1119,7 +1202,7 @@ try
1202 WSL_LOG("BuildImageStart", TraceLoggingValue(wsl::shared::string::Join(buildArgs, ' ').c_str(), "Command"));
1203
1204 ServiceProcessLauncher buildLauncher(buildArgs[0], buildArgs, buildEnv, WSLCProcessFlagsStdin);
1122 - auto buildProcess = buildLauncher.Launch(*m_virtualMachine);
1205 + auto buildProcess = buildLauncher.Launch(runtime.Vm());
1206
1207 // Opened before the IO context so it outlives the relay registered on it below.
1208 std::optional<UserHandle> userHandle;
@@ -1413,11 +1496,11 @@ try
1496 {
1497 WSLCExecutionContext context(this, WarningCallback);
1498
1416 - auto lock = m_lock.lock_shared();
1499 + auto lock = AcquireLease();
1500
1418 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1501 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
1502
1420 - auto requestContext = m_dockerClient->LoadImage(ContentSize);
1503 + auto requestContext = m_runtime.Docker().LoadImage(ContentSize);
1504
1505 std::ignore = ImportImageImpl(*requestContext, ImageHandle, LoadCallback);
1506
@@ -1447,11 +1530,11 @@ try
1530 tag = tagOrDigest.value();
1531 }
1532
1450 - auto lock = m_lock.lock_shared();
1533 + auto lock = AcquireLease();
1534
1452 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1535 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
1536
1454 - auto requestContext = m_dockerClient->ImportImage(repo, tag, ContentSize);
1537 + auto requestContext = m_runtime.Docker().ImportImage(repo, tag, ContentSize);
1538
1539 auto imageId = ImportImageImpl(*requestContext, ImageHandle);
1540 THROW_HR_IF_MSG(E_UNEXPECTED, !imageId.has_value(), "Docker import succeeded but did not return an image ID");
@@ -1481,7 +1564,7 @@ std::optional<std::string> WSLCSession::ImportImageImpl(DockerHTTPClient::HTTPRe
1564 comCall = RegisterUserCOMCallback();
1565 }
1566
1484 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1567 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
1568
1569 auto io = CreateIOContext();
1570
@@ -1618,11 +1701,11 @@ try
1701
1702 RETURN_HR_IF_NULL(E_POINTER, ImageNameOrID);
1703 RETURN_HR_IF(E_INVALIDARG, strlen(ImageNameOrID) > WSLC_MAX_IMAGE_NAME_LENGTH);
1621 - auto lock = m_lock.lock_shared();
1704 + auto lock = AcquireLease();
1705
1623 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1706 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
1707
1625 - auto retVal = m_dockerClient->SaveImage(ImageNameOrID);
1708 + auto retVal = m_runtime.Docker().SaveImage(ImageNameOrID);
1709 SaveImageImpl(retVal, OutHandle, CancelEvent);
1710 return S_OK;
1711 }
@@ -1651,11 +1734,11 @@ try
1734 names.emplace_back(ImageNames->Values[i]);
1735 }
1736
1654 - auto lock = m_lock.lock_shared();
1737 + auto lock = AcquireLease();
1738
1656 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1739 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
1740
1658 - auto retVal = m_dockerClient->SaveImages(names);
1741 + auto retVal = m_runtime.Docker().SaveImages(names);
1742 SaveImageImpl(retVal, OutHandle, CancelEvent);
1743 return S_OK;
1744 }
@@ -1665,7 +1748,7 @@ void WSLCSession::SaveImageImpl(std::pair<uint32_t, wil::unique_socket>& SocketC
1748 {
1749 auto userHandle = OpenUserHandle(OutputHandle);
1750
1668 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1751 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
1752
1753 auto io = CreateIOContext(CancelEvent);
1754
@@ -1727,14 +1810,14 @@ try
1810 filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Options->Filters, Options->FiltersCount);
1811 }
1812
1730 - auto lock = m_lock.lock_shared();
1813 + auto lock = AcquireLease();
1814
1732 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1815 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
1816
1817 std::vector<docker_schema::Image> images;
1818 try
1819 {
1737 - images = m_dockerClient->ListImages(all, digests, filters);
1820 + images = m_runtime.Docker().ListImages(all, digests, filters);
1821 }
1822 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list images");
1823
@@ -1836,14 +1919,14 @@ try
1919 *DeletedImages = nullptr;
1920 *Count = 0;
1921
1839 - auto lock = m_lock.lock_shared();
1922 + auto lock = AcquireLease();
1923
1841 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1924 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
1925
1926 std::vector<docker_schema::DeletedImage> deletedImages;
1927 try
1928 {
1846 - deletedImages = m_dockerClient->DeleteImage(
1929 + deletedImages = m_runtime.Docker().DeleteImage(
1930 Options->Image, WI_IsFlagSet(Options->Flags, WSLCDeleteImageFlagsForce), WI_IsFlagSet(Options->Flags, WSLCDeleteImageFlagsNoPrune));
1931 }
1932 catch (const DockerHTTPException& e)
@@ -1910,13 +1993,13 @@ try
1993 RETURN_HR_IF_NULL(E_POINTER, Options->Tag);
1994 RETURN_HR_IF(E_INVALIDARG, strlen(Options->Repo) + strlen(Options->Tag) + 1 > WSLC_MAX_IMAGE_NAME_LENGTH);
1995
1913 - auto lock = m_lock.lock_shared();
1996 + auto lock = AcquireLease();
1997
1915 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1998 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
1999
2000 try
2001 {
1919 - m_dockerClient->TagImage(Options->Image, Options->Repo, Options->Tag);
2002 + m_runtime.Docker().TagImage(Options->Image, Options->Repo, Options->Tag);
2003 }
2004 catch (const DockerHTTPException& e)
2005 {
@@ -1949,10 +2032,10 @@ try
2032 auto tagOrDigest = reference.TagOrDigest();
2033 EnforceRegistryAllowlist(repo);
2034
1952 - auto lock = m_lock.lock_shared();
1953 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
2035 + auto lock = AcquireLease();
2036 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
2037
1955 - auto requestContext = m_dockerClient->PushImage(repo.Name, tagOrDigest, RegistryAuthenticationInformation);
2038 + auto requestContext = m_runtime.Docker().PushImage(repo.Name, tagOrDigest, RegistryAuthenticationInformation);
2039 StreamImageOperation(*requestContext, Image, "Push", ProgressCallback);
2040
2041 return S_OK;
@@ -1970,8 +2053,8 @@ try
2053
2054 *Output = nullptr;
2055
1973 - auto lock = m_lock.lock_shared();
1974 - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
2056 + auto lock = AcquireLease();
2057 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
2058
2059 *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(InspectImageLockHeld(ImageNameOrId).c_str()).release();
2060
@@ -1984,7 +2067,7 @@ std::string WSLCSession::InspectImageLockHeld(const std::string& NameOrId)
2067 docker_schema::InspectImage dockerInspect;
2068 try
2069 {
1987 - dockerInspect = m_dockerClient->InspectImage(NameOrId);
2070 + dockerInspect = m_runtime.Docker().InspectImage(NameOrId);
2071 }
2072 catch (const DockerHTTPException& e)
2073 {
@@ -2018,14 +2101,14 @@ try
2101
2102 *IdentityToken = nullptr;
2103
2021 - auto lock = m_lock.lock_shared();
2022 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
2104 + auto lock = AcquireLease();
2105 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
2106
2107 wil::unique_cotaskmem_ansistring token;
2108
2109 try
2110 {
2028 - auto response = m_dockerClient->Authenticate(ServerAddress, Username, Password);
2111 + auto response = m_runtime.Docker().Authenticate(ServerAddress, Username, Password);
2112 token = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(response.c_str());
2113 }
2114 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to authenticate with registry: %hs", ServerAddress);
@@ -2050,13 +2133,13 @@ try
2133
2134 auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
2135
2053 - auto lock = m_lock.lock_shared();
2054 - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
2136 + auto lock = AcquireLease();
2137 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
2138
2139 docker_schema::PruneImageResult pruneResult;
2140 try
2141 {
2059 - pruneResult = m_dockerClient->PruneImages(filters);
2142 + pruneResult = m_runtime.Docker().PruneImages(filters);
2143 }
2144 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune images");
2145
@@ -2111,7 +2194,7 @@ try
2194 "Invalid process flags: 0x%x",
2195 containerOptions->InitProcessOptions.Flags);
2196
2114 - auto lock = m_lock.lock_shared();
2197 + auto lock = AcquireLease();
2198
2199 auto result = wil::ResultFromException([&]() { CreateContainerImpl(containerOptions, Container); });
2200
@@ -2132,10 +2215,10 @@ CATCH_RETURN();
2215
2216 void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptions, IWSLCContainer** Container)
2217 {
2135 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
2136 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_eventTracker);
2137 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient);
2138 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes);
2218 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
2219 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasEvents());
2220 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
2221 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes());
2222
2223 // Validate that name & images are valid.
2224 if (containerOptions->Name != nullptr && containerOptions->Name[0] != '\0')
@@ -2182,14 +2265,10 @@ void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptio
2265 *containerOptions,
2266 containerName,
2267 *this,
2185 - m_virtualMachine.value(),
2268 + m_runtime,
2269 m_pluginNotifier.get(),
2270 m_networks,
2188 - m_volumes.value(),
2189 - std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1),
2190 - m_eventTracker.value(),
2191 - m_dockerClient.value(),
2192 - m_ioRelay);
2271 + std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1));
2272
2273 // Key the map by Docker's container ID, which is set in the WSLCContainerImpl constructor and stable for its lifetime.
2274 auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container));
@@ -2222,7 +2301,7 @@ try
2301 ValidateName(Id, WSLC_MAX_CONTAINER_NAME_LENGTH);
2302
2303 // Look for an exact ID match first.
2225 - auto lock = m_lock.lock_shared();
2304 + auto lock = AcquireLease();
2305 std::lock_guard containersLock{m_containersLock};
2306
2307 // Purge containers that were auto-deleted via OnEvent (--rm).
@@ -2237,7 +2316,7 @@ try
2316
2317 try
2318 {
2240 - inspectResult = m_dockerClient->InspectContainer(Id);
2319 + inspectResult = m_runtime.Docker().InspectContainer(Id);
2320 }
2321 catch (DockerHTTPException& e)
2322 {
@@ -2261,6 +2340,77 @@ try
2340 }
2341 CATCH_RETURN();
2342
2343 +namespace {
2344 +
2345 + // Activity token holds an activity reference to prevent idle VM teardown while client holds it.
2346 + // Implements IFastRundown so crashed clients reclaim stub promptly instead of slow default rundown.
2347 + class ContainerOperation
2348 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IUnknown, IFastRundown>
2349 + {
2350 + public:
2351 + // Adopts an activity reference from CreateActivityToken; callback releases it.
2352 + void Initialize(std::function<void()>&& onRelease) noexcept
2353 + {
2354 + m_onRelease = std::move(onRelease);
2355 + }
2356 +
2357 + ~ContainerOperation() override
2358 + {
2359 + if (m_onRelease)
2360 + {
2361 + m_onRelease();
2362 + }
2363 + }
2364 +
2365 + private:
2366 + std::function<void()> m_onRelease;
2367 + };
2368 +
2369 +} // namespace
2370 +
2371 +Microsoft::WRL::ComPtr<IUnknown> WSLCSession::CreateActivityToken()
2372 +{
2373 + // Record the in-flight activity up front so the VM cannot idle-terminate before the caller
2374 + // takes ownership of the returned token.
2375 + m_runtime.Idle().AddActivity();
2376 + auto countCleanup = wil::scope_exit([this]() { m_runtime.Idle().ReleaseActivity(); });
2377 +
2378 + auto operation = Microsoft::WRL::Make<ContainerOperation>();
2379 + THROW_IF_NULL_ALLOC(operation.Get());
2380 +
2381 + // Capture shared idle state so token can outlive session and release activity without keeping session alive.
2382 + std::shared_ptr<IdleState> idleState = m_runtime.IdleStateShared();
2383 + operation->Initialize([idleState = std::move(idleState)]() { idleState->ReleaseActivity(); });
2384 +
2385 + // The token now owns the activity-count reference and will release it on destruction.
2386 + countCleanup.release();
2387 +
2388 + Microsoft::WRL::ComPtr<IUnknown> token;
2389 + THROW_IF_FAILED(operation.As(&token));
2390 + return token;
2391 +}
2392 +
2393 +HRESULT WSLCSession::BeginContainerOperation(IUnknown** Operation)
2394 +try
2395 +{
2396 + WSLCExecutionContext context(this);
2397 +
2398 + RETURN_HR_IF_NULL(E_POINTER, Operation);
2399 + *Operation = nullptr;
2400 +
2401 + // Do not start a new operation (which would hold the VM alive) once the session is terminating
2402 + // or has terminated. Mirrors the gate in EnsureVmRunning().
2403 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating.load() || m_sessionTerminatedEvent.is_signaled());
2404 +
2405 + // Record the in-flight operation up front so the VM cannot idle-terminate before the client
2406 + // resolves the container and issues the operation (and streams any output).
2407 + auto token = CreateActivityToken();
2408 +
2409 + RETURN_IF_FAILED(token.CopyTo(Operation));
2410 + return S_OK;
2411 +}
2412 +CATCH_RETURN();
2413 +
2414 HRESULT WSLCSession::ListContainers(
2415 const WSLCListContainersOptions* Options, WSLCContainerEntry** Containers, ULONG* Count, WSLCContainerPortMapping** Ports, ULONG* PortsCount)
2416 try
@@ -2295,13 +2445,13 @@ try
2445 filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Options->Filters, Options->FiltersCount);
2446 }
2447
2298 - auto lock = m_lock.lock_shared();
2299 - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
2448 + auto lock = AcquireLease();
2449 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
2450
2451 std::vector<docker_schema::ContainerInfo> dockerContainers;
2452 try
2453 {
2304 - dockerContainers = m_dockerClient->ListContainers(all, limit, filters);
2454 + dockerContainers = m_runtime.Docker().ListContainers(all, limit, filters);
2455 }
2456 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list containers");
2457
@@ -2374,8 +2524,8 @@ try
2524
2525 auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
2526
2377 - auto lock = m_lock.lock_shared();
2378 - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
2527 + auto lock = AcquireLease();
2528 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
2529
2530 std::lock_guard containersLock{m_containersLock};
2531
@@ -2383,7 +2533,7 @@ try
2533
2534 try
2535 {
2386 - pruneResult = m_dockerClient->PruneContainers(filters);
2536 + pruneResult = m_runtime.Docker().PruneContainers(filters);
2537 }
2538 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune containers");
2539
@@ -2430,7 +2580,7 @@ try
2580 CATCH_RETURN();
2581
2582 HRESULT WSLCSession::CreateRootNamespaceProcess(
2433 - LPCSTR Executable, const WSLCProcessOptions* Options, ULONG TtyRows, ULONG TtyColumns, IWSLCProcess** Process, int* Errno)
2583 + LPCSTR Executable, const WSLCProcessOptions* Options, ULONG TtyRows, ULONG TtyColumns, BOOL AcquireVmLease, IWSLCProcess** Process, int* Errno)
2584 try
2585 {
2586 WSLCExecutionContext context(this);
@@ -2445,10 +2595,26 @@ try
2595 *Errno = -1; // Make sure not to return 0 if something fails.
2596 }
2597
2448 - auto lock = m_lock.lock_shared();
2449 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
2598 + auto runtime = m_runtime.Acquire(LeasePolicyFor(AcquireVmLease));
2599 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
2600 +
2601 + auto process = runtime.Vm().CreateLinuxProcess(Executable, *Options, TtyRows, TtyColumns, Errno);
2602 +
2603 + // The VmLease above is released when this call returns, but the process keeps running in the
2604 + // VM and the client holds the returned proxy. A root-namespace process is not tracked as a
2605 + // container, so attach an activity token bound to the process's lifetime; this keeps the VM
2606 + // alive for as long as the client holds the process, preventing the idle worker from tearing
2607 + // the VM down and killing the process out from under the client.
2608 + //
2609 + // Not for a plugin-originated call: it was served by whatever VM was already running, possibly
2610 + // one already committed to stopping, and a plugin must never extend a VM's life. Attaching a
2611 + // token anyway would keep counting activity for as long as the plugin holds the proxy and would
2612 + // block idle termination of every subsequent VM in this session.
2613 + if (AcquireVmLease)
2614 + {
2615 + process->SetKeepAliveToken(CreateActivityToken());
2616 + }
2617
2451 - auto process = m_virtualMachine->CreateLinuxProcess(Executable, *Options, TtyRows, TtyColumns, Errno);
2618 THROW_IF_FAILED(process.CopyTo(Process));
2619
2620 return S_OK;
@@ -2459,7 +2625,7 @@ void WSLCSession::Ext4Format(const std::string& Device)
2625 {
2626 constexpr auto mkfsPath = "/usr/sbin/mkfs.ext4";
2627 ServiceProcessLauncher launcher(mkfsPath, {mkfsPath, Device});
2462 - auto result = launcher.Launch(*m_virtualMachine).WaitAndCaptureOutput();
2628 + auto result = launcher.Launch(m_runtime.Vm()).WaitAndCaptureOutput();
2629
2630 THROW_HR_IF_MSG(E_FAIL, result.Code != 0, "%hs", launcher.FormatResult(result).c_str());
2631 }
@@ -2471,17 +2637,17 @@ try
2637
2638 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(Path), !std::filesystem::path(Path).is_absolute());
2639
2474 - auto lock = m_lock.lock_shared();
2475 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
2640 + auto lock = AcquireLease();
2641 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
2642
2643 // Attach the disk to the VM (AttachDisk() performs the access check for the VHD file).
2478 - auto [lun, device] = m_virtualMachine->AttachDisk(Path, false);
2644 + auto [lun, device] = m_runtime.Vm().AttachDisk(Path, false);
2645
2646 // N.B. DetachDisk calls sync() before detaching.
2481 - auto detachDisk = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, lun]() { m_virtualMachine->DetachDisk(lun); });
2647 + auto detachDisk = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, lun]() { m_runtime.Vm().DetachDisk(lun); });
2648
2649 // Format it to ext4.
2484 - m_virtualMachine->Ext4Format(device);
2650 + m_runtime.Vm().Ext4Format(device);
2651
2652 return S_OK;
2653 }
@@ -2499,15 +2665,15 @@ try
2665 auto driverOpts = wslutil::ParseKeyValuePairs(Options->DriverOpts, Options->DriverOptsCount);
2666 auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCVolumeMetadataLabel);
2667
2502 - auto lock = m_lock.lock_shared();
2503 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes);
2668 + auto lock = AcquireLease();
2669 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes());
2670
2671 if (Options->Name != nullptr && Options->Name[0] != '\0')
2672 {
2673 ValidateName(Options->Name, WSLC_MAX_VOLUME_NAME_LENGTH);
2674 }
2675
2510 - *VolumeInfo = m_volumes->CreateVolume(Options->Name, Options->Driver, std::move(driverOpts), std::move(labels));
2676 + *VolumeInfo = m_runtime.Volumes().CreateVolume(Options->Name, Options->Driver, std::move(driverOpts), std::move(labels));
2677 return S_OK;
2678 }
2679 CATCH_RETURN();
@@ -2519,10 +2685,10 @@ try
2685
2686 RETURN_HR_IF_NULL(E_POINTER, Name);
2687
2522 - auto lock = m_lock.lock_shared();
2523 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes);
2688 + auto lock = AcquireLease();
2689 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes());
2690
2525 - m_volumes->DeleteVolume(Name);
2691 + m_runtime.Volumes().DeleteVolume(Name);
2692 return S_OK;
2693 }
2694 CATCH_RETURN();
@@ -2540,10 +2706,10 @@ try
2706
2707 auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
2708
2543 - auto lock = m_lock.lock_shared();
2544 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes);
2709 + auto lock = AcquireLease();
2710 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes());
2711
2546 - auto volumeList = m_volumes->ListVolumes(std::move(filters));
2712 + auto volumeList = m_runtime.Volumes().ListVolumes(std::move(filters));
2713
2714 if (volumeList.empty())
2715 {
@@ -2572,10 +2738,10 @@ try
2738 std::string name = Name;
2739 ValidateName(name.c_str(), WSLC_MAX_VOLUME_NAME_LENGTH);
2740
2575 - auto lock = m_lock.lock_shared();
2576 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes);
2741 + auto lock = AcquireLease();
2742 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes());
2743
2578 - std::string json = m_volumes->InspectVolume(name);
2744 + std::string json = m_runtime.Volumes().InspectVolume(name);
2745 *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(json.c_str()).release();
2746
2747 return S_OK;
@@ -2597,13 +2763,13 @@ try
2763
2764 auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
2765
2600 - auto lock = m_lock.lock_shared();
2601 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes);
2766 + auto lock = AcquireLease();
2767 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVolumes());
2768
2769 WSLCVolumes::PruneVolumesResult pruneResult;
2770 try
2771 {
2606 - pruneResult = m_volumes->PruneVolumes(filters);
2772 + pruneResult = m_runtime.Volumes().PruneVolumes(filters);
2773 }
2774 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune volumes");
2775
@@ -2629,32 +2795,6 @@ try
2795 }
2796 CATCH_RETURN();
2797
2632 -int WSLCSession::StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs)
2633 -{
2634 - auto signalResult = Process.Get().Signal(WSLCSignalSIGTERM);
2635 - if (FAILED(signalResult))
2636 - {
2637 - LOG_HR_MSG(signalResult, "Failed to terminate process %i", Process.Get().GetPid());
2638 - return -1;
2639 - }
2640 -
2641 - try
2642 - {
2643 - return Process.Wait(TerminateTimeoutMs);
2644 - }
2645 - catch (...)
2646 - {
2647 - LOG_CAUGHT_EXCEPTION();
2648 - try
2649 - {
2650 - LOG_IF_FAILED(Process.Get().Signal(WSLCSignalSIGKILL));
2651 - return Process.Wait(KillTimeoutMs);
2652 - }
2653 - CATCH_LOG();
2654 - }
2655 -
2656 - return -1;
2657 -}
2798 // Network management.
2799
2800 HRESULT WSLCSession::CreateNetwork(const WSLCNetworkOptions* Options, IWarningCallback* WarningCallback)
@@ -2676,9 +2816,9 @@ try
2816 auto driverOpts = wslutil::ParseKeyValuePairs(Options->DriverOpts, Options->DriverOptsCount);
2817 auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCNetworkManagedLabel);
2818
2679 - auto lock = m_lock.lock_shared();
2680 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient);
2681 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
2819 + auto lock = AcquireLease();
2820 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
2821 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
2822
2823 std::lock_guard networksLock(m_networksLock);
2824 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), m_networks.contains(name));
@@ -2725,7 +2865,7 @@ try
2865 docker_schema::CreateNetworkResponse createResult;
2866 try
2867 {
2728 - createResult = m_dockerClient->CreateNetwork(request);
2868 + createResult = m_runtime.Docker().CreateNetwork(request);
2869 }
2870 catch (const DockerHTTPException& e)
2871 {
@@ -2739,14 +2879,15 @@ try
2879 EMIT_USER_WARNING(wsl::shared::string::MultiByteToWide(createResult.Warning));
2880 }
2881
2742 - auto removeNetworkCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, &name]() { m_dockerClient->RemoveNetwork(name); });
2882 + auto removeNetworkCleanup =
2883 + wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, &name]() { m_runtime.Docker().RemoveNetwork(name); });
2884
2885 // Inspect the newly created network to cache full properties (IPAM, Scope, etc.)
2886 // since CreateNetworkResponse only returns {Id, Warning}.
2887 docker_schema::Network full;
2888 try
2889 {
2749 - full = m_dockerClient->InspectNetwork(name);
2890 + full = m_runtime.Docker().InspectNetwork(name);
2891 }
2892 catch (const DockerHTTPException& e)
2893 {
@@ -2793,9 +2934,9 @@ try
2934 std::string name = Name;
2935 ValidateName(name.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH);
2936
2796 - auto lock = m_lock.lock_shared();
2797 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient);
2798 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
2937 + auto lock = AcquireLease();
2938 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
2939 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
2940
2941 std::lock_guard networksLock(m_networksLock);
2942
@@ -2804,7 +2945,7 @@ try
2945
2946 try
2947 {
2807 - m_dockerClient->RemoveNetwork(name);
2948 + m_runtime.Docker().RemoveNetwork(name);
2949 }
2950 catch (const DockerHTTPException& e)
2951 {
@@ -2833,7 +2974,7 @@ try
2974 *Networks = nullptr;
2975 *Count = 0;
2976
2836 - auto lock = m_lock.lock_shared();
2977 + auto lock = AcquireLease();
2978 std::lock_guard networksLock(m_networksLock);
2979
2980 if (m_networks.empty())
@@ -2872,7 +3013,7 @@ try
3013 std::string name = Name;
3014 ValidateName(name.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH);
3015
2875 - auto lock = m_lock.lock_shared();
3016 + auto lock = AcquireLease();
3017 std::lock_guard networksLock(m_networksLock);
3018
3019 auto it = m_networks.find(name);
@@ -2928,16 +3069,16 @@ try
3069 // Scope the prune to WSLC-managed networks.
3070 filters["label"].push_back(WSLCNetworkManagedLabel);
3071
2931 - auto lock = m_lock.lock_shared();
2932 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient);
2933 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
3072 + auto lock = AcquireLease();
3073 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker());
3074 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
3075
3076 std::lock_guard networksLock(m_networksLock);
3077
3078 docker_schema::PruneNetworkResult pruneResult;
3079 try
3080 {
2940 - pruneResult = m_dockerClient->PruneNetworks(filters);
3081 + pruneResult = m_runtime.Docker().PruneNetworks(filters);
3082 }
3083 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to prune networks");
3084
@@ -3008,10 +3149,10 @@ bool WSLCSession::WaitForEventOrSessionTerminating(HANDLE Event, std::chrono::mi
3149 HRESULT WSLCSession::Terminate()
3150 try
3151 {
3011 - // Ensure only one Terminate() runs. This must be checked before taking m_lock
3012 - // because OnVmExited() is called from the IORelay thread — if an external Terminate()
3013 - // holds m_lock and calls m_ioRelay.Stop(), the relay thread must not re-enter
3014 - // Terminate() and deadlock on m_lock.
3152 + // Ensure only one Terminate() runs. This must be checked before taking the runtime's exclusive
3153 + // lock because OnVmExited() is called from the IORelay thread — if an external Terminate()
3154 + // holds that lock and calls m_runtime.Relay()->Stop(), the relay thread must not re-enter
3155 + // Terminate() and deadlock on it.
3156 if (m_terminating.exchange(true))
3157 {
3158 return S_OK;
@@ -3033,8 +3174,8 @@ try
3174 {
3175 std::lock_guard lock(m_userHandlesLock);
3176
3036 - // m_sessionTerminatingEvent is always valid, so it can be signalled without holding m_lock.
3037 - // This allows a session to be unblocked if a stuck operation is holding m_lock.
3177 + // m_sessionTerminatingEvent is always valid, so it can be signalled without holding the runtime lock.
3178 + // This allows a session to be unblocked if a stuck operation is holding the runtime lock.
3179 // N.B. This must happen under m_userHandlesLock to synchronize with potentially running operations.
3180 if (!m_sessionTerminatingEvent.is_signaled())
3181 {
@@ -3054,101 +3195,20 @@ try
3195 CancelUserCOMCallbacks();
3196 }
3197
3057 - sessionLock = m_lock.try_lock_exclusive();
3198 + sessionLock = m_runtime.TryLockExclusive();
3199 retrying = true;
3200 }
3201
3061 - // Acquire an exclusive lock to ensure that no operation is running.
3062 - WI_VERIFY(sessionLock);
3063 -
3064 - std::lock_guard containersLock(m_containersLock);
3065 - std::lock_guard networksLock(m_networksLock);
3066 -
3067 - m_containers.clear();
3068 - m_volumes.reset();
3069 - m_networks.clear();
3070 -
3071 - // Stop the IO relay.
3072 - // This stops:
3073 - // - container state monitoring.
3074 - // - container init process relays
3075 - // - execs relays
3076 - // - container logs relays
3077 - m_ioRelay.Stop();
3202 + m_runtime.Shutdown(sessionLock, m_terminationReason, m_terminationDetails);
3203
3204 + // Idle teardown is disabled and no operation can run past termination, so the parked VM
3205 + // factory can no longer be re-fetched; revoke it from the GIT.
3206 + if (m_vmFactoryGitCookie != 0)
3207 {
3080 - std::lock_guard allocatedPortsLock(m_allocatedPortsLock);
3081 - m_allocatedPorts.clear();
3208 + LOG_IF_FAILED(m_git->RevokeInterfaceFromGlobal(m_vmFactoryGitCookie));
3209 + m_vmFactoryGitCookie = 0;
3210 }
3211
3084 - m_eventTracker.reset();
3085 - m_dockerClient.reset();
3086 -
3087 - // Check if the VM has already exited (e.g., killed externally).
3088 - // If so, skip operations that require a live VM to avoid unnecessary waits.
3089 - // N.B. m_vmExitedEvent may be uninitialized if Terminate() is called from the
3090 - // Initialize() error path before GetTerminationEvent() succeeds.
3091 - if (m_vmExitedEvent && m_vmExitedEvent.is_signaled())
3092 - {
3093 - WSL_LOG("SkippingGracefulShutdown_VmDead", TraceLoggingValue(m_id, "SessionId"));
3094 -
3095 - // The VM exited on its own, so it recorded the cause.
3096 - if (m_virtualMachine)
3097 - {
3098 - wil::unique_cotaskmem_string details;
3099 - LOG_IF_FAILED(m_virtualMachine->GetTerminationReason(&m_terminationReason, &details));
3100 - m_terminationDetails = details ? details.get() : L"";
3101 - }
3102 - }
3103 - else
3104 - {
3105 - // The VM is still alive, so this is a graceful shutdown initiated by us.
3106 - m_terminationReason = WSLCVirtualMachineTerminationReasonShutdown;
3107 -
3108 - if (m_virtualMachine)
3109 - {
3110 - m_virtualMachine->OnSessionTerminated();
3111 -
3112 - // Stop dockerd first, then containerd (dockerd is a client of containerd).
3113 - // N.B. dockerd waits a couple seconds if there are any outstanding HTTP request sockets opened.
3114 - if (m_dockerdProcess.has_value())
3115 - {
3116 - auto dockerdExitCode = StopProcess(m_dockerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs);
3117 - WSL_LOG("DockerdExit", TraceLoggingValue(dockerdExitCode, "code"));
3118 - }
3119 -
3120 - if (m_containerdProcess.has_value())
3121 - {
3122 - auto containerdExitCode = StopProcess(m_containerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs);
3123 - WSL_LOG("ContainerdExit", TraceLoggingValue(containerdExitCode, "code"));
3124 - }
3125 -
3126 - // N.B. dockerd has exited by this point, so unmounting the VHD is safe since no container can be running.
3127 - if (m_storageMounted)
3128 - {
3129 - try
3130 - {
3131 - m_virtualMachine->Unmount(c_containerdStorage);
3132 - m_storageMounted = false;
3133 - }
3134 - CATCH_LOG();
3135 - }
3136 - }
3137 - }
3138 -
3139 - m_dockerdProcess.reset();
3140 - m_containerdProcess.reset();
3141 - m_virtualMachine.reset();
3142 -
3143 - // Delete the ephemeral swap VHD now that the VM is gone.
3144 - if (!m_swapVhdPath.empty())
3145 - {
3146 - LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_swapVhdPath.c_str()));
3147 - m_swapVhdPath.clear();
3148 - }
3149 -
3150 - m_sessionTerminatedEvent.SetEvent();
3151 -
3212 return S_OK;
3213 }
3214 CATCH_RETURN();
@@ -3208,7 +3268,7 @@ try
3268 }
3269 CATCH_LOG();
3270
3211 -HRESULT WSLCSession::MountWindowsFolder(LPCWSTR WindowsPath, LPCSTR LinuxPath, BOOL ReadOnly)
3271 +HRESULT WSLCSession::MountWindowsFolder(LPCWSTR WindowsPath, LPCSTR LinuxPath, BOOL ReadOnly, BOOL AcquireVmLease)
3272 try
3273 {
3274 WSLCExecutionContext context(this);
@@ -3216,24 +3276,24 @@ try
3276 RETURN_HR_IF_NULL(E_POINTER, WindowsPath);
3277 RETURN_HR_IF_NULL(E_POINTER, LinuxPath);
3278
3219 - auto lock = m_lock.lock_shared();
3220 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
3279 + auto lock = AcquireLease(LeasePolicyFor(AcquireVmLease));
3280 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
3281
3222 - return m_virtualMachine->MountWindowsFolder(WindowsPath, LinuxPath, ReadOnly);
3282 + return m_runtime.Vm().MountWindowsFolder(WindowsPath, LinuxPath, ReadOnly);
3283 }
3284 CATCH_RETURN();
3285
3226 -HRESULT WSLCSession::UnmountWindowsFolder(LPCSTR LinuxPath)
3286 +HRESULT WSLCSession::UnmountWindowsFolder(LPCSTR LinuxPath, BOOL AcquireVmLease)
3287 try
3288 {
3289 WSLCExecutionContext context(this);
3290
3291 RETURN_HR_IF_NULL(E_POINTER, LinuxPath);
3292
3233 - auto lock = m_lock.lock_shared();
3234 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
3293 + auto lock = AcquireLease(LeasePolicyFor(AcquireVmLease));
3294 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
3295
3236 - return m_virtualMachine->UnmountWindowsFolder(LinuxPath);
3296 + return m_runtime.Vm().UnmountWindowsFolder(LinuxPath);
3297 }
3298 CATCH_RETURN();
3299
@@ -3242,36 +3302,37 @@ try
3302 {
3303 WSLCExecutionContext context(this);
3304
3245 - auto lock = m_lock.lock_shared();
3246 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
3305 + auto lock = AcquireLease();
3306 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
3307
3248 - std::lock_guard allocatedPortsLock(m_allocatedPortsLock);
3308 + std::lock_guard allocatedPortsLock(m_runtime.AllocatedPortsLock());
3309
3310 // Look for an existing allocation first.
3251 - auto it = m_allocatedPorts.find(LinuxPort);
3311 + auto& allocatedPorts = m_runtime.AllocatedPorts();
3312 + auto it = allocatedPorts.find(LinuxPort);
3313
3314 bool inserted = false;
3315 auto cleanup = wil::scope_exit([&]() {
3316 if (inserted)
3317 {
3257 - m_allocatedPorts.erase(it);
3318 + allocatedPorts.erase(it);
3319 }
3320 });
3321
3261 - if (it == m_allocatedPorts.end())
3322 + if (it == allocatedPorts.end())
3323 {
3324 // No existing port allocation, create a new one.
3264 - auto allocated = std::make_pair(m_virtualMachine->TryAllocatePort(LinuxPort, Family, IPPROTO_TCP), static_cast<size_t>(0));
3325 + auto allocated = std::make_pair(m_runtime.Vm().TryAllocatePort(LinuxPort, Family, IPPROTO_TCP), static_cast<size_t>(0));
3326 THROW_HR_IF(HRESULT_FROM_WIN32(WSAEADDRINUSE), allocated.first == nullptr);
3327
3267 - it = m_allocatedPorts.emplace(LinuxPort, allocated).first;
3328 + it = allocatedPorts.emplace(LinuxPort, allocated).first;
3329 inserted = true;
3330 }
3331
3332 auto mapping = VMPortMapping::LocalhostTcpMapping(Family, WindowsPort);
3333 mapping.AssignVmPort(it->second.first);
3334
3274 - m_virtualMachine->MapPort(mapping);
3335 + m_runtime.Vm().MapPort(mapping);
3336
3337 // Increase usage count.
3338 it->second.second++;
@@ -3288,34 +3349,48 @@ try
3349 {
3350 WSLCExecutionContext context(this);
3351
3291 - auto lock = m_lock.lock_shared();
3292 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
3352 + auto lock = AcquireLease();
3353 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm());
3354
3294 - std::lock_guard allocatedPortsLock(m_allocatedPortsLock);
3355 + std::lock_guard allocatedPortsLock(m_runtime.AllocatedPortsLock());
3356
3296 - auto it = m_allocatedPorts.find(LinuxPort);
3297 - RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == m_allocatedPorts.end());
3357 + auto& allocatedPorts = m_runtime.AllocatedPorts();
3358 + auto it = allocatedPorts.find(LinuxPort);
3359 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == allocatedPorts.end());
3360
3361 auto mapping = VMPortMapping::LocalhostTcpMapping(Family, WindowsPort);
3362 mapping.AssignVmPort(it->second.first);
3301 - mapping.Attach(m_virtualMachine.value());
3363 + mapping.Attach(m_runtime.Vm());
3364
3365 auto cleanup = wil::scope_exit([&]() { mapping.Release(); });
3366
3305 - m_virtualMachine->UnmapPort(mapping);
3367 + m_runtime.Vm().UnmapPort(mapping);
3368
3369 it->second.second--;
3370
3371 // If usage count drops to 0, release the port allocation.
3372 if (it->second.second == 0)
3373 {
3312 - m_allocatedPorts.erase(it);
3374 + allocatedPorts.erase(it);
3375 }
3376
3377 return S_OK;
3378 }
3379 CATCH_RETURN();
3380
3381 +HRESULT WSLCSession::TriggerIdleTermination(BOOL* WasAlreadyIdle)
3382 +try
3383 +{
3384 + WSLCExecutionContext context(this);
3385 +
3386 + THROW_HR_IF_NULL(E_POINTER, WasAlreadyIdle);
3387 +
3388 + *WasAlreadyIdle = m_runtime.TriggerIdleTerminationForTest() ? TRUE : FALSE;
3389 +
3390 + return S_OK;
3391 +}
3392 +CATCH_RETURN();
3393 +
3394 HRESULT WSLCSession::InterfaceSupportsErrorInfo(REFIID riid)
3395 {
3396 return riid == __uuidof(IWSLCSession) || riid == __uuidof(IWSLCCompatSession) ? S_OK : S_FALSE;
@@ -3617,7 +3692,11 @@ void WSLCSession::CancelUserCOMCallbacks()
3692
3693 void WSLCSession::OnContainerDeleted(const WSLCContainerImpl* Container)
3694 {
3620 - auto lock = m_lock.lock_shared();
3695 + // N.B. Invoked only from WSLCContainer::Delete, which already holds a VmLease (the shared
3696 + // session lock). The lease prevents a concurrent idle teardown from clearing m_containers,
3697 + // so this only needs m_containersLock. It must NOT re-acquire the shared session lock here:
3698 + // doing so while the idle worker is queued for the exclusive lock would deadlock (recursive
3699 + // shared acquire behind a pending writer).
3700 std::lock_guard containersLock(m_containersLock);
3701
3702 // N.B. once a container transitions to a 'Deleted' state, a call to ListContainers() can remove it from m_containers.
@@ -3668,26 +3747,38 @@ CATCH_RETURN();
3747
3748 void WSLCSession::RecoverExistingContainers()
3749 {
3671 - WI_ASSERT(m_dockerClient.has_value());
3672 - WI_ASSERT(m_eventTracker.has_value());
3673 - WI_ASSERT(m_virtualMachine.has_value());
3750 + WI_ASSERT(m_runtime.HasDocker());
3751 + WI_ASSERT(m_runtime.HasEvents());
3752 + WI_ASSERT(m_runtime.HasVm());
3753
3675 - auto containers = m_dockerClient->ListContainers(true); // all=true to include stopped containers
3754 + auto containers = m_runtime.Docker().ListContainers(true); // all=true to include stopped containers
3755
3756 + std::lock_guard containersLock(m_containersLock);
3757 for (const auto& dockerContainer : containers)
3758 {
3759 + // Keep existing wrappers and their client COM references in place, then re-register their
3760 + // ports against the restarted VM.
3761 + if (auto existing = m_containers.find(dockerContainer.Id); existing != m_containers.end())
3762 + {
3763 + // Isolate recovery failures to this container so one bad container cannot fail lazy start
3764 + // for every client, mirroring the Open() failure path below.
3765 + try
3766 + {
3767 + existing->second->RecoverPorts(dockerContainer);
3768 + }
3769 + catch (...)
3770 + {
3771 + LOG_CAUGHT_EXCEPTION_MSG("Failed to recover container state: %hs", dockerContainer.Id.c_str());
3772 + EMIT_USER_WARNING(
3773 + Localization::MessageWslcFailedToRecoverContainer(wsl::shared::string::MultiByteToWide(dockerContainer.Id)));
3774 + }
3775 + continue;
3776 + }
3777 +
3778 try
3779 {
3780 auto container = WSLCContainerImpl::Open(
3682 - dockerContainer,
3683 - *this,
3684 - m_virtualMachine.value(),
3685 - m_pluginNotifier.get(),
3686 - m_volumes.value(),
3687 - std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1),
3688 - m_eventTracker.value(),
3689 - m_dockerClient.value(),
3690 - m_ioRelay);
3781 + dockerContainer, *this, m_runtime, m_pluginNotifier.get(), std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1));
3782
3783 auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container));
3784 WI_ASSERT(inserted);
@@ -3708,10 +3799,10 @@ void WSLCSession::RecoverExistingContainers()
3799
3800 void WSLCSession::RecoverExistingNetworks()
3801 {
3711 - WI_ASSERT(m_dockerClient.has_value());
3712 - WI_ASSERT(m_virtualMachine.has_value());
3802 + WI_ASSERT(m_runtime.HasDocker());
3803 + WI_ASSERT(m_runtime.HasVm());
3804
3714 - auto networks = m_dockerClient->ListNetworks();
3805 + auto networks = m_runtime.Docker().ListNetworks();
3806
3807 std::lock_guard networksLock(m_networksLock);
3808
src/windows/wslcsession/WSLCSession.h
+80 -31
@@ -18,12 +18,16 @@ Abstract:
18 #include "WSLCCompat.h"
19 #include "WSLCVirtualMachine.h"
20 #include "WSLCContainer.h"
21 +#include "WSLCIdleState.h"
22 #include "WSLCVolumes.h"
23 +#include "WSLCSessionRuntime.h"
24 #include "WSLCNetworkMetadata.h"
25 #include "DockerEventTracker.h"
26 #include "DockerHTTPClient.h"
27 #include "IORelay.h"
28 +#include <atomic>
29 #include <list>
30 +#include <optional>
31 #include <unordered_map>
32
33 namespace wsl::windows::service::wslc {
@@ -71,13 +75,20 @@ private:
75 //
76 // WSLCSession - Implements IWSLCSession for container management.
77 // Runs in a per-user COM server process for security isolation.
74 -// The SYSTEM service creates the VM and passes IWSLCVirtualMachine to Initialize().
78 +// The SYSTEM service passes an IWSLCVirtualMachineFactory to Initialize(); the VM is created
79 +// lazily on first use and may be torn down when idle and recreated on demand.
80 //
81 class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession
82 : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::WinRtClassicComMix>, IWSLCSession, IWSLCCompatSession, IFastRundown, ISupportErrorInfo>
83 {
84 + // WSLCContainer::Delete acquires a VmLease to keep the VM alive (and block idle
85 + // teardown) for the duration of a container deletion.
86 + friend class WSLCContainer;
87 +
88 public:
80 - WSLCSession() = default;
89 + WSLCSession() : m_runtime(*this)
90 + {
91 + }
92
93 ~WSLCSession();
94
@@ -143,6 +154,7 @@ public:
154 // Container management.
155 IFACEMETHOD(CreateContainer)(_In_ const WSLCContainerOptions* Options, _In_opt_ IWarningCallback* WarningCallback, _Out_ IWSLCContainer** Container) override;
156 IFACEMETHOD(OpenContainer)(_In_ LPCSTR Id, _In_ IWSLCContainer** Container) override;
157 + IFACEMETHOD(BeginContainerOperation)(_Outptr_ IUnknown** Operation) override;
158 IFACEMETHOD(ListContainers)(
159 _In_opt_ const WSLCListContainersOptions* Options,
160 _Out_ WSLCContainerEntry** Containers,
@@ -157,6 +169,7 @@ public:
169 _In_ const WSLCProcessOptions* Options,
170 _In_ ULONG TtyRows,
171 _In_ ULONG TtyColumns,
172 + _In_ BOOL AcquireVmLease,
173 _Out_ IWSLCProcess** VirtualMachine,
174 _Out_ int* Errno) override;
175
@@ -199,10 +212,11 @@ public:
212 IFACEMETHOD(InterfaceSupportsErrorInfo)(_In_ REFIID riid) override;
213
214 // Testing.
202 - IFACEMETHOD(MountWindowsFolder)(_In_ LPCWSTR WindowsPath, _In_ LPCSTR LinuxPath, _In_ BOOL ReadOnly) override;
203 - IFACEMETHOD(UnmountWindowsFolder)(_In_ LPCSTR LinuxPath) override;
215 + IFACEMETHOD(MountWindowsFolder)(_In_ LPCWSTR WindowsPath, _In_ LPCSTR LinuxPath, _In_ BOOL ReadOnly, _In_ BOOL AcquireVmLease) override;
216 + IFACEMETHOD(UnmountWindowsFolder)(_In_ LPCSTR LinuxPath, _In_ BOOL AcquireVmLease) override;
217 IFACEMETHOD(MapVmPort)(_In_ int Family, _In_ unsigned short WindowsPort, _In_ unsigned short LinuxPort) override;
218 IFACEMETHOD(UnmapVmPort)(_In_ int Family, _In_ unsigned short WindowsPort, _In_ unsigned short LinuxPort) override;
219 + IFACEMETHOD(TriggerIdleTermination)(_Out_ BOOL* WasAlreadyIdle) override;
220
221 // IWSLCCompatSession - converts the WSLCCompat types to the wslc.idl types and forwards to the methods above.
222 // Methods that have an identical signature in both interfaces (Terminate, DeleteVolume, Authenticate,
@@ -260,34 +274,65 @@ public:
274
275 bool WaitForEventOrSessionTerminating(HANDLE Event, std::chrono::milliseconds Timeout) const;
276
277 + // Shared idle-termination state. Exposed so VM-scoped objects (e.g. running containers via
278 + // WSLCContainerImpl's ActivityRef) can hold an activity reference for their lifetime without
279 + // keeping the session object itself alive.
280 + std::shared_ptr<IdleState> IdleStateShared() const noexcept
281 + {
282 + return m_runtime.IdleStateShared();
283 + }
284 +
285 + WSLCSessionRuntime& Runtime() noexcept
286 + {
287 + return m_runtime;
288 + }
289 +
290 + const WSLCSessionRuntime& Runtime() const noexcept
291 + {
292 + return m_runtime;
293 + }
294 +
295 + // Creates an opaque activity token that holds a reference on this session's activity count for
296 + // its lifetime, deferring idle teardown of the VM until every outstanding token is released.
297 + // Used both for transient client operations (BeginContainerOperation) and to keep the VM alive
298 + // for the lifetime of a process whose wrapper a client may keep (root-namespace and exec'd
299 + // processes).
300 + Microsoft::WRL::ComPtr<IUnknown> CreateActivityToken();
301 +
302 private:
303 ULONG m_id = 0;
304 + void PersistSettings(const WSLCSessionInitSettings& Settings, PSID UserSid);
305 +
306 + using VmLease = WSLCSessionRuntime::VmLease;
307 + [[nodiscard]] VmLease AcquireLease(WSLCSessionRuntime::VmLeasePolicy Policy = WSLCSessionRuntime::VmLeasePolicy::Acquire);
308 +
309 + // Maps the AcquireVmLease flag that plugin-reachable methods carry onto the lease policy. The
310 + // service passes FALSE for every plugin-originated call: a plugin is never a reason to create a
311 + // VM, so it is served by the running one -- including one committed to stopping -- or rejected.
312 + [[nodiscard]] static constexpr WSLCSessionRuntime::VmLeasePolicy LeasePolicyFor(BOOL AcquireVmLease) noexcept
313 + {
314 + return AcquireVmLease ? WSLCSessionRuntime::VmLeasePolicy::Acquire : WSLCSessionRuntime::VmLeasePolicy::ExistingOnly;
315 + }
316
317 __requires_lock_held(m_userHandlesLock) void CancelUserHandleIO();
318 __requires_lock_held(m_userCOMCallbacksLock) void CancelUserCOMCallbacks();
319
269 - _Requires_shared_lock_held_(m_lock)
320 void CreateContainerImpl(const WSLCContainerOptions* Options, IWSLCContainer** Container);
321
322 void ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID UserSid);
323
324 void Ext4Format(const std::string& Device);
275 - _Requires_shared_lock_held_(m_lock)
325 std::string InspectImageLockHeld(const std::string& Id);
326 void OnContainerDeleted(const WSLCContainerImpl* Container);
327
328 void OnCrashDumpWritten(const std::wstring& DumpPath, const std::string& ProcessName, ULONG Pid, ULONG Signal, ULONGLONG Timestamp);
329
281 - _Requires_shared_lock_held_(m_lock)
330 void OnImageCreated(const std::string& ImageNameOrId) noexcept;
331
284 - _Requires_shared_lock_held_(m_lock)
332 void OnImageDeleted(const std::string& ImageId) noexcept;
333
287 - void OnProcessLog(const gsl::span<char>& Data, PCSTR Source);
334 void OnContainerdExited();
335 void OnDockerdExited();
290 - void OnVmExited();
336 ServiceRunningProcess StartProcess(
337 const std::string& Executable, const std::vector<std::string>& Args, PCSTR LogSource, std::function<void()>&& ExitCallback);
338 void InstallTrustedRootCertificates();
@@ -302,35 +347,41 @@ private:
347 void SaveImageImpl(std::pair<uint32_t, wil::unique_socket>& RequestCodePair, WSLCHandle OutputHandle, HANDLE CancelEvent);
348 void StreamImageOperation(DockerHTTPClient::HTTPRequestContext& requestContext, LPCSTR Image, LPCSTR OperationName, IProgressCallback* ProgressCallback);
349
305 - std::optional<DockerHTTPClient> m_dockerClient;
306 - std::optional<WSLCVirtualMachine> m_virtualMachine;
307 - std::optional<DockerEventTracker> m_eventTracker;
308 - wil::unique_event m_dockerdReadyEvent{wil::EventOptions::ManualReset};
350 + // The VM factory is a cross-process proxy supplied by the SYSTEM service at Initialize() time
351 + // but first used later (on demand) from a different thread/apartment. A directly stored proxy
352 + // would fail with RPC_E_WRONG_THREAD, so it is parked in the process Global Interface Table and
353 + // re-fetched (re-marshalled into the calling apartment) each time a VM is created.
354 + wil::com_ptr<IGlobalInterfaceTable> m_git;
355 + DWORD m_vmFactoryGitCookie{};
356 +
357 + WSLCSessionRuntime m_runtime;
358 std::wstring m_displayName;
359 std::wstring m_creatorProcessName;
360 std::filesystem::path m_storageVhdPath;
312 - std::filesystem::path m_swapVhdPath;
313 - bool m_storageMounted = false;
361
315 - // N.B. m_lock must be acquired before acquiring m_containersLock or m_networksLock.
316 - // These locks protect m_containers without requiring an exclusive m_lock.
362 + // N.B. The runtime lock must be acquired before acquiring m_containersLock or m_networksLock.
363 + // These locks protect m_containers without requiring an exclusive hold on the runtime lock.
364 // This allows independent operations to proceed while container bookkeeping remains synchronized.
318 - // WSLCVolumes has its own internal srwlock and does not require m_lock.
365 + // WSLCVolumes has its own internal srwlock and does not require the runtime lock.
366 std::mutex m_containersLock;
367 std::unordered_map<std::string, std::shared_ptr<WSLCContainerImpl>> m_containers;
321 - std::optional<WSLCVolumes> m_volumes;
368 std::mutex m_networksLock;
369 std::unordered_map<std::string, NetworkEntry> m_networks;
324 - wil::unique_event m_sessionTerminatingEvent{wil::EventOptions::ManualReset};
325 - wil::unique_event m_sessionTerminatedEvent{wil::EventOptions::ManualReset};
326 - wil::unique_event m_vmExitedEvent;
370 + wil::shared_event m_sessionTerminatingEvent{wil::EventOptions::ManualReset};
371 + wil::shared_event m_sessionTerminatedEvent{wil::EventOptions::ManualReset};
372
373 WSLCVirtualMachineTerminationReason m_terminationReason{WSLCVirtualMachineTerminationReasonUnknown};
374 std::wstring m_terminationDetails;
330 - wil::srwlock m_lock;
331 - IORelay m_ioRelay;
332 - std::optional<ServiceRunningProcess> m_containerdProcess;
333 - std::optional<ServiceRunningProcess> m_dockerdProcess;
375 +
376 + // Persisted settings required to (re)create the VM on demand. The string fields point
377 + // into the owned storage members below (or m_displayName) so they remain valid for the
378 + // lifetime of the session.
379 + WSLCSessionInitSettings m_settings{};
380 + std::optional<std::wstring> m_settingsCreatorProcessName;
381 + std::optional<std::wstring> m_settingsStoragePath;
382 + std::optional<std::string> m_settingsRootVhdTypeOverride;
383 + std::vector<BYTE> m_userSid;
384 +
385 WSLCFeatureFlags m_featureFlags{};
386 std::function<void()> m_destructionCallback;
387 std::atomic<bool> m_terminating{false};
@@ -349,14 +400,12 @@ private:
400 // survive insertions and unrelated erasures, so each CrashDumpSubscription stashes its own
401 // iterator and uses it as an O(1) removal handle when the last reference is released.
402 // The session's lifetime extends past Terminate() (the COM object outlives the VM), so this
352 - // list may outlive m_virtualMachine; that's fine because dispatch only runs while the VM
403 + // list may outlive the runtime VM instance; that's fine because dispatch only runs while the VM
404 // thread is alive.
405 mutable wil::srwlock m_crashDumpLock;
406 _Guarded_by_(m_crashDumpLock) CrashDumpCallbackList m_crashDumpCallbacks;
407
357 - // Used for testing only.
358 - std::mutex m_allocatedPortsLock;
359 - __guarded_by(m_allocatedPortsLock) std::map<uint16_t, std::pair<std::shared_ptr<VmPortAllocation>, size_t>> m_allocatedPorts;
408 + friend class WSLCSessionRuntime;
409 };
410
411 } // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCSessionRuntime.cpp new
+977
@@ -0,0 +1,977 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCSessionRuntime.cpp
8 +
9 +Abstract:
10 +
11 + Contains the implementation for WSLCSessionRuntime.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "WSLCSessionRuntime.h"
17 +#include "WSLCSession.h"
18 +#include "WSLCSessionDefaults.h"
19 +
20 +using wsl::windows::service::wslc::WSLCSessionRuntime;
21 +
22 +namespace {
23 +
24 +constexpr auto c_containerdStorage = wsl::windows::wslc::ContainerdStorageMountPoint;
25 +constexpr auto c_dockerdReadyLogLine = "API listen on /var/run/docker.sock";
26 +constexpr DWORD c_processTerminateTimeoutMs = 30 * 1000;
27 +constexpr DWORD c_processKillTimeoutMs = 10 * 1000;
28 +
29 +// How long a VM lease waits for an announced stop to complete before logging that it is still
30 +// blocked. Purely diagnostic: the wait itself is unbounded (see VmLease).
31 +constexpr DWORD c_vmStopWaitLogIntervalMs = 30 * 1000;
32 +
33 +} // namespace
34 +
35 +namespace wsl::windows::service::wslc {
36 +
37 +WSLCSessionRuntime::WSLCSessionRuntime(WSLCSession& Session) noexcept : m_session(&Session)
38 +{
39 +}
40 +
41 +void WSLCSessionRuntime::Initialize(
42 + DWORD vmFactoryGitCookie,
43 + wil::com_ptr<IGlobalInterfaceTable> git,
44 + const WSLCSessionInitSettings* settings,
45 + std::chrono::milliseconds idleGrace,
46 + SessionContext sessionContext,
47 + RuntimeHooks hooks)
48 +{
49 + m_vmFactoryGitCookie = vmFactoryGitCookie;
50 + m_git = std::move(git);
51 + m_settings = settings;
52 + m_id = sessionContext.Id;
53 + m_displayName = std::move(sessionContext.DisplayName);
54 + m_terminating = sessionContext.Terminating;
55 + m_sessionTerminatingEvent = sessionContext.SessionTerminatingEvent;
56 + m_sessionTerminatedEvent = sessionContext.SessionTerminatedEvent;
57 + m_hooks = std::move(hooks);
58 +
59 + m_idleState->Initialize(idleGrace, [this]() { OnIdleTimer(); });
60 +
61 + // Session-scoped: subscriptions must survive VM restarts. Rebound per-VM in InitializeDockerRuntime.
62 + m_eventTracker.emplace(*m_session);
63 +
64 + m_initialized = true;
65 +}
66 +
67 +WSLCVirtualMachine& WSLCSessionRuntime::Vm()
68 +{
69 + WI_ASSERT(m_virtualMachine.has_value());
70 + return m_virtualMachine.value();
71 +}
72 +
73 +bool WSLCSessionRuntime::HasVm() const noexcept
74 +{
75 + return m_hasVm.load();
76 +}
77 +
78 +IORelay* WSLCSessionRuntime::Relay()
79 +{
80 + return m_ioRelay ? &m_ioRelay.value() : nullptr;
81 +}
82 +
83 +bool WSLCSessionRuntime::HasRelay() const noexcept
84 +{
85 + return m_ioRelay.has_value();
86 +}
87 +
88 +DockerHTTPClient& WSLCSessionRuntime::Docker()
89 +{
90 + WI_ASSERT(m_dockerClient.has_value());
91 + return m_dockerClient.value();
92 +}
93 +
94 +bool WSLCSessionRuntime::HasDocker() const noexcept
95 +{
96 + return m_dockerClient.has_value();
97 +}
98 +
99 +DockerEventTracker& WSLCSessionRuntime::Events()
100 +{
101 + WI_ASSERT(m_eventTracker.has_value());
102 + return m_eventTracker.value();
103 +}
104 +
105 +bool WSLCSessionRuntime::HasEvents() const noexcept
106 +{
107 + return m_eventTracker.has_value();
108 +}
109 +
110 +WSLCVolumes& WSLCSessionRuntime::Volumes()
111 +{
112 + WI_ASSERT(m_volumes.has_value());
113 + return m_volumes.value();
114 +}
115 +
116 +bool WSLCSessionRuntime::HasVolumes() const noexcept
117 +{
118 + return m_volumes.has_value();
119 +}
120 +
121 +wil::rwlock_release_exclusive_scope_exit WSLCSessionRuntime::TryLockExclusive() noexcept
122 +{
123 + return m_lock.try_lock_exclusive();
124 +}
125 +
126 +IdleState& WSLCSessionRuntime::Idle() noexcept
127 +{
128 + return *m_idleState;
129 +}
130 +
131 +std::shared_ptr<IdleState> WSLCSessionRuntime::IdleStateShared() const noexcept
132 +{
133 + return m_idleState;
134 +}
135 +
136 +WSLCSessionRuntime::VmState WSLCSessionRuntime::State() const noexcept
137 +{
138 + return m_vmState.load();
139 +}
140 +
141 +WSLCSessionRuntime::VmExitDisposition WSLCSessionRuntime::ExitDisposition() const noexcept
142 +{
143 + return m_vmExitDisposition.load();
144 +}
145 +
146 +bool WSLCSessionRuntime::VmExited() const noexcept
147 +{
148 + return m_vmExited.load();
149 +}
150 +
151 +void WSLCSessionRuntime::ResetDockerdReady() noexcept
152 +{
153 + m_dockerdReadyEvent.ResetEvent();
154 +}
155 +
156 +void WSLCSessionRuntime::OnProcessLog(const gsl::span<char>& buffer, PCSTR source) noexcept
157 +try
158 +{
159 + if (buffer.empty())
160 + {
161 + return;
162 + }
163 +
164 + std::string entry{buffer.begin(), buffer.end()};
165 + WSL_LOG(
166 + "ContainerdLog",
167 + TraceLoggingValue(source, "Source"),
168 + TraceLoggingValue(entry.c_str(), "Content"),
169 + TraceLoggingValue(m_displayName.c_str(), "Name"));
170 +
171 + if (!m_dockerdReadyEvent.is_signaled() && entry.find(c_dockerdReadyLogLine) != std::string::npos)
172 + {
173 + m_dockerdReadyEvent.SetEvent();
174 + }
175 +}
176 +CATCH_LOG()
177 +
178 +void WSLCSessionRuntime::SetContainerdProcess(ServiceRunningProcess&& process)
179 +{
180 + m_containerdProcess = std::move(process);
181 +}
182 +
183 +void WSLCSessionRuntime::SetDockerdProcess(ServiceRunningProcess&& process)
184 +{
185 + m_dockerdProcess = std::move(process);
186 +}
187 +
188 +void WSLCSessionRuntime::SetSwapVhdPath(std::filesystem::path path)
189 +{
190 + m_swapVhdPath = std::move(path);
191 +}
192 +
193 +void WSLCSessionRuntime::SetStorageMounted(bool value) noexcept
194 +{
195 + m_storageMounted = value;
196 +}
197 +
198 +std::mutex& WSLCSessionRuntime::AllocatedPortsLock() noexcept
199 +{
200 + return m_allocatedPortsLock;
201 +}
202 +
203 +std::map<uint16_t, std::pair<std::shared_ptr<VmPortAllocation>, size_t>>& WSLCSessionRuntime::AllocatedPorts() noexcept
204 +{
205 + return m_allocatedPorts;
206 +}
207 +
208 +bool WSLCSessionRuntime::IdleTerminationEnabled() const noexcept
209 +{
210 + // Only tear the VM down when there is persistent storage to recover from. A tmpfs-backed
211 + // session would lose all image/container state on teardown, so its VM is kept alive once started.
212 + return m_settings->StoragePath != nullptr;
213 +}
214 +
215 +int WSLCSessionRuntime::StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs)
216 +{
217 + auto signalResult = Process.Get().Signal(WSLCSignalSIGTERM);
218 + if (FAILED(signalResult))
219 + {
220 + LOG_HR_MSG(signalResult, "Failed to terminate process %i", Process.Get().GetPid());
221 + return -1;
222 + }
223 +
224 + try
225 + {
226 + return Process.Wait(TerminateTimeoutMs);
227 + }
228 + catch (...)
229 + {
230 + LOG_CAUGHT_EXCEPTION();
231 + try
232 + {
233 + LOG_IF_FAILED(Process.Get().Signal(WSLCSignalSIGKILL));
234 + return Process.Wait(KillTimeoutMs);
235 + }
236 + CATCH_LOG();
237 + }
238 +
239 + return -1;
240 +}
241 +
242 +void WSLCSessionRuntime::EnsureVmRunning()
243 +{
244 + // Reject leases once the session is terminating/terminated, including on the running fast path:
245 + // Shutdown() drops the lock to fire OnVmStopping, and a reentrant lease that finds the VM still
246 + // Running must fail here rather than run work against a VM being permanently torn down.
247 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating->load() || m_sessionTerminatedEvent.is_signaled());
248 +
249 + if (m_vmState.load() == VmState::Running)
250 + {
251 + return;
252 + }
253 +
254 + bool started = false;
255 + uint64_t generation = 0;
256 + {
257 + auto lock = m_lock.lock_exclusive();
258 +
259 + // Re-check under the lock: terminating may have been set since the check above. This also
260 + // bounds VmLease's retry loop: a lease that races with Terminate() fails here instead of
261 + // restarting a VM that is being permanently torn down.
262 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_terminating->load() || m_sessionTerminatedEvent.is_signaled());
263 +
264 + if (m_vmState.load() != VmState::Running)
265 + {
266 + StartVmLockHeld();
267 + started = true;
268 + }
269 +
270 + generation = m_vmGeneration.load();
271 + }
272 +
273 + // Notify plugins that a VM has started, outside the exclusive lock: the handler forwards to the
274 + // plugin, which may call back into the session (e.g. WSLCCreateProcess acquires a VM lease and
275 + // the exclusive lock), so firing under the lock would deadlock. EnsureVmRunning's only caller
276 + // (VmLease) holds an activity reference across this call, so idle teardown cannot race the VM
277 + // down in the gap between releasing the lock and notifying.
278 + if (started)
279 + {
280 + NotifyVmStarted(generation);
281 + }
282 +}
283 +
284 +void WSLCSessionRuntime::NotifyVmStarted(uint64_t Generation)
285 +{
286 + // Hold m_notifyLock across both the pairing-state flip and the hook invocation, so a concurrent
287 + // NotifyVmStopping on another thread cannot deliver its OnVmStopping in between and observe this
288 + // OnVmStarted arrive late (which would otherwise leave the plugin with a trailing "started" for a
289 + // VM that already stopped). m_notifyLock is recursive: the handler may reentrantly restart the VM
290 + // (e.g. WSLCCreateProcess -> NotifyVmStarted/Stopping) on this same thread, which would self-deadlock
291 + // under a plain mutex. Suppressed once terminating, else a racing Shutdown leaves OnVmStarted unpaired.
292 + // m_notifiedGeneration tracks the VM lifecycle, not whether OnVmStarted is installed, so a hooks
293 + // user that sets only OnVmStopping still gets paired stop notifications.
294 + auto lock = std::lock_guard(m_notifyLock);
295 +
296 + // Drop the notification if this instance is already gone: between releasing the runtime lock and
297 + // getting here it may have been torn down and replaced, and announcing it now would misattribute
298 + // the start to whichever instance is running.
299 + if (m_terminating->load() || m_vmGeneration.load() != Generation)
300 + {
301 + return;
302 + }
303 +
304 + m_notifiedGeneration.store(Generation);
305 + if (m_hooks.OnVmStarted)
306 + {
307 + m_hooks.OnVmStarted();
308 + }
309 +}
310 +
311 +void WSLCSessionRuntime::NotifyVmStopping(uint64_t Generation)
312 +{
313 + // See NotifyVmStarted: hold m_notifyLock across both the pairing check and the hook invocation, so
314 + // the two notifications cannot interleave across threads. Recursive because the handler may
315 + // reentrantly restart the VM and call NotifyVmStarted on this same thread.
316 + //
317 + // N.B. The CAS below retires the generation as the stop is announced, so a second stop for the
318 + // same VM (e.g. Terminate() racing an idle teardown that has dropped the lock across its
319 + // notification) cannot deliver OnVmStopping twice. This is safe because the stop is committed
320 + // before it is announced: the VM cannot come back, so nothing needs the generation afterwards.
321 + //
322 + // Generation 0 is the sentinel for "no VM has been announced": both counters start there, so a
323 + // session that is torn down without ever starting a VM must not match, or Shutdown would deliver
324 + // an OnVmStopping that no OnVmStarted ever paired with.
325 + auto lock = std::lock_guard(m_notifyLock);
326 +
327 + auto expected = Generation;
328 + if (Generation != 0 && m_notifiedGeneration.compare_exchange_strong(expected, 0) && m_hooks.OnVmStopping)
329 + {
330 + m_hooks.OnVmStopping();
331 + }
332 +}
333 +
334 +void WSLCSessionRuntime::BeginVmStopLockHeld() noexcept
335 +{
336 + // Reset before publishing the flag, so a lease cannot observe the pending stop while the event is
337 + // still signaled from the previous teardown.
338 + m_vmStopCompleteEvent.ResetEvent();
339 + m_vmStopPending.store(true);
340 +}
341 +
342 +void WSLCSessionRuntime::EndVmStop() noexcept
343 +{
344 + m_vmStopPending.store(false);
345 + m_vmStopCompleteEvent.SetEvent();
346 +}
347 +
348 +bool WSLCSessionRuntime::TryClaimExpectedStop() noexcept
349 +{
350 + auto expected = VmExitDisposition::Active;
351 + return m_vmExitDisposition.compare_exchange_strong(expected, VmExitDisposition::StopRequested);
352 +}
353 +
354 +bool WSLCSessionRuntime::TryClaimSpontaneousExit() noexcept
355 +{
356 + auto expected = VmExitDisposition::Active;
357 + return m_vmExitDisposition.compare_exchange_strong(expected, VmExitDisposition::ExitClaimed);
358 +}
359 +
360 +void WSLCSessionRuntime::StartVmLockHeld()
361 +{
362 + WI_ASSERT(m_vmState.load() != VmState::Running);
363 +
364 + WSL_LOG("WslcVmStarting", TraceLoggingValue(m_id, "SessionId"));
365 +
366 + m_vmState.store(VmState::Starting);
367 + m_vmExitDisposition.store(VmExitDisposition::Active);
368 +
369 + // Identify this instance. Bumped under the runtime lock so a notification that had to drop the
370 + // lock to fire can tell whether it still describes the VM it was raised for. The first VM is
371 + // generation 1, so the 0 stored by TearDownVmLockHeld never collides with a live instance.
372 + m_vmGeneration.fetch_add(1);
373 +
374 + // Tear back down if bring-up fails partway. The VM may have exited on its own during bring-up,
375 + // so claim the stop first and only tear down if we win it (TryClaimExpectedStop()); otherwise
376 + // OnVmExited() owns the teardown and we just release the lock to let its Terminate() finish.
377 + auto startCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
378 + if (TryClaimExpectedStop())
379 + {
380 + TearDownVmLockHeld();
381 + m_vmState.store(VmState::None);
382 + }
383 + else
384 + {
385 + WSL_LOG("WslcVmExitedDuringStart", TraceLoggingValue(m_id, "SessionId"));
386 + }
387 + });
388 +
389 + // Create a fresh IO relay for this VM instance. The previous one (if any) was stopped
390 + // during teardown and cannot be restarted.
391 + m_ioRelay.emplace();
392 +
393 + // Create the VM via the factory. Re-fetch the factory from the GIT so we call it through a
394 + // proxy marshalled into this thread's apartment (see m_git). The VM produces crash events;
395 + // the session multiplexes them out to any registered ICrashDumpCallback subscribers via
396 + // OnCrashDumpWritten.
397 + wil::com_ptr<IWSLCVirtualMachineFactory> vmFactory;
398 + THROW_IF_FAILED(m_git->GetInterfaceFromGlobal(m_vmFactoryGitCookie, __uuidof(IWSLCVirtualMachineFactory), vmFactory.put_void()));
399 +
400 + wil::com_ptr<IWSLCVirtualMachine> vm;
401 + THROW_IF_FAILED(vmFactory->CreateVirtualMachine(&vm));
402 +
403 + m_virtualMachine.emplace(vm.get(), m_settings, m_sessionTerminatingEvent.get(), WSLCVirtualMachine::TOnCrashDump(m_hooks.OnCrashDump));
404 +
405 + // Publish only once the object is constructed. If Initialize() below throws, startCleanup tears
406 + // the VM down and clears this again.
407 + m_hasVm.store(true);
408 +
409 + m_virtualMachine->Initialize();
410 +
411 + // Get an event from the service that is signaled when the VM exits.
412 + m_vmExitedEvent.reset();
413 + THROW_IF_FAILED(vm->GetTerminationEvent(&m_vmExitedEvent));
414 + m_vmExited.store(false);
415 +
416 + if (m_hooks.BringUp)
417 + {
418 + m_hooks.BringUp();
419 + }
420 +
421 + // Monitor for unexpected VM exit.
422 + m_ioRelay->AddHandle(
423 + std::make_unique<windows::common::io::EventHandle>(m_vmExitedEvent.get(), std::bind(&WSLCSessionRuntime::OnVmExited, this)));
424 +
425 + if (m_hooks.RecoverState)
426 + {
427 + m_hooks.RecoverState();
428 + }
429 +
430 + m_vmState.store(VmState::Running);
431 + startCleanup.release();
432 +
433 + WSL_LOG("WslcVmStarted", TraceLoggingValue(m_id, "SessionId"));
434 +}
435 +
436 +void WSLCSessionRuntime::InitializeDockerRuntime(const std::filesystem::path& storagePath)
437 +{
438 + // Wait for dockerd to be ready before starting the event tracker.
439 + THROW_WIN32_IF_MSG(
440 + ERROR_TIMEOUT, !m_dockerdReadyEvent.wait(m_settings->BootTimeoutMs), "Timed out waiting for dockerd to start");
441 +
442 + [[maybe_unused]] auto [pid, ptyMaster, channel] = m_virtualMachine->Fork(WSLC_FORK::Thread);
443 +
444 + m_dockerClient.emplace(std::move(channel), m_virtualMachine->TerminatingEvent(), m_virtualMachine->VmId(), 10 * 1000);
445 +
446 + // (Re)bind the session-scoped event tracker to this VM's docker client and relay. Existing
447 + // container subscriptions are preserved across restarts.
448 + m_eventTracker->Connect(m_dockerClient.value(), *m_ioRelay);
449 +
450 + m_volumes.emplace(m_dockerClient.value(), m_virtualMachine.value(), m_eventTracker.value(), storagePath);
451 +}
452 +
453 +void WSLCSessionRuntime::StopVmLockHeld()
454 +{
455 + if (m_vmState.load() != VmState::Running)
456 + {
457 + return;
458 + }
459 +
460 + WSL_LOG("WslcVmIdleStop", TraceLoggingValue(m_id, "SessionId"));
461 +
462 + // N.B. The caller has claimed StopRequested (via TryClaimExpectedStop), so VM/dockerd/containerd
463 + // exit callbacks firing from the relay thread during teardown are treated as expected, not as a
464 + // crash.
465 + m_vmState.store(VmState::Stopping);
466 +
467 + TearDownVmLockHeld();
468 +
469 + m_vmState.store(VmState::None);
470 +}
471 +
472 +void WSLCSessionRuntime::TearDownVmLockHeld(bool CaptureTerminationReason)
473 +{
474 + // The VM is committed to going away from here on, so the plugin's view of it ends here. Retiring
475 + // the notified instance up front, before any step below can throw, also covers the case where the
476 + // VM is torn down without an OnVmStopping ever being announced (e.g. bring-up failed). Written
477 + // under the runtime lock; NotifyVmStarted/NotifyVmStopping read it under m_notifyLock without the
478 + // runtime lock, so this store can land while one of them is inside a plugin handler. That is
479 + // benign: both compare against a generation they captured earlier, so a store of 0 can only make
480 + // them decline to notify, and by this point either the stop has already been announced (its CAS
481 + // ran first) or the instance is gone and must not be announced at all.
482 + m_notifiedGeneration.store(0);
483 +
484 + // Latch whether the guest is already dead before running session-state cleanup so container
485 + // teardown (ReleaseRuntimeResources) can skip VM-dependent calls, e.g. volume unmounts, on a VM
486 + // that has exited. A graceful stop reaches here with the VM still alive (is_signaled() false), so
487 + // its mounts are unmounted through the live VM; StartVmLockHeld clears this for the next instance.
488 + if (m_vmExitedEvent && m_vmExitedEvent.is_signaled())
489 + {
490 + m_vmExited.store(true);
491 + }
492 +
493 + if (m_hooks.TearDownSessionState)
494 + {
495 + m_hooks.TearDownSessionState(CaptureTerminationReason);
496 + }
497 +
498 + m_volumes.reset();
499 +
500 + // Stop the IO relay.
501 + // This stops:
502 + // - container state monitoring.
503 + // - container init process relays
504 + // - execs relays
505 + // - container logs relays
506 + if (m_ioRelay)
507 + {
508 + m_ioRelay->Stop();
509 + }
510 +
511 + {
512 + std::lock_guard allocatedPortsLock(m_allocatedPortsLock);
513 + m_allocatedPorts.clear();
514 + }
515 +
516 + // The session-scoped event tracker is intentionally not reset. Its stream handle dies with the IO
517 + // relay above, and InitializeDockerRuntime re-binds it on the next start.
518 + m_dockerClient.reset();
519 +
520 + if (CaptureTerminationReason)
521 + {
522 + // Default: an explicit/graceful teardown is a shutdown (the VM is still alive and we are
523 + // bringing it down). Overridden below if the VM exited on its own and recorded a cause.
524 + m_lastTerminationReason = WSLCVirtualMachineTerminationReasonShutdown;
525 + m_lastTerminationDetails.clear();
526 + }
527 +
528 + // Check if the VM has already exited (e.g., killed externally).
529 + // If so, skip operations that require a live VM to avoid unnecessary waits.
530 + // N.B. m_vmExitedEvent may be uninitialized if teardown runs before GetTerminationEvent() succeeds.
531 + if (m_vmExitedEvent && m_vmExitedEvent.is_signaled())
532 + {
533 + WSL_LOG("SkippingGracefulShutdown_VmDead", TraceLoggingValue(m_id, "SessionId"));
534 +
535 + // The VM exited on its own, so it recorded the cause.
536 + if (CaptureTerminationReason && m_virtualMachine)
537 + {
538 + wil::unique_cotaskmem_string details;
539 + LOG_IF_FAILED(m_virtualMachine->GetTerminationReason(&m_lastTerminationReason, &details));
540 + m_lastTerminationDetails = details ? details.get() : L"";
541 + }
542 + }
543 + else if (m_virtualMachine)
544 + {
545 + m_virtualMachine->OnSessionTerminated();
546 +
547 + // Stop dockerd first, then containerd (dockerd is a client of containerd).
548 + // N.B. dockerd waits a couple seconds if there are any outstanding HTTP request sockets opened.
549 + if (m_dockerdProcess.has_value())
550 + {
551 + auto dockerdExitCode = StopProcess(m_dockerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs);
552 + WSL_LOG("DockerdExit", TraceLoggingValue(dockerdExitCode, "code"));
553 + }
554 +
555 + if (m_containerdProcess.has_value())
556 + {
557 + auto containerdExitCode = StopProcess(m_containerdProcess.value(), c_processTerminateTimeoutMs, c_processKillTimeoutMs);
558 + WSL_LOG("ContainerdExit", TraceLoggingValue(containerdExitCode, "code"));
559 + }
560 +
561 + // N.B. dockerd has exited by this point, so unmounting the VHD is safe since no container can be running.
562 + if (m_storageMounted)
563 + {
564 + try
565 + {
566 + m_virtualMachine->Unmount(c_containerdStorage);
567 + }
568 + CATCH_LOG();
569 + }
570 + }
571 +
572 + m_dockerdProcess.reset();
573 + m_containerdProcess.reset();
574 +
575 + // Retire the lock-free mirror before destroying the object so an unlocked reader never sees
576 + // "a VM is present" while it is being torn down.
577 + m_hasVm.store(false);
578 + m_virtualMachine.reset();
579 + m_storageMounted = false;
580 +
581 + // Destroy the relay unless we're on its own thread (~IORelay joins the thread, which would
582 + // deadlock). On unexpected-VM-exit path (runs on relay thread), leave it for ~WSLCSession.
583 + if (!m_ioRelay || !m_ioRelay->IsRelayThread())
584 + {
585 + m_ioRelay.reset();
586 + m_vmExitedEvent.reset();
587 + }
588 +
589 + // Delete the ephemeral swap VHD now that the VM is gone.
590 + if (!m_swapVhdPath.empty())
591 + {
592 + LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(m_swapVhdPath.c_str()));
593 + m_swapVhdPath.clear();
594 + }
595 +}
596 +
597 +void WSLCSessionRuntime::OnIdleTimer()
598 +try
599 +{
600 + // Idle teardown releases cross-process COM proxies (the VM and its VM-scoped state), so this
601 + // threadpool callback must join the process MTA; otherwise those Release/calls fail with
602 + // RPC_E_WRONG_THREAD. The function-try-block keeps this (and everything below) under CATCH_LOG:
603 + // the threadpool callback that invokes us is noexcept, so an escaping throw would terminate.
604 + const auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED);
605 +
606 + if (m_terminating->load() || !IdleTerminationEnabled())
607 + {
608 + return;
609 + }
610 +
611 + // Non-blocking acquire: a blocking exclusive would queue behind in-flight operations, and
612 + // SRW locks favor waiting writers, stalling all new ops. If the lock is held, an operation
613 + // is in flight; it holds an activity reference and will re-arm the timer (via the 1->0
614 + // transition) when it releases, so there is nothing to do here.
615 + auto lock = m_lock.try_lock_exclusive();
616 + if (!lock)
617 + {
618 + return;
619 + }
620 +
621 + // Re-check every teardown precondition under the lock. The activity count is the single
622 + // source of truth for "the VM is needed"; a 0->1 transition since the timer fired (cancel
623 + // raced the callback) is caught here.
624 + if (m_terminating->load() || m_vmState.load() != VmState::Running || m_idleState->ActivityCount() != 0)
625 + {
626 + return;
627 + }
628 +
629 + // Claim the stop. If we lose, OnVmExited() owns a spontaneous-exit teardown and is spinning for
630 + // this lock, so release it and let that run instead of joining the relay ourselves.
631 + if (!TryClaimExpectedStop())
632 + {
633 + return;
634 + }
635 +
636 + // Restore Active on completion (or early exit) so the next StartVmLockHeld starts clean; only
637 + // clear our own claim.
638 + auto dispositionCleanup = wil::scope_exit([this]() {
639 + auto stopRequested = VmExitDisposition::StopRequested;
640 + m_vmExitDisposition.compare_exchange_strong(stopRequested, VmExitDisposition::Active);
641 + });
642 +
643 + // Final activity re-check under the lock, and the last point at which the stop can still be called
644 + // off: a VmLease that bumped the count since the check above is now blocked on the shared lock we
645 + // hold. Back out here -- nothing has been announced yet -- rather than stopping a VM that is about
646 + // to be used again.
647 + if (m_idleState->ActivityCount() != 0)
648 + {
649 + return;
650 + }
651 +
652 + // Past this point the VM is going away, whatever happens. Publishing the pending stop under the
653 + // lock is what makes the announcement below true: an ordinary lease that races in while the lock
654 + // is dropped no longer gets this VM, it waits for the teardown and is served by the next one. The
655 + // exception is a lease taken by the OnVmStopping handler itself, which must be served or it would
656 + // deadlock against the very callback this teardown is waiting on. Work that handler leaves running
657 + // dies with the VM -- which is exactly what it was just told would happen.
658 + //
659 + // N.B. endStop is declared after 'lock' so it runs *before* the lock is released (reverse
660 + // declaration order): waiters are released only once the teardown is complete and published.
661 + BeginVmStopLockHeld();
662 + auto endStop = wil::scope_exit([this]() { EndVmStop(); });
663 +
664 + // Fire OnVmStopping with m_lock dropped so a plugin handler may take a VM lease without
665 + // deadlocking, and while the VM is still running so the handler can still use it.
666 + //
667 + // The notification cannot be allowed to abort the teardown: the stop is already published and its
668 + // generation retired, so bailing out here would leave waiters to be served by the VM they were
669 + // promised would die, and its eventual teardown would be silent.
670 + const auto generation = m_vmGeneration.load();
671 +
672 + lock.reset();
673 + try
674 + {
675 + NotifyVmStopping(generation);
676 + }
677 + CATCH_LOG();
678 + lock = m_lock.lock_exclusive();
679 +
680 + // Unconditional: the stop was announced, so it happens. Reacquiring the exclusive lock first
681 + // drains every in-flight operation, so nothing is cut off mid-call. StopVmLockHeld no-ops if a
682 + // concurrent Terminate already tore the VM down, and copes with a VM that crashed while the lock
683 + // was dropped -- OnVmExited() declined that exit (we hold the expected-stop claim) and its exit
684 + // handle is one-shot, so this is the only teardown left to run.
685 + StopVmLockHeld();
686 +}
687 +CATCH_LOG();
688 +
689 +bool WSLCSessionRuntime::TriggerIdleTerminationForTest()
690 +{
691 + // Mirror OnIdleTimer's MTA context on a dedicated thread: the incoming RPC thread is an STA, so
692 + // both re-initializing MTA on it and running teardown inline would fail (RPC_E_CHANGED_MODE /
693 + // RPC_E_WRONG_THREAD when releasing the VM's cross-process COM proxies).
694 + bool wasAlreadyIdle = false;
695 + std::exception_ptr error;
696 +
697 + std::thread worker([&]() {
698 + try
699 + {
700 + const auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED);
701 +
702 + auto lock = m_lock.lock_exclusive();
703 +
704 + if (m_terminating->load() || m_vmState.load() != VmState::Running)
705 + {
706 + wasAlreadyIdle = true;
707 + return;
708 + }
709 +
710 + // tmpfs sessions have no persistent storage to recover from, so tearing a running VM down
711 + // loses all state. Check this after the VM state so a never-started session still reports
712 + // that it was already idle.
713 + if (!IdleTerminationEnabled())
714 + {
715 + return;
716 + }
717 +
718 + // Match the production idle timer: an active container or operation keeps the VM alive.
719 + if (m_idleState->ActivityCount() != 0)
720 + {
721 + return;
722 + }
723 +
724 + if (!TryClaimExpectedStop())
725 + {
726 + wasAlreadyIdle = true;
727 + return;
728 + }
729 +
730 + auto dispositionCleanup = wil::scope_exit([this]() {
731 + auto stopRequested = VmExitDisposition::StopRequested;
732 + m_vmExitDisposition.compare_exchange_strong(stopRequested, VmExitDisposition::Active);
733 + });
734 +
735 + // Mirror OnIdleTimer's final re-check under the lock: a lease taken while the stop claim
736 + // was being made is now blocked on the lock we hold, and must back the teardown out here
737 + // rather than announce a stop production code would have suppressed.
738 + if (m_idleState->ActivityCount() != 0)
739 + {
740 + return;
741 + }
742 +
743 + // Commit to the stop before announcing it, then fire OnVmStopping without holding m_lock
744 + // and tear down unconditionally. See OnIdleTimer for the full rationale.
745 + BeginVmStopLockHeld();
746 + auto endStop = wil::scope_exit([this]() { EndVmStop(); });
747 +
748 + const auto generation = m_vmGeneration.load();
749 +
750 + lock.reset();
751 + try
752 + {
753 + NotifyVmStopping(generation);
754 + }
755 + CATCH_LOG();
756 + lock = m_lock.lock_exclusive();
757 +
758 + StopVmLockHeld();
759 + }
760 + catch (...)
761 + {
762 + error = std::current_exception();
763 + }
764 + });
765 +
766 + worker.join();
767 +
768 + if (error)
769 + {
770 + std::rethrow_exception(error);
771 + }
772 +
773 + return wasAlreadyIdle;
774 +}
775 +
776 +WSLCSessionRuntime::VmLease WSLCSessionRuntime::AcquireVmLease(VmLeasePolicy Policy)
777 +{
778 + return VmLease(*this, Policy);
779 +}
780 +
781 +WSLCSessionRuntime::LockedRuntime WSLCSessionRuntime::Acquire(VmLeasePolicy Policy)
782 +{
783 + return LockedRuntime(*this, Policy);
784 +}
785 +
786 +WSLCSessionRuntime::VmLease::VmLease(WSLCSessionRuntime& Runtime, VmLeasePolicy Policy) : m_runtime(&Runtime)
787 +{
788 + // Record an in-flight operation before bringing the VM up so idle teardown cannot tear it down
789 + // between EnsureVmRunning() and acquiring the shared lock. AddActivity cancels any pending idle
790 + // timer.
791 + m_runtime->m_idleState->AddActivity();
792 +
793 + auto countCleanup = wil::scope_exit([this]() {
794 + m_runtime->m_idleState->ReleaseActivity();
795 + m_runtime = nullptr;
796 + });
797 +
798 + // Activity increment may race with idle teardown. Retry until we hold the lock with VM running.
799 + for (;;)
800 + {
801 + if (Policy == VmLeasePolicy::Acquire)
802 + {
803 + m_runtime->EnsureVmRunning();
804 + }
805 +
806 + m_lock = m_runtime->m_lock.lock_shared();
807 +
808 + if (m_runtime->m_vmState.load() == VmState::Running)
809 + {
810 + // An announced stop always happens, so a VM with one pending is unusable even though it is
811 + // still Running: wait for the teardown, then retry, which brings up a fresh VM. ExistingOnly
812 + // callers are exempt and are served by the stopping VM -- see VmLeasePolicy.
813 + if (Policy == VmLeasePolicy::ExistingOnly || !m_runtime->m_vmStopPending.load())
814 + {
815 + break;
816 + }
817 +
818 + // Release the shared lock before waiting. The teardown must be able to take the exclusive
819 + // lock, and it is the only thing that will ever signal us.
820 + m_lock.reset();
821 +
822 + // The wait is bounded only so that a handler wedging the teardown is visible in traces;
823 + // there is no correct way to proceed without a VM, so the retry is unconditional.
824 + while (!m_runtime->m_vmStopCompleteEvent.wait(c_vmStopWaitLogIntervalMs))
825 + {
826 + WSL_LOG(
827 + "WslcVmLeaseWaitingForStop",
828 + TraceLoggingLevel(WINEVENT_LEVEL_WARNING),
829 + TraceLoggingValue(m_runtime->m_id, "SessionId"));
830 + }
831 +
832 + continue;
833 + }
834 +
835 + // ExistingOnly never starts a VM, so retrying could only spin. Reject the caller instead.
836 + THROW_HR_IF(WSLC_E_VM_NOT_RUNNING, Policy == VmLeasePolicy::ExistingOnly);
837 +
838 + WSL_LOG(
839 + "WslcVmLeaseRetry",
840 + TraceLoggingValue(m_runtime->m_id, "SessionId"),
841 + TraceLoggingValue(static_cast<uint32_t>(m_runtime->m_vmState.load()), "VmState"));
842 + m_lock.reset();
843 + }
844 +
845 + countCleanup.release();
846 +}
847 +
848 +WSLCSessionRuntime::VmLease::VmLease(VmLease&& Other) noexcept :
849 + m_runtime(std::exchange(Other.m_runtime, nullptr)), m_lock(std::move(Other.m_lock))
850 +{
851 +}
852 +
853 +WSLCSessionRuntime::VmLease& WSLCSessionRuntime::VmLease::operator=(VmLease&& Other) noexcept
854 +{
855 + if (this != &Other)
856 + {
857 + if (m_runtime != nullptr)
858 + {
859 + // Release the shared lock before the activity reference so that, if this was the last
860 + // activity, idle teardown can immediately take the exclusive lock.
861 + m_lock.reset();
862 + m_runtime->m_idleState->ReleaseActivity();
863 + }
864 +
865 + m_runtime = std::exchange(Other.m_runtime, nullptr);
866 + m_lock = std::move(Other.m_lock);
867 + }
868 +
869 + return *this;
870 +}
871 +
872 +WSLCSessionRuntime::VmLease::~VmLease()
873 +{
874 + if (m_runtime != nullptr)
875 + {
876 + // Release the shared lock before the activity reference so that, if this was the last
877 + // activity, idle teardown can immediately take the exclusive lock. ReleaseActivity arms the
878 + // idle timer on the 1->0 transition.
879 + m_lock.reset();
880 + m_runtime->m_idleState->ReleaseActivity();
881 + }
882 +}
883 +
884 +WSLCSessionRuntime::LockedRuntime::LockedRuntime(WSLCSessionRuntime& Runtime, VmLeasePolicy Policy) :
885 + m_runtime(&Runtime), m_lease(Runtime.AcquireVmLease(Policy))
886 +{
887 +}
888 +
889 +WSLCVirtualMachine& WSLCSessionRuntime::LockedRuntime::Vm()
890 +{
891 + return m_runtime->Vm();
892 +}
893 +
894 +IORelay* WSLCSessionRuntime::LockedRuntime::Relay()
895 +{
896 + return m_runtime->Relay();
897 +}
898 +
899 +DockerHTTPClient& WSLCSessionRuntime::LockedRuntime::Docker()
900 +{
901 + return m_runtime->Docker();
902 +}
903 +
904 +void WSLCSessionRuntime::OnVmExited()
905 +{
906 + // A spontaneous exit we must permanently terminate, unless an expected stop already claimed it,
907 + // in which case the exit was wanted and we decline.
908 + if (!TryClaimSpontaneousExit())
909 + {
910 + WSL_LOG("WslcVmExitedDuringStop", TraceLoggingValue(m_id, "SessionId"));
911 + return;
912 + }
913 +
914 + WSL_LOG(
915 + "VmExited",
916 + TraceLoggingLevel(WINEVENT_LEVEL_WARNING),
917 + TraceLoggingValue(m_id, "SessionId"),
918 + TraceLoggingValue(m_displayName.c_str(), "Name"),
919 + TraceLoggingValue(!m_sessionTerminatingEvent.is_signaled(), "Unexpected"));
920 +
921 + if (m_hooks.OnSpontaneousExit)
922 + {
923 + m_hooks.OnSpontaneousExit();
924 + }
925 +}
926 +
927 +void WSLCSessionRuntime::Shutdown(
928 + wil::rwlock_release_exclusive_scope_exit& runtimeLock, WSLCVirtualMachineTerminationReason& terminationReason, std::wstring& terminationDetails)
929 +{
930 + if (!m_initialized)
931 + {
932 + return;
933 + }
934 +
935 + // runtimeLock is an exclusive hold on m_lock, guaranteeing no operation is running.
936 + WI_VERIFY(runtimeLock);
937 +
938 + // Permanently disable idle teardown and drain any in-flight timer callback on every exit path,
939 + // including an exception escaping the teardown below. The timer callback captures this runtime,
940 + // but IdleState outlives it (activity tokens hold shared_ptr copies), so skipping the disarm
941 + // would leave a late 1->0 transition able to re-arm a timer that references freed memory.
942 + //
943 + // The exclusive lock is released first: a timer callback already blocked acquiring it must be
944 + // able to obtain it, observe m_terminating, and return, otherwise the drain below deadlocks.
945 + auto disarmIdleState = wil::scope_exit([&]() {
946 + runtimeLock.reset();
947 + m_idleState->Disarm();
948 + });
949 +
950 + // Notify with m_lock dropped, then re-lock for the teardown. The handler may call back into the
951 + // session (e.g. WSLCCreateProcess) which takes a VM lease and this lock; firing under it would
952 + // deadlock. m_terminating is set, so any such reentrant lease fails at EnsureVmRunning's gate
953 + // rather than restarting the VM, and the reacquire can't block on it. A throwing handler must not
954 + // skip the teardown below -- the session is terminating either way.
955 + runtimeLock.reset();
956 + try
957 + {
958 + NotifyVmStopping(m_vmGeneration.load());
959 + }
960 + CATCH_LOG();
961 + runtimeLock = m_lock.lock_exclusive();
962 +
963 + // Tear down the VM (if running) and all VM-scoped state, capturing the termination reason; the
964 + // reacquired exclusive hold on m_lock satisfies TearDownVmLockHeld's precondition.
965 + TearDownVmLockHeld(/* CaptureTerminationReason */ true);
966 +
967 + m_vmState.store(VmState::None);
968 +
969 + terminationReason = m_lastTerminationReason;
970 + terminationDetails = m_lastTerminationDetails;
971 +
972 + // Signal completion last so any observer of the terminated event sees a fully torn-down
973 + // session and a populated termination reason.
974 + m_sessionTerminatedEvent.SetEvent();
975 +}
976 +
977 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCSessionRuntime.h new
+305
@@ -0,0 +1,305 @@
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
src/windows/wslcsession/WSLCVirtualMachine.cpp
+30 -28
@@ -34,8 +34,8 @@ constexpr auto CONTAINER_PORT_RANGE = std::pair<uint16_t, uint16_t>(20002, 65535
34
35 static_assert(c_ephemeralPortRange.second < CONTAINER_PORT_RANGE.first);
36
37 -VmPortAllocation::VmPortAllocation(uint16_t port, int family, int protocol, WSLCVirtualMachine& vm) :
38 - m_port(port), m_family(family), m_protocol(protocol), m_vm(&vm)
37 +VmPortAllocation::VmPortAllocation(uint16_t port, int family, int protocol, std::weak_ptr<VmPortReservations> reservations) :
38 + m_port(port), m_family(family), m_protocol(protocol), m_reservations(std::move(reservations))
39 {
40 }
41
@@ -52,7 +52,7 @@ VmPortAllocation& VmPortAllocation::operator=(VmPortAllocation&& Other)
52 m_port = Other.m_port;
53 m_family = Other.m_family;
54 m_protocol = Other.m_protocol;
55 - m_vm = Other.m_vm;
55 + m_reservations = Other.m_reservations;
56
57 Other.Release();
58 }
@@ -66,16 +66,20 @@ VmPortAllocation::~VmPortAllocation()
66
67 void VmPortAllocation::Reset()
68 {
69 - if (m_vm != nullptr)
69 + // Release the reservation only if the owning VM (and its table) is still alive. If the VM was torn
70 + // down the table is already gone and lock() returns null, so a surviving allocation is a safe no-op.
71 + if (auto reservations = m_reservations.lock())
72 {
71 - m_vm->ReleasePort(*this);
72 - Release();
73 + std::lock_guard lock{reservations->Mutex};
74 + LOG_HR_IF(E_UNEXPECTED, reservations->Ports.erase(m_port) != 1);
75 }
76 +
77 + Release();
78 }
79
80 void VmPortAllocation::Release()
81 {
78 - m_vm = nullptr;
82 + m_reservations.reset();
83 m_port = 0;
84 m_family = 0;
85 m_protocol = 0;
@@ -1255,33 +1259,40 @@ void WSLCVirtualMachine::OnSessionTerminated()
1259
1260 std::shared_ptr<VmPortAllocation> WSLCVirtualMachine::TryAllocatePort(uint16_t Port, int Family, int Protocol)
1261 {
1258 - std::lock_guard lock{m_lock};
1262 + std::lock_guard lock{m_reservations->Mutex};
1263
1264 WSL_LOG("AllocatePort", TraceLoggingValue(Port, "Port"));
1265
1262 - auto [_, inserted] = m_allocatedPorts.insert(Port);
1263 -
1264 - if (inserted)
1265 - {
1266 - return std::make_shared<VmPortAllocation>(Port, Family, Protocol, *this);
1267 - }
1268 - else
1266 + if (!m_reservations->Ports.insert(Port).second)
1267 {
1268 return {};
1269 }
1270 +
1271 + // Roll the reservation back if the allocation object can't be created: nothing owns the port
1272 + // until the shared_ptr exists, so it would otherwise stay marked in use for the VM's lifetime.
1273 + auto reservationCleanup = wil::scope_exit([&]() { m_reservations->Ports.erase(Port); });
1274 + auto allocation = std::make_shared<VmPortAllocation>(Port, Family, Protocol, m_reservations);
1275 + reservationCleanup.release();
1276 +
1277 + return allocation;
1278 }
1279
1280 std::shared_ptr<VmPortAllocation> WSLCVirtualMachine::AllocatePort(int Family, int Protocol)
1281 {
1276 - std::lock_guard lock{m_lock};
1282 + std::lock_guard lock{m_reservations->Mutex};
1283
1284 for (uint32_t i = CONTAINER_PORT_RANGE.first; i <= CONTAINER_PORT_RANGE.second; i++)
1285 {
1286 uint16_t port = static_cast<uint16_t>(i);
1281 - if (!m_allocatedPorts.contains(port))
1287 + if (!m_reservations->Ports.contains(port))
1288 {
1283 - WI_VERIFY(m_allocatedPorts.insert(port).second);
1284 - return std::make_shared<VmPortAllocation>(port, Family, Protocol, *this);
1289 + WI_VERIFY(m_reservations->Ports.insert(port).second);
1290 +
1291 + auto reservationCleanup = wil::scope_exit([&]() { m_reservations->Ports.erase(port); });
1292 + auto allocation = std::make_shared<VmPortAllocation>(port, Family, Protocol, m_reservations);
1293 + reservationCleanup.release();
1294 +
1295 + return allocation;
1296 }
1297 }
1298
@@ -1289,15 +1300,6 @@ std::shared_ptr<VmPortAllocation> WSLCVirtualMachine::AllocatePort(int Family, i
1300 THROW_HR_MSG(HRESULT_FROM_WIN32(ERROR_NO_SYSTEM_RESOURCES), "Failed to allocate port");
1301 }
1302
1292 -void WSLCVirtualMachine::ReleasePort(VmPortAllocation& Port)
1293 -{
1294 - std::lock_guard lock{m_lock};
1295 -
1296 - WSL_LOG("ReleasePort", TraceLoggingValue(Port.Port(), "Port"));
1297 -
1298 - LOG_HR_IF(E_UNEXPECTED, m_allocatedPorts.erase(Port.Port()) != 1);
1299 -}
1300 -
1303 wil::unique_socket WSLCVirtualMachine::ConnectUnixSocket(const char* Path)
1304 {
1305 auto [_, __, channel] = Fork(WSLC_FORK::Thread);
src/windows/wslcsession/WSLCVirtualMachine.h
+13 -4
@@ -23,6 +23,7 @@ Abstract:
23 #include <thread>
24 #include <filesystem>
25 #include <optional>
26 +#include <set>
27
28 namespace wsl::windows::service::wslc {
29
@@ -49,11 +50,20 @@ struct WSLCProcessFd
50
51 class WSLCVirtualMachine;
52
53 +// Owns the set of in-use VM-side port numbers for a single VM instance. Held by shared_ptr from the
54 +// VM (sole owner) and referenced weakly by each VmPortAllocation, so a VM teardown drops every
55 +// reservation and any surviving allocation self-neuters instead of dangling into a freed VM.
56 +struct VmPortReservations
57 +{
58 + std::mutex Mutex;
59 + std::set<uint16_t> Ports;
60 +};
61 +
62 struct VmPortAllocation
63 {
64 NON_COPYABLE(VmPortAllocation);
65
56 - VmPortAllocation(uint16_t port, int Family, int Protocol, WSLCVirtualMachine& vm);
66 + VmPortAllocation(uint16_t port, int Family, int Protocol, std::weak_ptr<VmPortReservations> reservations);
67 VmPortAllocation(VmPortAllocation&& Other);
68 ~VmPortAllocation();
69
@@ -69,7 +79,7 @@ private:
79 uint16_t m_port{};
80 int m_family{};
81 int m_protocol{};
72 - WSLCVirtualMachine* m_vm{};
82 + std::weak_ptr<VmPortReservations> m_reservations;
83 };
84
85 struct VMPortMapping
@@ -146,7 +156,6 @@ public:
156
157 std::shared_ptr<VmPortAllocation> TryAllocatePort(uint16_t Port, int Family, int Protocol);
158 std::shared_ptr<VmPortAllocation> AllocatePort(int Family, int Protocol);
149 - void ReleasePort(VmPortAllocation& Port);
159
160 Microsoft::WRL::ComPtr<WSLCProcess> CreateLinuxProcess(
161 _In_ LPCSTR Executable,
@@ -253,7 +262,7 @@ private:
262 std::thread m_processExitThread;
263 std::thread m_crashDumpThread;
264
256 - std::set<uint16_t> m_allocatedPorts;
265 + std::shared_ptr<VmPortReservations> m_reservations = std::make_shared<VmPortReservations>();
266
267 GUID m_vmId{};
268
test/windows/PluginTests.cpp
+147 -1
@@ -606,7 +606,7 @@ class PluginTests
606 return sessionManager;
607 }
608
609 - static wil::com_ptr<IWSLCSession> CreateWslcSession(LPCWSTR Name, WSLCNetworkingMode NetworkingMode = WSLCNetworkingModeNone)
609 + static wil::com_ptr<IWSLCSession> CreateWslcSession(LPCWSTR Name, WSLCNetworkingMode NetworkingMode = WSLCNetworkingModeNone, LPCWSTR StoragePath = nullptr)
610 {
611 WSLCSessionSettings settings{};
612 settings.DisplayName = Name;
@@ -614,6 +614,8 @@ class PluginTests
614 settings.MemoryMb = 4096;
615 settings.BootTimeoutMs = 30 * 1000;
616 settings.NetworkingMode = NetworkingMode;
617 + settings.StoragePath = StoragePath;
618 + settings.MaximumStorageSizeMb = 1024 * 20; // 20GB, only used when StoragePath is set.
619
620 auto manager = OpenWslcSessionManager();
621 wil::com_ptr<IWSLCSession> session;
@@ -780,6 +782,150 @@ class PluginTests
782 ValidateLogFile(ExpectedOutput);
783 }
784
785 + // Validates the VM-lifecycle hooks: OnWslcVmStarted fires each time the VM is (re)created and
786 + // OnWslcVmStopping each time it is torn down, decoupled from the once-per-session hooks. Also
787 + // proves the started hook can call back into the session (WSLCCreateProcess) without deadlocking.
788 + WSL2_TEST_METHOD(WslcVmRestart)
789 + {
790 + ConfigurePlugin(PluginTestType::WslcVmRestart);
791 +
792 + // Idle termination only tears down storage-backed sessions (tmpfs state is unrecoverable), so
793 + // this restart lifecycle test needs a dedicated persistent storage directory.
794 + const auto storageDir = std::filesystem::current_path() / "test-storage-wslc-vm-restart";
795 + std::error_code storageError;
796 + std::filesystem::remove_all(storageDir, storageError);
797 + std::filesystem::create_directories(storageDir);
798 + auto storageCleanup = wil::scope_exit([&]() {
799 + std::error_code ec;
800 + std::filesystem::remove_all(storageDir, ec);
801 + });
802 +
803 + {
804 + auto session = CreateWslcSession(L"plugin-wslc-vm-restart", WSLCNetworkingModeNone, storageDir.c_str());
805 +
806 + // First operation brings the VM up -> OnWslcVmStarted (which reentrantly runs a process).
807 + {
808 + wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"});
809 + auto process = launcher.Launch(*session);
810 + }
811 +
812 + // Force idle teardown of the running VM -> OnWslcVmStopping.
813 + BOOL wasAlreadyIdle = TRUE;
814 + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle));
815 + VERIFY_IS_FALSE(wasAlreadyIdle);
816 +
817 + // Next operation lazily restarts the VM -> OnWslcVmStarted fires again.
818 + {
819 + wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"});
820 + auto process = launcher.Launch(*session);
821 + }
822 +
823 + // Session teardown tears the second VM down -> OnWslcVmStopping, then OnWslcSessionStopping.
824 + }
825 +
826 + constexpr auto ExpectedOutput =
827 + LR"(Plugin loaded. TestMode=22
828 + WSLC Session created, name=plugin-wslc-vm-restart, id=*, pid=*, token=set, sid=set
829 + WSLC VM started, session=*
830 + WSLC VM started reentrant WSLCCreateProcess: ok
831 + WSLC VM started mount+unmount: ok
832 + WSLC VM stopping, session=*
833 + WSLC VM stopping reentrant WSLCCreateProcess: ok
834 + WSLC VM stopping mount+unmount: ok
835 + WSLC VM started, session=*
836 + WSLC VM started reentrant WSLCCreateProcess: ok
837 + WSLC VM started mount+unmount: ok
838 + WSLC VM stopping, session=*
839 + WSLC VM stopping reentrant WSLCCreateProcess: failed
840 + WSLC VM stopping mount+unmount: skipped
841 + WSLC Session stopping, name=plugin-wslc-vm-restart, id=*)";
842 +
843 + ValidateLogFile(ExpectedOutput);
844 + }
845 +
846 + // Validates that an announced VM teardown always happens. The plugin leaves a process running when
847 + // OnWslcVmStopping returns -- which under the previous design made the runtime abandon the
848 + // teardown -- and starts a call from a thread of its own during the notification window. The VM
849 + // must stop anyway, the leaked process must die with it, and the windowed call must be served by
850 + // the stopping VM rather than blocking on the teardown it cannot influence.
851 + WSL2_TEST_METHOD(WslcVmStopCommitted)
852 + {
853 + ConfigurePlugin(PluginTestType::WslcVmStopCommitted);
854 +
855 + // Idle termination only tears down storage-backed sessions (see WslcVmRestart).
856 + const auto storageDir = std::filesystem::current_path() / "test-storage-wslc-vm-stop-committed";
857 + std::error_code storageError;
858 + std::filesystem::remove_all(storageDir, storageError);
859 + std::filesystem::create_directories(storageDir);
860 + auto storageCleanup = wil::scope_exit([&]() {
861 + std::error_code ec;
862 + std::filesystem::remove_all(storageDir, ec);
863 + });
864 +
865 + {
866 + auto session = CreateWslcSession(L"plugin-wslc-vm-stop-committed", WSLCNetworkingModeNone, storageDir.c_str());
867 +
868 + // Bring the VM up -> OnWslcVmStarted.
869 + {
870 + wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"});
871 + auto process = launcher.Launch(*session);
872 + }
873 +
874 + // The teardown is announced and then carried out, even though the plugin's callback left a
875 + // process running. The session is genuinely idle afterwards.
876 + BOOL wasAlreadyIdle = TRUE;
877 + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle));
878 + VERIFY_IS_FALSE(wasAlreadyIdle);
879 +
880 + // The VM is gone, so this starts a fresh one -> a second OnWslcVmStarted.
881 + {
882 + wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"});
883 + auto process = launcher.Launch(*session);
884 + }
885 + }
886 +
887 + // "leaked process died: yes" is the assertion that the announced stop actually happened: the
888 + // process the callback left running was killed by the teardown rather than keeping the VM
889 + // alive. "stop-window caller: ok" shows a plugin call from another thread was served by the
890 + // stopping VM instead of deadlocking against the teardown that callback was holding up. Both
891 + // are reported when that thread is joined, at session teardown.
892 + constexpr auto ExpectedOutput =
893 + LR"(Plugin loaded. TestMode=23
894 + WSLC Session created, name=plugin-wslc-vm-stop-committed, id=*, pid=*, token=set, sid=set
895 + WSLC VM started, session=*
896 + WSLC VM stopping, session=*
897 + WSLC VM stopping leaked process: ok
898 + WSLC VM started, session=*
899 + WSLC stop-window caller: ok
900 + WSLC leaked process died: yes
901 + WSLC Session stopping, name=plugin-wslc-vm-stop-committed, id=*)";
902 +
903 + ValidateLogFile(ExpectedOutput);
904 + }
905 +
906 + WSL2_TEST_METHOD(WslcVmNeverStarted)
907 + {
908 + ConfigurePlugin(PluginTestType::WslcVmNeverStarted);
909 +
910 + // A session whose VM is never needed. VM bring-up is lazy, so creating and destroying the
911 + // session must not produce either VM notification: OnWslcVmStopping is documented to fire
912 + // exactly once per OnWslcVmStarted, and a stop for a VM that never existed would break the
913 + // pairing every plugin relies on to track VM lifetime. The plugin also issues a call from
914 + // OnWslcSessionCreated, which must be rejected rather than bring a VM up.
915 + {
916 + auto session = CreateWslcSession(L"plugin-wslc-vm-never-started");
917 + VERIFY_IS_NOT_NULL(session.get());
918 + }
919 +
920 + constexpr auto ExpectedOutput =
921 + LR"(Plugin loaded. TestMode=24
922 + WSLC Session created, name=plugin-wslc-vm-never-started, id=*, pid=*, token=set, sid=set
923 + WSLC no-vm caller: rejected
924 + WSLC Session stopping, name=plugin-wslc-vm-never-started, id=*)";
925 +
926 + ValidateLogFile(ExpectedOutput);
927 + }
928 +
929 // This test must run last so it doesn't break test cases that depends on plugin signature.
930 WSL2_TEST_METHOD(InvalidPluginSignature)
931 {
test/windows/PluginTests.h
+4 -1
@@ -41,7 +41,10 @@ enum class PluginTestType
41 WslcSuccess,
42 WslcSessionRejected,
43 WslcContainerRejected,
44 - WslcImagePull
44 + WslcImagePull,
45 + WslcVmRestart,
46 + WslcVmStopCommitted,
47 + WslcVmNeverStarted
48 };
49
50 constexpr auto c_testType = L"TestType";
test/windows/WSLCTests.cpp
+350 -43
@@ -3539,9 +3539,9 @@ class WSLCTests
3539 constexpr auto c_mountPoint = "/testdata";
3540 auto mountSource = std::filesystem::absolute(g_testDataPath);
3541
3542 - VERIFY_SUCCEEDED(session->MountWindowsFolder(mountSource.c_str(), c_mountPoint, true));
3543 - auto unmount =
3544 - wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_FAILED(session->UnmountWindowsFolder(c_mountPoint)); });
3542 + VERIFY_SUCCEEDED(session->MountWindowsFolder(mountSource.c_str(), c_mountPoint, true, TRUE));
3543 + auto unmount = wil::scope_exit_log(
3544 + WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_FAILED(session->UnmountWindowsFolder(c_mountPoint, TRUE)); });
3545
3546 const auto installCommand = std::format("tdnf install -y --disablerepo='*' --nogpgcheck {}/packages/*.rpm", c_mountPoint);
3547 auto installSocat = WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", installCommand}).Launch(*session);
@@ -3718,35 +3718,35 @@ class WSLCTests
3718
3719 // Validate writeable mount.
3720 {
3721 - VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false));
3721 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false, TRUE));
3722 ExpectMount(session.get(), "/win-path", expectedMountOptions(false));
3723
3724 // Validate that mount can't be stacked on each other
3725 - VERIFY_ARE_EQUAL(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
3725 + VERIFY_ARE_EQUAL(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false, TRUE), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
3726
3727 // Validate that folder is writeable from linux
3728 ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo -n content > /win-path/file.txt && sync"}, 0);
3729 VERIFY_ARE_EQUAL(ReadFileContent(testFolder / "file.txt"), L"content");
3730
3731 - VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3731 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path", TRUE));
3732 ExpectMount(session.get(), "/win-path", {});
3733 }
3734
3735 // Validate read-only mount.
3736 {
3737 - VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
3737 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true, TRUE));
3738 ExpectMount(session.get(), "/win-path", expectedMountOptions(true));
3739
3740 // Validate that folder is not writeable from linux
3741 ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo -n content > /win-path/file.txt"}, 1);
3742
3743 - VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3743 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path", TRUE));
3744 ExpectMount(session.get(), "/win-path", {});
3745 }
3746
3747 // Validate that a read-only share cannot be made writeable via mount -o remount,rw.
3748 {
3749 - VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
3749 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true, TRUE));
3750 ExpectMount(session.get(), "/win-path", expectedMountOptions(true));
3751
3752 // Attempt an in-place remount to read-write from the guest.
@@ -3755,14 +3755,14 @@ class WSLCTests
3755 // Verify the folder is still not writeable.
3756 ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo -n content > /win-path/file.txt"}, 1);
3757
3758 - VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3758 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path", TRUE));
3759 ExpectMount(session.get(), "/win-path", {});
3760 }
3761
3762 // Validate that the device host enforces read-only even if the guest tries to bypass mount options.
3763 if (enableVirtioFs)
3764 {
3765 - VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
3765 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true, TRUE));
3766 ExpectMount(session.get(), "/win-path", expectedMountOptions(true));
3767
3768 // Remount a bind of the share as read-write to ensure the device host still enforces read-only access.
@@ -3780,25 +3780,25 @@ class WSLCTests
3780 ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo -n content > /win-path-rw/file.txt"}, 1);
3781 ExpectCommandResult(session.get(), {"/bin/sh", "-c", "umount /win-path-rw && rmdir /win-path-rw"}, 0);
3782
3783 - VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3783 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path", TRUE));
3784 ExpectMount(session.get(), "/win-path", {});
3785 }
3786
3787 // Validate various error paths
3788 {
3789 - VERIFY_ARE_EQUAL(session->MountWindowsFolder(L"relative-path", "/win-path", true), E_INVALIDARG);
3790 - VERIFY_ARE_EQUAL(session->MountWindowsFolder(L"C:\\does-not-exist", "/win-path", true), HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND));
3791 - VERIFY_ARE_EQUAL(session->MountWindowsFolder(testFolder.c_str(), "relative-mountpoint", true), E_INVALIDARG);
3792 - VERIFY_ARE_EQUAL(session->MountWindowsFolder(testFolder.c_str(), "", true), E_INVALIDARG);
3793 - VERIFY_ARE_EQUAL(session->UnmountWindowsFolder("/not-mounted"), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
3794 - VERIFY_ARE_EQUAL(session->UnmountWindowsFolder("/proc"), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
3789 + VERIFY_ARE_EQUAL(session->MountWindowsFolder(L"relative-path", "/win-path", true, TRUE), E_INVALIDARG);
3790 + VERIFY_ARE_EQUAL(session->MountWindowsFolder(L"C:\\does-not-exist", "/win-path", true, TRUE), HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND));
3791 + VERIFY_ARE_EQUAL(session->MountWindowsFolder(testFolder.c_str(), "relative-mountpoint", true, TRUE), E_INVALIDARG);
3792 + VERIFY_ARE_EQUAL(session->MountWindowsFolder(testFolder.c_str(), "", true, TRUE), E_INVALIDARG);
3793 + VERIFY_ARE_EQUAL(session->UnmountWindowsFolder("/not-mounted", TRUE), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
3794 + VERIFY_ARE_EQUAL(session->UnmountWindowsFolder("/proc", TRUE), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
3795
3796 // Validate that folders that are manually unmounted from the guest are handled properly
3797 - VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
3797 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true, TRUE));
3798 ExpectMount(session.get(), "/win-path", expectedMountOptions(true));
3799
3800 ExpectCommandResult(session.get(), {"/usr/bin/umount", "/win-path"}, 0);
3801 - VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3801 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path", TRUE));
3802 }
3803 }
3804
@@ -3834,8 +3834,8 @@ class WSLCTests
3834
3835 // Concurrent mounts of the same host path use distinct children on the same aggregate device.
3836 {
3837 - VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path-1", false));
3838 - VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path-2", false));
3837 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path-1", false, TRUE));
3838 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path-2", false, TRUE));
3839
3840 auto firstDevice = getMountField("/win-path-1", "MAJ:MIN");
3841 auto secondDevice = getMountField("/win-path-2", "MAJ:MIN");
@@ -3853,19 +3853,19 @@ class WSLCTests
3853 const auto firstChild = std::format("/run/wsl/virtiofs-mounts/{}{}", LX_INIT_DRVFS_VIRTIO_TAG, firstRoot);
3854 const auto secondChild = std::format("/run/wsl/virtiofs-mounts/{}{}", LX_INIT_DRVFS_VIRTIO_TAG, secondRoot);
3855
3856 - VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path-1"));
3856 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path-1", TRUE));
3857 ExpectCommandResult(session.get(), {"/bin/cat", "/win-path-2/marker.txt"}, 0);
3858 ExpectCommandResult(session.get(), {"/usr/bin/test", "!", "-e", firstChild}, 0);
3859 ExpectCommandResult(session.get(), {"/usr/bin/test", "-e", secondChild}, 0);
3860
3861 - VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path-2"));
3861 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path-2", TRUE));
3862 ExpectCommandResult(session.get(), {"/usr/bin/test", "!", "-e", secondChild}, 0);
3863 }
3864
3865 // Verify that read-write and read-only shares use different children on the same aggregate device.
3866 {
3867 - VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path-rw", false));
3868 - VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path-ro", true));
3867 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path-rw", false, TRUE));
3868 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path-ro", true, TRUE));
3869
3870 auto rwDevice = getMountField("/win-path-rw", "MAJ:MIN");
3871 auto roDevice = getMountField("/win-path-ro", "MAJ:MIN");
@@ -3875,8 +3875,8 @@ class WSLCTests
3875 VERIFY_ARE_EQUAL(rwDevice, roDevice);
3876 VERIFY_ARE_NOT_EQUAL(rwRoot, roRoot);
3877
3878 - VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path-rw"));
3879 - VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path-ro"));
3878 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path-rw", TRUE));
3879 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path-ro", TRUE));
3880 }
3881 }
3882
@@ -3904,8 +3904,8 @@ class WSLCTests
3904 return root;
3905 };
3906
3907 - VERIFY_SUCCEEDED(session->MountWindowsFolder(firstFolder.c_str(), "/remove-child-first", false));
3908 - VERIFY_SUCCEEDED(session->MountWindowsFolder(secondFolder.c_str(), "/remove-child-second", false));
3907 + VERIFY_SUCCEEDED(session->MountWindowsFolder(firstFolder.c_str(), "/remove-child-first", false, TRUE));
3908 + VERIFY_SUCCEEDED(session->MountWindowsFolder(secondFolder.c_str(), "/remove-child-second", false, TRUE));
3909
3910 const auto firstRoot = getMountRoot("/remove-child-first");
3911 const auto secondRoot = getMountRoot("/remove-child-second");
@@ -3917,12 +3917,12 @@ class WSLCTests
3917 ExpectCommandResult(session.get(), {"/usr/bin/test", "-e", firstChild}, 0);
3918 ExpectCommandResult(session.get(), {"/usr/bin/test", "-e", secondChild}, 0);
3919
3920 - VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/remove-child-first"));
3920 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/remove-child-first", TRUE));
3921 ExpectCommandResult(session.get(), {"/usr/bin/test", "!", "-e", firstChild}, 0);
3922 ExpectCommandResult(session.get(), {"/bin/cat", "/remove-child-second/marker.txt"}, 0);
3923 ExpectCommandResult(session.get(), {"/usr/bin/test", "-e", secondChild}, 0);
3924
3925 - VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/remove-child-second"));
3925 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/remove-child-second", TRUE));
3926 ExpectCommandResult(session.get(), {"/usr/bin/test", "!", "-e", secondChild}, 0);
3927 }
3928
@@ -3948,7 +3948,7 @@ class WSLCTests
3948 std::ofstream(folder / "marker.txt") << index;
3949
3950 const auto mountPoint = std::format("/vfs-many-{}", index);
3951 - VERIFY_SUCCEEDED(session->MountWindowsFolder(folder.c_str(), mountPoint.c_str(), false));
3951 + VERIFY_SUCCEEDED(session->MountWindowsFolder(folder.c_str(), mountPoint.c_str(), false, TRUE));
3952 mountPoints.emplace_back(mountPoint);
3953
3954 const auto command = std::format("cat {}/marker.txt", mountPoint);
@@ -3958,7 +3958,7 @@ class WSLCTests
3958
3959 for (const auto& mountPoint : mountPoints)
3960 {
3961 - VERIFY_SUCCEEDED(session->UnmountWindowsFolder(mountPoint.c_str()));
3961 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder(mountPoint.c_str(), TRUE));
3962 }
3963 }
3964
@@ -4134,7 +4134,7 @@ class WSLCTests
4134 options.Flags = static_cast<WSLCProcessFlags>(0x4);
4135 wil::com_ptr<IWSLCProcess> process;
4136 int err = 0;
4137 - VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateRootNamespaceProcess("/bin/true", &options, 0, 0, &process, &err));
4137 + VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateRootNamespaceProcess("/bin/true", &options, 0, 0, FALSE, &process, &err));
4138 }
4139
4140 // Simple case
@@ -12169,6 +12169,10 @@ class WSLCTests
12169 auto settings = GetDefaultSessionSettings(c_sessionName);
12170 auto session = CreateSession(settings);
12171
12172 + // Session creation is lazy, so start the VM by launching a process before killing it.
12173 + WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"});
12174 + auto process = launcher.Launch(*session);
12175 +
12176 KillVmByOwner(c_sessionName);
12177
12178 WaitForSessionTermination(session.get());
@@ -12193,6 +12197,284 @@ class WSLCTests
12197 VERIFY_IS_FALSE(IsVmRunning(c_sessionName));
12198 }
12199
12200 + // TriggerIdleTermination runs the idle-teardown path synchronously and reports whether the VM
12201 + // was already idle. Validates the idle -> running -> forced-idle -> running lifecycle.
12202 + WSLC_TEST_METHOD(TriggerIdleTerminationRestartsVm)
12203 + {
12204 + constexpr auto c_sessionName = L"wslc-idle-trigger-test";
12205 +
12206 + // Idle termination is only permitted for storage-backed sessions (tmpfs state is
12207 + // unrecoverable), so this lifecycle test uses a dedicated storage directory.
12208 + const auto storageDir = std::filesystem::current_path() / "test-storage-idle-restart";
12209 + std::error_code storageError;
12210 + std::filesystem::remove_all(storageDir, storageError);
12211 + std::filesystem::create_directories(storageDir);
12212 + auto storageCleanup = wil::scope_exit([&]() {
12213 + std::error_code ec;
12214 + std::filesystem::remove_all(storageDir, ec);
12215 + });
12216 +
12217 + auto settings = GetDefaultSessionSettings(c_sessionName);
12218 + settings.StoragePath = storageDir.c_str();
12219 + auto session = CreateSession(settings);
12220 +
12221 + // The VM starts lazily, so a freshly created session is already idle.
12222 + BOOL wasAlreadyIdle = FALSE;
12223 + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle));
12224 + VERIFY_IS_TRUE(wasAlreadyIdle);
12225 + VERIFY_IS_FALSE(IsVmRunning(c_sessionName));
12226 +
12227 + // Starting a process brings the VM up.
12228 + {
12229 + WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"});
12230 + auto process = launcher.Launch(*session);
12231 + VERIFY_IS_TRUE(IsVmRunning(c_sessionName));
12232 + }
12233 +
12234 + // Releasing the process wrapper removes its activity hold, allowing idle termination.
12235 + wasAlreadyIdle = TRUE;
12236 + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle));
12237 + VERIFY_IS_FALSE(wasAlreadyIdle);
12238 + VERIFY_IS_FALSE(IsVmRunning(c_sessionName));
12239 +
12240 + // A second trigger is now a no-op.
12241 + wasAlreadyIdle = FALSE;
12242 + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle));
12243 + VERIFY_IS_TRUE(wasAlreadyIdle);
12244 +
12245 + // The session survives and lazily restarts the VM on the next operation.
12246 + WSLCProcessLauncher launcher2("/bin/sleep", {"/bin/sleep", "60"});
12247 + auto process2 = launcher2.Launch(*session);
12248 + VERIFY_IS_TRUE(IsVmRunning(c_sessionName));
12249 + }
12250 +
12251 + // A tmpfs-backed session has no persistent storage, so its VM state cannot be recovered after a
12252 + // teardown. TriggerIdleTermination must refuse to tear such a session down (matching the
12253 + // automatic idle timer), leaving the VM running rather than destroying unrecoverable state.
12254 + WSLC_TEST_METHOD(TriggerIdleTerminationRefusedWithoutStorage)
12255 + {
12256 + constexpr auto c_sessionName = L"wslc-idle-tmpfs-test";
12257 + auto session = CreateSession(GetDefaultSessionSettings(c_sessionName));
12258 +
12259 + // A never-started session is already idle even though idle termination is disabled.
12260 + BOOL wasAlreadyIdle = FALSE;
12261 + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle));
12262 + VERIFY_IS_TRUE(wasAlreadyIdle);
12263 + VERIFY_IS_FALSE(IsVmRunning(c_sessionName));
12264 +
12265 + WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"});
12266 + auto process = launcher.Launch(*session);
12267 + VERIFY_IS_TRUE(IsVmRunning(c_sessionName));
12268 +
12269 + wasAlreadyIdle = TRUE;
12270 + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle));
12271 + VERIFY_IS_FALSE(wasAlreadyIdle);
12272 +
12273 + // The VM must still be running: the tmpfs session was not torn down.
12274 + VERIFY_IS_TRUE(IsVmRunning(c_sessionName));
12275 + }
12276 +
12277 + // A running container pins the VM via its activity hold, matching the production idle timer.
12278 + WSLC_TEST_METHOD(TriggerIdleTerminationDefersForRunningContainer)
12279 + {
12280 + WSLCContainerLauncher launcher("debian:latest", "wslc-idle-active", {"/bin/sleep", "600"});
12281 + auto container = launcher.Launch(*m_defaultSession, WSLCContainerStartFlagsNone);
12282 +
12283 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
12284 + VERIFY_IS_TRUE(IsVmRunning(c_testSessionName));
12285 +
12286 + // The test hook honors the same activity guard as automatic idle teardown.
12287 + BOOL wasAlreadyIdle = TRUE;
12288 + VERIFY_SUCCEEDED(m_defaultSession->TriggerIdleTermination(&wasAlreadyIdle));
12289 + VERIFY_IS_FALSE(wasAlreadyIdle);
12290 + VERIFY_IS_TRUE(IsVmRunning(c_testSessionName));
12291 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
12292 + }
12293 +
12294 + // A created or stopped container has no activity hold. Its port mapping and bind mount must remain
12295 + // usable after each idle teardown and lazy VM restart.
12296 + WSLC_TEST_METHOD(TriggerIdleTerminationRecoversStoppedContainerResources)
12297 + {
12298 + const auto hostFolder = std::filesystem::current_path() / "test-idle-container-volume";
12299 + std::filesystem::create_directories(hostFolder);
12300 + VERIFY_IS_TRUE((std::ofstream(hostFolder / "marker.txt") << "idle-recovery").good());
12301 + auto folderCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
12302 + std::error_code ec;
12303 + std::filesystem::remove_all(hostFolder, ec);
12304 + });
12305 +
12306 + WSLCContainerLauncher launcher(
12307 + "python:3.12-alpine",
12308 + "wslc-idle-resource-recovery",
12309 + {"python3", "-m", "http.server", "8000", "--bind", "0.0.0.0", "--directory", "/data"},
12310 + {"PYTHONUNBUFFERED=1"},
12311 + "bridge");
12312 + launcher.AddPort(1270, 8000, AF_INET);
12313 + launcher.AddVolume(hostFolder.wstring(), "/data", true);
12314 + auto container = launcher.Create(*m_defaultSession);
12315 +
12316 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateCreated);
12317 +
12318 + // Tear down before the first start, then validate both recovered resources.
12319 + BOOL wasAlreadyIdle = TRUE;
12320 + VERIFY_SUCCEEDED(m_defaultSession->TriggerIdleTermination(&wasAlreadyIdle));
12321 + VERIFY_IS_FALSE(wasAlreadyIdle);
12322 + VERIFY_IS_FALSE(IsVmRunning(c_testSessionName));
12323 +
12324 + for (int iteration = 0; iteration < 2; ++iteration)
12325 + {
12326 + {
12327 + VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsAttach, nullptr, nullptr));
12328 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
12329 + VERIFY_IS_TRUE(IsVmRunning(c_testSessionName));
12330 +
12331 + auto initProcess = container.GetInitProcess();
12332 + WaitForOutput(initProcess.GetStdHandle(1), "Serving HTTP on");
12333 +
12334 + const auto ports = container.Inspect().Ports;
12335 + VERIFY_IS_TRUE(ports.contains("8000/tcp"));
12336 + VERIFY_ARE_EQUAL(ports.at("8000/tcp").size(), 1u);
12337 + VERIFY_ARE_EQUAL(ports.at("8000/tcp")[0].HostPort, std::string{"1270"});
12338 + ExpectHttpResponse(L"http://127.0.0.1:1270/marker.txt", 200);
12339 +
12340 + VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
12341 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
12342 + }
12343 +
12344 + wasAlreadyIdle = TRUE;
12345 + VERIFY_SUCCEEDED(m_defaultSession->TriggerIdleTermination(&wasAlreadyIdle));
12346 + VERIFY_IS_FALSE(wasAlreadyIdle);
12347 + VERIFY_IS_FALSE(IsVmRunning(c_testSessionName));
12348 + }
12349 + }
12350 +
12351 + // A container that outlives an idle teardown still owns VM-scoped state (bind mounts, port
12352 + // relays) that is released from ~WSLCContainerImpl when the session is finally torn down. By
12353 + // then the VM object is gone, and a graceful idle teardown leaves VmExited() false, so the
12354 + // release path must key off "no VM" as well; a throw out of the destructor is unrecoverable
12355 + // because destructors are noexcept and would terminate the session host.
12356 + WSLC_TEST_METHOD(SessionTerminationAfterIdleTerminationWithContainer)
12357 + {
12358 + const auto hostFolder = std::filesystem::current_path() / "test-idle-terminate-volume";
12359 + std::filesystem::create_directories(hostFolder);
12360 + auto folderCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
12361 + std::error_code ec;
12362 + std::filesystem::remove_all(hostFolder, ec);
12363 + });
12364 +
12365 + WSLCContainerLauncher launcher("debian:latest", "wslc-idle-terminate-session", {"/bin/sleep", "600"});
12366 + launcher.AddVolume(hostFolder.wstring(), "/data", true);
12367 +
12368 + {
12369 + auto container = launcher.Launch(*m_defaultSession, WSLCContainerStartFlagsNone);
12370 +
12371 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
12372 + VERIFY_IS_TRUE(IsVmRunning(c_testSessionName));
12373 +
12374 + // Stop the container so it releases its activity hold on the VM.
12375 + VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
12376 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
12377 +
12378 + // Drop the client reference without deleting: the session keeps the only remaining
12379 + // reference in m_containers, so the impl is destroyed by the session teardown below
12380 + // rather than here (where the VM is still alive and the release path is trivially safe).
12381 + container.SetDeleteOnClose(false);
12382 + }
12383 +
12384 + // Tear the VM down. The container metadata (including its mounted-volume state) deliberately
12385 + // survives, while the VM object is released without the exit event ever being signaled.
12386 + BOOL wasAlreadyIdle = TRUE;
12387 + VERIFY_SUCCEEDED(m_defaultSession->TriggerIdleTermination(&wasAlreadyIdle));
12388 + VERIFY_IS_FALSE(wasAlreadyIdle);
12389 + VERIFY_IS_FALSE(IsVmRunning(c_testSessionName));
12390 +
12391 + // Terminating clears m_containers, running ~WSLCContainerImpl with no VM to unmount from.
12392 + VERIFY_SUCCEEDED(m_defaultSession->Terminate());
12393 + WaitForSessionTermination(m_defaultSession.get());
12394 +
12395 + {
12396 + auto restore = ResetTestSession();
12397 + }
12398 +
12399 + // The session host must still be alive and usable: if the destructor threw, the per-user
12400 + // host process died and this fails.
12401 + WSLCProcessLauncher processLauncher("/bin/echo", {"/bin/echo", "OK"});
12402 + auto process = processLauncher.Launch(*m_defaultSession);
12403 + VERIFY_ARE_EQUAL(process.Wait(), 0);
12404 + PruneResult pruneResult;
12405 + LOG_IF_FAILED(m_defaultSession->PruneContainers(nullptr, 0, &pruneResult.result));
12406 + }
12407 +
12408 + // Hammer the idle-teardown path concurrently with VM-level operations to surface deadlocks or
12409 + // stale-state races. Operations may fail while the VM is being torn down; that is tolerated, but
12410 + // the workers must never hang and the session must remain usable afterwards.
12411 + WSLC_TEST_METHOD(TriggerIdleTerminationConcurrentWithOperations)
12412 + {
12413 + constexpr auto c_sessionName = L"wslc-idle-hammer-test";
12414 +
12415 + // Idle termination only tears down storage-backed sessions, so a dedicated storage directory
12416 + // is required for the teardown path to actually run under the concurrent hammering.
12417 + const auto storageDir = std::filesystem::current_path() / "test-storage-idle-hammer";
12418 + std::error_code storageError;
12419 + std::filesystem::remove_all(storageDir, storageError);
12420 + std::filesystem::create_directories(storageDir);
12421 + auto storageCleanup = wil::scope_exit([&]() {
12422 + std::error_code ec;
12423 + std::filesystem::remove_all(storageDir, ec);
12424 + });
12425 +
12426 + auto settings = GetDefaultSessionSettings(c_sessionName);
12427 + settings.StoragePath = storageDir.c_str();
12428 + auto session = CreateSession(settings);
12429 +
12430 + std::atomic<bool> stop = false;
12431 + std::atomic<unsigned int> opFailures = 0;
12432 +
12433 + // Run the hammering worker via a future so the join is bounded: a real teardown/operation
12434 + // deadlock would otherwise hang the test host indefinitely instead of failing. If the worker
12435 + // does not drain within the timeout after we signal stop, treat it as a deadlock and fail.
12436 + auto worker = std::async(std::launch::async, [&]() {
12437 + while (!stop.load())
12438 + {
12439 + try
12440 + {
12441 + WSLCProcessLauncher launcher("/bin/true", {"/bin/true"});
12442 + auto process = launcher.Launch(*session);
12443 + process.GetExitEvent().wait(5000);
12444 + }
12445 + catch (...)
12446 + {
12447 + opFailures.fetch_add(1);
12448 + }
12449 + }
12450 + });
12451 +
12452 + for (int i = 0; i < 25; ++i)
12453 + {
12454 + BOOL wasAlreadyIdle = FALSE;
12455 + VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle));
12456 + }
12457 +
12458 + stop.store(true);
12459 +
12460 + // A real teardown/operation deadlock would leave the worker wedged forever. The std::future
12461 + // destructor blocks until the task completes, so letting a failed VERIFY unwind here would hang
12462 + // the test host -- exactly what this test guards against. Fail fast with a dump on timeout so we
12463 + // never unwind with an unfinished async task.
12464 + FAIL_FAST_IF_MSG(
12465 + worker.wait_for(std::chrono::seconds(60)) != std::future_status::ready,
12466 + "hammering worker did not drain after stop; likely teardown/operation deadlock");
12467 +
12468 + worker.get();
12469 +
12470 + LogInfo("TriggerIdleTerminationConcurrentWithOperations tolerated %u operation failures", opFailures.load());
12471 +
12472 + // The session must still be usable after the hammering.
12473 + WSLCProcessLauncher launcher("/bin/true", {"/bin/true"});
12474 + auto process = launcher.Launch(*session);
12475 + VERIFY_IS_TRUE(process.GetExitEvent().wait(30000));
12476 + }
12477 +
12478 // Helper: COM callback that captures all warnings received.
12479 class CapturingWarningCallback
12480 : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IWarningCallback, IFastRundown>
@@ -12260,13 +12542,21 @@ class WSLCTests
12542 VERIFY_SUCCEEDED(sessionManager2->CreateSession(&settings2, WSLCSessionFlagsNone, warningCallback.Get(), &session2));
12543 wsl::windows::common::security::ConfigureForCOMImpersonation(session2.get());
12544
12263 - // Verify the warning matches the expected localized message for the corrupt container.
12545 + // The VM (and container recovery) starts lazily on the first operation. Trigger it via a
12546 + // callback-bearing operation (CreateNetwork) so recovery warnings reach the warning callback.
12547 + WSLCNetworkOptions triggerNetwork{};
12548 + triggerNetwork.Name = "wslc-recovery-trigger";
12549 + triggerNetwork.Driver = "bridge";
12550 + VERIFY_SUCCEEDED(session2->CreateNetwork(&triggerNetwork, warningCallback.Get()));
12551 +
12552 + // Recovery runs during the lazy VM start under this operation's context, so the failure
12553 + // warning is delivered to its warning callback.
12554 auto warnings = warningCallback->GetWarnings();
12265 - auto expectedWarning = std::format(
12555 + auto recoveryWarning = std::format(
12556 L"wsl: {}\n",
12557 wsl::shared::Localization::MessageWslcFailedToRecoverContainer(wsl::shared::string::MultiByteToWide(containerId)));
12558
12269 - VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == expectedWarning; }));
12559 + VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == recoveryWarning; }));
12560
12561 VERIFY_SUCCEEDED(session2->Terminate());
12562 }
@@ -12328,12 +12618,20 @@ class WSLCTests
12618 VERIFY_SUCCEEDED(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, warningCallback.Get(), &session));
12619 wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
12620
12331 - // Verify the warning matches the expected localized message for the missing volume.
12621 + // The VM (and volume recovery) starts lazily on the first operation. Trigger it via a
12622 + // callback-bearing operation (CreateNetwork) so recovery warnings reach the warning callback.
12623 + WSLCNetworkOptions triggerNetwork{};
12624 + triggerNetwork.Name = "wslc-recovery-trigger";
12625 + triggerNetwork.Driver = "bridge";
12626 + VERIFY_SUCCEEDED(session->CreateNetwork(&triggerNetwork, warningCallback.Get()));
12627 +
12628 + // Recovery runs during the lazy VM start under this operation's context, so the failure
12629 + // warning is delivered to its warning callback.
12630 auto warnings = warningCallback->GetWarnings();
12333 - auto expectedWarning =
12631 + auto recoveryWarning =
12632 std::format(L"wsl: {}\n", wsl::shared::Localization::MessageWslcFailedToRecoverVolume(L"wslc-test-warning-recovery"));
12633
12336 - VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == expectedWarning; }));
12634 + VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == recoveryWarning; }));
12635
12636 // Clean up the orphaned volume from Docker's metadata.
12637 LOG_IF_FAILED(session->DeleteVolume("wslc-test-warning-recovery"));
@@ -12393,10 +12691,19 @@ class WSLCTests
12691 VERIFY_SUCCEEDED(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, warningCallback.Get(), &session));
12692 wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
12693
12694 + // The VM (and guest volume recovery) starts lazily on the first operation. Trigger it via a
12695 + // callback-bearing operation (CreateNetwork) so recovery warnings reach the warning callback.
12696 + WSLCNetworkOptions triggerNetwork{};
12697 + triggerNetwork.Name = "wslc-recovery-trigger";
12698 + triggerNetwork.Driver = "bridge";
12699 + VERIFY_SUCCEEDED(session->CreateNetwork(&triggerNetwork, warningCallback.Get()));
12700 +
12701 + // Recovery runs during the lazy VM start under this operation's context, so the failure
12702 + // warning is delivered to its warning callback.
12703 auto warnings = warningCallback->GetWarnings();
12397 - auto expectedWarning = std::format(L"wsl: {}\n", wsl::shared::Localization::MessageWslcFailedToRecoverVolume(c_volumeName));
12704 + auto recoveryWarning = std::format(L"wsl: {}\n", wsl::shared::Localization::MessageWslcFailedToRecoverVolume(c_volumeName));
12705
12399 - VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == expectedWarning; }));
12706 + VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == recoveryWarning; }));
12707
12708 // Clean up the volume from Docker's metadata.
12709 ExpectCommandResult(session.get(), {"/usr/bin/docker", "volume", "rm", "-f", c_volumeName}, 0);
test/windows/testplugin/Plugin.cpp
+244 -5
@@ -13,6 +13,8 @@ Abstract:
13 --*/
14
15 #include "precomp.h"
16 +#include <atomic>
17 +#include <thread>
18 #include "WslPluginApi.h"
19 #include "wslc_schema.h"
20
@@ -29,6 +31,23 @@ std::optional<GUID> g_distroGuid;
31 const WSLPluginAPIV1* g_api = nullptr;
32 PluginTestType g_testType = PluginTestType::Invalid;
33
34 +// Process deliberately left running across OnWslcVmStopping by the WslcVmStopCommitted test, to
35 +// prove the announced teardown happens anyway. Never released: it dies with the VM.
36 +//
37 +// The exit event is fetched on the callback's own thread and cached here as a plain Win32 handle:
38 +// the process itself is a COM proxy marshalled to that thread, so the stop-window thread below
39 +// cannot call methods on it (RPC_E_WRONG_THREAD), but it can wait on the handle.
40 +std::atomic<WSLCProcessHandle> g_leakedProcess = nullptr;
41 +std::atomic<HANDLE> g_leakedProcessExitEvent = nullptr;
42 +
43 +// Set by the WslcVmStopCommitted test: a call issued from a thread the plugin owns while
44 +// OnWslcVmStopping is running. Like the callback itself it is served by the VM that is stopping, and
45 +// must not block on the teardown. It deliberately logs nothing of its own -- its results are written
46 +// when it is joined -- so g_logfile keeps a single writer and the expected output stays ordered.
47 +std::thread g_stopWindowCaller;
48 +HRESULT g_stopWindowCallerResult = E_PENDING;
49 +std::atomic<bool> g_leakedProcessDied = false;
50 +
51 std::optional<uint32_t> g_previousInitPid;
52
53 std::vector<char> ReadFromSocket(SOCKET socket)
@@ -349,13 +368,36 @@ try
368 << ", pid=" << Session->ApplicationPid << ", token=" << (Session->UserToken != nullptr ? "set" : "null")
369 << ", sid=" << (Session->UserSid != nullptr ? "set" : "null") << std::endl;
370
371 + if (g_testType == PluginTestType::WslcVmNeverStarted)
372 + {
373 + // A plugin call is never a reason to create a VM. This one has to be rejected rather than
374 + // bringing one up, which the absence of any VM notification in the expected output confirms.
375 + std::vector<const char*> args = {"/bin/true", nullptr};
376 + WSLCProcessHandle process = nullptr;
377 + const auto hr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, nullptr);
378 + if (SUCCEEDED(hr))
379 + {
380 + g_api->WSLCReleaseProcess(process);
381 + }
382 +
383 + g_logfile << "WSLC no-vm caller: " << (hr == WSLC_E_VM_NOT_RUNNING ? "rejected" : "unexpected") << std::endl;
384 + return S_OK;
385 + }
386 +
387 if (g_testType == PluginTestType::WslcSessionRejected)
388 {
389 g_logfile << "OnWslcSessionCreated: ERROR_ACCESS_DENIED" << std::endl;
390 return HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED);
391 }
392
358 - if (g_testType == PluginTestType::WslcSuccess)
393 + return S_OK;
394 +}
395 +CATCH_RETURN();
396 +
397 +// These checks need a running VM, and a plugin call never creates one, so they run from the VM-started
398 +// hook rather than from session creation.
399 +void RunWslcSuccessChecks(const WSLCSessionInformation* Session)
400 +{
401 {
402 // Helper: run a command in the root namespace and return (status, stdout, stderr).
403 auto runCommand = [&](const char* cmd,
@@ -488,13 +530,31 @@ try
530
531 g_logfile << "Test completed" << std::endl;
532 }
491 -
492 - return S_OK;
533 }
494 -CATCH_RETURN();
534
535 HRESULT OnWslcSessionStopping(const WSLCSessionInformation* Session)
536 {
537 + // Drain the stop-window thread first, then report what it observed. Logging from here rather than
538 + // from that thread keeps the log single-writer and the expected output deterministic. Safe
539 + // because this is the last event of the session, so the join cannot run inside a VM notification.
540 + if (g_stopWindowCaller.joinable())
541 + {
542 + g_stopWindowCaller.join();
543 +
544 + g_logfile << "WSLC stop-window caller: " << (SUCCEEDED(g_stopWindowCallerResult) ? "ok" : "failed") << std::endl;
545 + g_logfile << "WSLC leaked process died: " << (g_leakedProcessDied.load() ? "yes" : "no") << std::endl;
546 + }
547 +
548 + // Close the duplicated exit event if the stop-window thread did not get far enough to claim it.
549 + // The leaked process wrapper is deliberately not released: it holds a COM proxy marshalled to the
550 + // OnWslcVmStopping callback's thread, and releasing it from this one risks the same
551 + // RPC_E_WRONG_THREAD hazard that forced the exit event to be cached as a plain handle. It is one
552 + // wrapper for the lifetime of a test process, so leaking it is the safer trade.
553 + if (auto* exitEvent = g_leakedProcessExitEvent.exchange(nullptr); exitEvent != nullptr)
554 + {
555 + const wil::unique_handle owned{exitEvent};
556 + }
557 +
558 g_logfile << "WSLC Session stopping, name=" << wsl::shared::string::WideToMultiByte(Session->DisplayName)
559 << ", id=" << Session->SessionId << std::endl;
560
@@ -539,6 +599,183 @@ HRESULT OnWslcImageDeleted(const WSLCSessionInformation* Session, LPCSTR ImageId
599 return S_OK;
600 }
601
602 +HRESULT OnWslcVmStarted(const WSLCSessionInformation* Session)
603 +try
604 +{
605 + if (g_testType == PluginTestType::WslcSuccess)
606 + {
607 + // Run once: the checks are written against the first VM of the session.
608 + static std::atomic<bool> done = false;
609 + if (!done.exchange(true))
610 + {
611 + RunWslcSuccessChecks(Session);
612 + }
613 +
614 + return S_OK;
615 + }
616 +
617 + if (g_testType == PluginTestType::WslcVmStopCommitted)
618 + {
619 + g_logfile << "WSLC VM started, session=" << Session->SessionId << std::endl;
620 + return S_OK;
621 + }
622 +
623 + // The VM-never-started test expects no VM hook to fire at all. Logging here is the diagnostic
624 + // that makes a regression visible: any line from this hook fails the expected output.
625 + if (g_testType == PluginTestType::WslcVmNeverStarted)
626 + {
627 + g_logfile << "WSLC VM started, session=" << Session->SessionId << std::endl;
628 + return S_OK;
629 + }
630 +
631 + // Only log/exercise for the dedicated VM-restart test so other WSLC plugin tests (which start
632 + // and stop VMs incidentally) are not affected by extra log lines.
633 + if (g_testType != PluginTestType::WslcVmRestart)
634 + {
635 + return S_OK;
636 + }
637 +
638 + g_logfile << "WSLC VM started, session=" << Session->SessionId << std::endl;
639 +
640 + // Prove the VM is usable from within the started hook, and that calling back into the session
641 + // (WSLCCreateProcess acquires a VM lease + the runtime lock) does not deadlock.
642 + std::vector<const char*> args = {"/bin/true", nullptr};
643 + WSLCProcessHandle process = nullptr;
644 + const auto hr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, nullptr);
645 + g_logfile << "WSLC VM started reentrant WSLCCreateProcess: " << (SUCCEEDED(hr) ? "ok" : "failed") << std::endl;
646 + if (SUCCEEDED(hr))
647 + {
648 + g_api->WSLCReleaseProcess(process);
649 + }
650 +
651 + // Also exercise a reentrant mount + unmount from the started hook; the session is alive here so
652 + // both calls succeed, validating that mount management reentrant from OnVmStarted does not deadlock.
653 + constexpr auto* mountpoint = "/test-plugin/vm-started-mount";
654 + const auto mountHr = g_api->WSLCMountFolder(Session->SessionId, L"C:\\", mountpoint, TRUE);
655 + if (SUCCEEDED(mountHr))
656 + {
657 + const auto unmountHr = g_api->WSLCUnmountFolder(Session->SessionId, mountpoint);
658 + g_logfile << "WSLC VM started mount+unmount: " << (SUCCEEDED(unmountHr) ? "ok" : "failed") << std::endl;
659 + }
660 + else
661 + {
662 + g_logfile << "WSLC VM started mount+unmount: skipped" << std::endl;
663 + }
664 +
665 + return S_OK;
666 +}
667 +CATCH_RETURN();
668 +
669 +HRESULT OnWslcVmStopping(const WSLCSessionInformation* Session)
670 +try
671 +{
672 + if (g_testType == PluginTestType::WslcVmNeverStarted)
673 + {
674 + g_logfile << "WSLC VM stopping, session=" << Session->SessionId << std::endl;
675 + return S_OK;
676 + }
677 +
678 + if (g_testType == PluginTestType::WslcVmStopCommitted)
679 + {
680 + // Only the idle teardown is interesting here. The session's final teardown races with the
681 + // session-stopping notification, which is delivered independently, so logging it would make
682 + // the expected output order-dependent on that race.
683 + if (g_stopWindowCaller.joinable())
684 + {
685 + return S_OK;
686 + }
687 +
688 + g_logfile << "WSLC VM stopping, session=" << Session->SessionId << std::endl;
689 +
690 + // Deliberately leave a live process behind when this callback returns. The stop is committed
691 + // before it is announced, so the VM goes away regardless and this process dies with it --
692 + // which is exactly what the callback was just told would happen.
693 + //
694 + // Created before the thread below is started so its exit event is published first: that thread
695 + // claims the event and must not race ahead of it.
696 + std::vector<const char*> args = {"/bin/sleep", "60", nullptr};
697 + WSLCProcessHandle leaked = nullptr;
698 + const auto hr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &leaked, nullptr);
699 + g_leakedProcess.store(leaked);
700 +
701 + // Cache the exit event while still on the thread the process proxy is marshalled to.
702 + if (SUCCEEDED(hr))
703 + {
704 + HANDLE exitEvent = nullptr;
705 + if (SUCCEEDED(g_api->WSLCProcessGetExitEvent(leaked, &exitEvent)))
706 + {
707 + g_leakedProcessExitEvent.store(exitEvent);
708 + }
709 + }
710 +
711 + g_logfile << "WSLC VM stopping leaked process: " << (SUCCEEDED(hr) ? "ok" : "failed") << std::endl;
712 +
713 + // A call from a thread this plugin owns is served on the same terms as the callback itself:
714 + // by the VM that is stopping, not by a future one and not by a new one. It must not block on
715 + // the teardown. Results are logged when this thread is joined, so the output stays deterministic.
716 + const auto sessionId = Session->SessionId;
717 + g_stopWindowCaller = std::thread([sessionId]() {
718 + std::vector<const char*> processArgs = {"/bin/true", nullptr};
719 + WSLCProcessHandle process = nullptr;
720 + g_stopWindowCallerResult = g_api->WSLCCreateProcess(sessionId, processArgs[0], processArgs.data(), nullptr, &process, nullptr);
721 + if (SUCCEEDED(g_stopWindowCallerResult))
722 + {
723 + g_api->WSLCReleaseProcess(process);
724 + }
725 +
726 + // The announced stop takes the VM away and the leaked process with it. Prove that
727 + // directly instead of inferring it from the fact that a new VM started -- a process that
728 + // outlived the stop is the exact symptom of an announced stop that did not happen.
729 + if (auto* exitEvent = g_leakedProcessExitEvent.exchange(nullptr); exitEvent != nullptr)
730 + {
731 + // GetExitEvent is marshalled as an [out, system_handle(sh_event)] parameter, so this
732 + // is a duplicate owned by this process.
733 + const wil::unique_handle owned{exitEvent};
734 + g_leakedProcessDied = WaitForSingleObject(owned.get(), 30 * 1000) == WAIT_OBJECT_0;
735 + }
736 + });
737 +
738 + // Give the thread time to issue its call inside the stop window. If it has not, the test still
739 + // passes -- it just proves less.
740 + std::this_thread::sleep_for(500ms);
741 +
742 + return S_OK;
743 + }
744 +
745 + if (g_testType != PluginTestType::WslcVmRestart)
746 + {
747 + return S_OK;
748 + }
749 +
750 + g_logfile << "WSLC VM stopping, session=" << Session->SessionId << std::endl;
751 +
752 + // Proves OnVmStopping doesn't deadlock a plugin that calls back in: on idle teardown these are
753 + // served by the VM that is still stopping and succeed; on permanent teardown they fail cleanly.
754 + std::vector<const char*> args = {"/bin/true", nullptr};
755 + WSLCProcessHandle process = nullptr;
756 + const auto processHr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, nullptr);
757 + g_logfile << "WSLC VM stopping reentrant WSLCCreateProcess: " << (SUCCEEDED(processHr) ? "ok" : "failed") << std::endl;
758 + if (SUCCEEDED(processHr))
759 + {
760 + g_api->WSLCReleaseProcess(process);
761 + }
762 +
763 + constexpr auto* mountpoint = "/test-plugin/vm-stopping-mount";
764 + const auto mountHr = g_api->WSLCMountFolder(Session->SessionId, L"C:\\", mountpoint, TRUE);
765 + if (SUCCEEDED(mountHr))
766 + {
767 + const auto unmountHr = g_api->WSLCUnmountFolder(Session->SessionId, mountpoint);
768 + g_logfile << "WSLC VM stopping mount+unmount: " << (SUCCEEDED(unmountHr) ? "ok" : "failed") << std::endl;
769 + }
770 + else
771 + {
772 + g_logfile << "WSLC VM stopping mount+unmount: skipped" << std::endl;
773 + }
774 +
775 + return S_OK;
776 +}
777 +CATCH_RETURN();
778 +
779 EXTERN_C __declspec(dllexport) HRESULT WSLPLUGINAPI_ENTRYPOINTV1(const WSLPluginAPIV1* Api, WSLPluginHooksV1* Hooks)
780 {
781 try
@@ -550,7 +787,7 @@ EXTERN_C __declspec(dllexport) HRESULT WSLPLUGINAPI_ENTRYPOINTV1(const WSLPlugin
787 THROW_HR_IF(E_UNEXPECTED, !g_logfile);
788
789 g_testType = static_cast<PluginTestType>(ReadDword(key.get(), nullptr, c_testType, static_cast<DWORD>(PluginTestType::Invalid)));
553 - THROW_HR_IF(E_INVALIDARG, static_cast<DWORD>(g_testType) <= 0 || static_cast<DWORD>(g_testType) > static_cast<DWORD>(PluginTestType::WslcImagePull));
790 + THROW_HR_IF(E_INVALIDARG, static_cast<DWORD>(g_testType) <= 0 || static_cast<DWORD>(g_testType) > static_cast<DWORD>(PluginTestType::WslcVmNeverStarted));
791
792 g_logfile << "Plugin loaded. TestMode=" << static_cast<DWORD>(g_testType) << std::endl;
793 g_api = Api;
@@ -566,6 +803,8 @@ EXTERN_C __declspec(dllexport) HRESULT WSLPLUGINAPI_ENTRYPOINTV1(const WSLPlugin
803 Hooks->ContainerStopping = &OnWslcContainerStopping;
804 Hooks->ImageCreated = &OnWslcImageCreated;
805 Hooks->ImageDeleted = &OnWslcImageDeleted;
806 + Hooks->WslcVmStarted = &OnWslcVmStarted;
807 + Hooks->WslcVmStopping = &OnWslcVmStopping;
808
809 if (g_testType == PluginTestType::FailToLoad)
810 {
test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp
+7 -2
@@ -109,9 +109,14 @@ class WSLCE2ESessionEnterTests
109 WSLC_TEST_METHOD(WSLCE2E_SessionEnter_StoragePathNotFound)
110 {
111 auto result = RunWslc(L"system session enter does-not-exist");
112 - const auto expectedPath = std::filesystem::absolute(L"does-not-exist").wstring();
112 +
113 + // The CLI resolves the storage argument to an absolute path (see EnterSession task) and the
114 + // service validates it eagerly at session creation, reporting the friendly "No WSLC session
115 + // found in '<path>'" message rather than a bare system error.
116 + const auto storagePath = std::filesystem::absolute(L"does-not-exist").wstring();
117 result.Verify({
114 - .Stderr = std::format(L"No WSLC session found in '{}'\r\nError code: ERROR_PATH_NOT_FOUND\r\n", expectedPath),
118 + .Stderr = wsl::shared::Localization::MessageWslcSessionStorageNotFound(storagePath) +
119 + L"\r\nError code: ERROR_PATH_NOT_FOUND\r\n",
120 .ExitCode = 1,
121 });
122 }
test/windows/wslc/e2e/WSLCE2EWarningTests.cpp
+17 -10
@@ -8,8 +8,9 @@ Module Name:
8
9 Abstract:
10
11 - End-to-end tests validating that warnings emitted by the WSLC COM service are
12 - surfaced on the wslc.exe CLI's stderr via the IWarningCallback integration.
11 + End-to-end tests validating how warnings emitted by the WSLC COM service are
12 + surfaced (or intentionally suppressed) on the wslc.exe CLI's stderr via the
13 + IWarningCallback integration.
14 --*/
15
16 #include "precomp.h"
@@ -72,9 +73,10 @@ class WSLCE2EWarningTests
73 CATCH_LOG()
74
75 // Injects a container with corrupt WSLC metadata into the default session's storage,
75 - // then verifies that running the wslc.exe CLI surfaces the COM service's recovery
76 - // warning on stderr.
77 - WSLC_TEST_METHOD(WSLCE2E_Warning_ContainerRecoveryPrintedOnStderr)
76 + // then verifies that running the wslc.exe CLI does not surface the COM service's recovery
77 + // warning on stderr: recovery runs outside the user's current command, so it is logged
78 + // (and written to the event log) rather than streamed back via IWarningCallback.
79 + WSLC_TEST_METHOD(WSLCE2E_Warning_ContainerRecoveryNotPrintedOnStderr)
80 {
81 std::string corruptContainerId;
82
@@ -98,15 +100,20 @@ class WSLCE2EWarningTests
100 // Terminate the default session so the next wslc command recreates it and runs recovery.
101 EnsureSessionIsTerminated();
102
101 - // Run the CLI: recovery of the corrupt container fails and the warning is printed on stderr.
103 + // Run the CLI: recovery of the corrupt container fails, but because the recovery runs
104 + // outside the user's current command, the warning is not printed on stderr.
105 auto result = RunWslc(L"container list");
106 VERIFY_IS_TRUE(result.ExitCode.has_value());
107 VERIFY_ARE_EQUAL(0u, result.ExitCode.value());
105 - VERIFY_IS_TRUE(result.Stderr.has_value());
108
107 - const auto expectedStderr = std::format(
108 - L"wsl: {}\r\n", wsl::shared::Localization::MessageWslcFailedToRecoverContainer(string::MultiByteToWide(corruptContainerId)));
109 - VERIFY_ARE_EQUAL(expectedStderr, result.Stderr.value());
109 + const auto recoveryWarning =
110 + wsl::shared::Localization::MessageWslcFailedToRecoverContainer(string::MultiByteToWide(corruptContainerId));
111 +
112 + // Assert that capture actually worked before searching it, matching the other e2e tests:
113 + // RunWslc always populates Stderr, so a missing value means capture broke and the search
114 + // below would pass without proving anything.
115 + VERIFY_IS_TRUE(result.Stderr.has_value());
116 + VERIFY_IS_TRUE(result.Stderr->find(recoveryWarning) == std::wstring::npos);
117 }
118 };
119