@samitouri / QOSAMI-WSL / commits / 2bd96563

Add WSLCVolumes class to track volumes with Docker event-driven synchronization (#40300)

* Fix X Y (#14514) * Rethink image parsing logic (#14496) * Save state * Rethink image parsing to correctly handle different reference formats * Foramt * Fix merge * Apply PR feedback * Format * Apply PR feedback * Use a regex based parser * Format * Reset the test session after termination * Fix test case * Apply PR feedback * Set container cursor (#14516) * Container cursor * Update pull image callback * Clang format * Signal wslcsdk.dll in the release pipeline (#14517) * Add volume alias (#14518) * Add volume alias * Fix Tests * Add DCAT registration remediation (#14450) * Rename image delete to remove and add aliases (#14521) * Rename image delete to remove * Clang format * Handle singular/plural units in FormatRelativeTime * Add more WSLC E2E tests (#14365) * Initial support for WSLC E2E tests * More E2E tests * More E2E tests * WIP * CP exit code fix * Enhance structure * Added E2E file * Clang format * Added E2E test * Enhance structure * Clang format * Update header comment * Update wslc path * Resolving copilot comments * Added E2E test * Pull latest * Addressed comments * Addressed comments * Clang format * Addressed comments * Load image * Addressed comments * Pull latest * Addressed comments * E2E test for wslc container delete * Addressed comments * Resolving copilot comments * Added E2E test * Added E2E test * Added E2E test * Added E2E test * Added E2E test * Added E2E test * Added E2E test * Added E2E test * Added E2E test * Fix test * Added E2E tests * Added E2E tests * Added E2E tests * Added E2E tests * Added E2E tests * Added E2E tests * Added E2E tests * Resolve copilot comment * Clang format * Replace S_OK with 0 * Resolve copilot comment * Addressed comments * Resolve copilot comment * Update test after latest changes * Implement SDK IO callbacks (#14462) Implements `WslcSetProcessSettingsIOCallback` and the use of those inputs for both initial and subsequent processes. A new thread is started to run the `MultiHandleWait` and the container/process objects hold a `shared_ptr` reference to it. The caller must keep one of the objects alive for the callback thread to keep working. * test: add support for ARM64 WSLC testing. (#14519) --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Localization change from build: 142911151 (#14535) Co-authored-by: WSL localization <noreply@microsoft.com> * Initialize environment and add E2E tests with formatting (#14475) * Init env * Added E2E tests * Clang format * WIP * Init tests * Added UT * Added more UT * Added more E2E Tests * Added more E2E Tests * Code enhancement * Added more E2E Tests * Added more E2E Tests * Added more E2E Tests * Clang format * Resolve copilot comment * Addressed comments * Fix test * Addressed comments * merge master -> feature/wsl-for-apps (#14537) * test: enable virtiofs tests and enable WSLG during testing (#14387) * test: enable virtiofs tests and enable WSLG during testing * test fix --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * chore(distributions): Almalinux auto-update - 20260311 14:52:02 (#14404) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Fix CVE-2026-26127: bump .NET runtime from 10.0.0 to 10.0.4 (#14421) Addresses Dependabot alerts #10 and #11. The Microsoft.NETCore.App.Runtime packages (win-x64 and win-arm64) at version 10.0.0 are vulnerable to a denial of service via out-of-bounds read when decoding malformed Base64Url input (CVSS 7.5 High). Bumped to 10.0.4 which includes the fix. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Notice change from build: 141806547 (#14423) Co-authored-by: WSL notice <noreply@microsoft.com> * Ship initrd.img in MSI using build-time generation via powershell script (#14424) * Ship initrd.img in MSI using build-time generation via tar.exe Replace the install-time CreateInitrd/RemoveInitrd custom actions with a build-time step that generates initrd.img using the Windows built-in tar.exe (libarchive/bsdtar) and ships it directly in the MSI. The install-time approach had a race condition: wsl.exe could launch before the CreateInitrd custom action completed, causing ERROR_FILE_NOT_FOUND for initrd.img. Changes: - Add CMake custom command to generate initrd.img via tar.exe --format=newc - Add initrd.img as a regular file in the MSI tools component - Remove CreateInitrd/RemoveInitrd custom actions from WiX, DllMain, and wslinstall.def - Remove CreateCpioInitrd helper and its tests (no longer needed) - Update pipeline build targets to build initramfs instead of init * pr feedback * more pr feedback * switch to using a powershell script instead of tar.exe * powershell script feedback * hopefully final pr feedback --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * virtiofs: update logic so querying virtiofs mount source does not require a call to the service (#14380) * virtiofs: update logic so querying virtiofs mount source does not require a call to the service * more pr feedback * use std::filesystem::read_symlink * pr feedback and use canonical path in virtiofs symlink * make sure canonical path is always used --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * virtio networking: add support for ipv6 (#14350) * VirtioProxy: Add IPv6 address, gateway, and route support - Add PreferredIpv6Address field and GetBestGatewayV6* methods to NetworkSettings - Extend GetHostEndpointSettings() to discover IPv6 unicast address and gateway - Add UpdateIpv6Address() using ModifyGuestEndpointSettingRequest<IPAddress> - Push IPv6 default route to guest via UpdateDefaultRoute(AF_INET6) - Remove AF_INET6 early return in ModifyOpenPorts, use INETADDR_PORT() - Add EndpointRoute::DefaultRoute() static factory - Pass client_ip_ipv6 in devicehost options (not yet parsed by devicehost) - Remove gateway_ip from devicehost options (only needed for DHCP) - Include IPv6 DNS servers in non-tunneling DNS settings - Add ConfigurationV6 and DnsResolutionAAAA tests * cleanup and add more ipv6 tests * added test coverage and minor updates * clang format * pr feedback * format source * pr feedback * test fixes --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Track `bind` syscall when port is 0 (#14333) * Initial work * . * pr feedback and add unit test * minor tweaks an fix use after free in logging statement * implement PR feedback * hopefully final pr feedback * pr feedback in test function * Address PR feedback: add try/catch to TrackPort and PortZeroBind queue push --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Add iptables to list of apps to install in WSL (#14459) There were instructions already on how to install tcpdump in WSL, but iptables are also needed for the log collection to be complete, so this PR adds instructions on how to also install iptables. Co-authored-by: Andre Muezerie <andremue@linux.microsoft.com> * Update Microsoft.WSL.DeviceHost to version 1.1.39-0 (#14460) Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Moves all Ubuntu distros to the tar-based format (#14463) * Move all supported Ubuntu images to the new format We backported the build pipeline so all current LTSes come out in the new tar-based format * Remove the appx based distros All WSL users can run tar-based distros by now, right? There is no benefit in maintaining both formats. * Enable DNS tunneling for VirtioProxy networking mode (#14461) - Allow VirtioProxy to keep EnableDnsTunneling=true in config, but clear socket-specific options (BestEffortDnsParsing, DnsTunnelingIpAddress) - Suppress dedicated DNS tunneling hvsocket for VirtioProxy; tunneling is handled through the VirtioNetworking device host instead - Set DnsTunneling flag on VirtioNetworkingFlags so the device host knows to tunnel DNS - Expand SWIOTLB kernel cmdline to cover VirtioFs and VirtioProxy - Bump DeviceHost package to 1.1.39-0 - Add VirtioProxy DNS test coverage for tunneling on/off - Skip GuestPortIsReleasedV6 on Windows 10 Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * test: disable LoopbackExplicit due to OS build 29555 regression (#14477) Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Refactor: trim unnecessary DLL deps from COMMON_LINK_LIBRARIES (#14426) * Refactor: trim unnecessary DLL deps from COMMON_LINK_LIBRARIES - Split MSI/Wintrust install functions from wslutil.cpp into install.cpp - Remove MI.lib, wsldeps.lib, msi.lib, Wintrust.lib, computecore.lib, computenetwork.lib, Iphlpapi.lib from COMMON_LINK_LIBRARIES - Add per-target MSI_LINK_LIBRARIES, HCS_LINK_LIBRARIES, SERVICE_LINK_LIBRARIES - Delay-load msi.dll and WINTRUST.dll for wsl.exe and wslg.exe - Result: wslhost, wslrelay, wslcsdk, testplugin lose msi/wintrust startup imports; wsl.exe and wslg.exe defer msi/wintrust loading until actually needed; wslservice is the only target that imports computecore/computenetwork/Iphlpapi * minor fixes to install.cpp that were caught during PR * move to wsl::windows::common::install namespace --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Fix wsl stuck when misconfigured cifs mount presents (#14466) * detach terminal before running mount -a * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * use _exit on error before execv in child process to avoid unintentional resource release * Add regression test * Fix clang format issue * fix all clang format issue * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * resolve ai comments * move test to unit test * Fix string literal * Overwrite fstab to resolve pipeline missing file issue --------- Co-authored-by: Feng Wang <wangfen@microsoft.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Update localization and notice scripts to target the branch that the pipeline is running on (#14492) * test: Add arm64 test distro support (#14500) * test: Add arm64 test distro support * update unit test baseline * more test baseline updates --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * test: remove duplicated DNS test coverage (#14522) * test: remove duplicated DNS test coverage * format source --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Fix: Fail and warn the user when --uninstall is given parameters (#14524) Fail and warn the user when --uninstall is given parameters. * Localization change from build: 142847827 (#14525) Co-authored-by: WSL localization <noreply@microsoft.com> * virito net: revert to previous DNS behavior while we debug an issue with DNS over TCP (#14532) Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * devicehost: update to latest devicehost nuget with tracing improvements (#14531) Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * fix merge issues --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: AlmaLinux Autobot <107999298+almalinuxautobot@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Blue <OneBlue@users.noreply.github.com> Co-authored-by: WSL notice <noreply@microsoft.com> Co-authored-by: Daman Mulye <daman_mulye@hotmail.com> Co-authored-by: Andre Muezerie <108841174+andremueiot@users.noreply.github.com> Co-authored-by: Andre Muezerie <andremue@linux.microsoft.com> Co-authored-by: Carlos Nihelton <carlos.santanadeoliveira@canonical.com> Co-authored-by: Feng Wang <wang6922@outlook.com> Co-authored-by: Feng Wang <wangfen@microsoft.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * cleanup: rename wsla -> wslc (#14502) Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * [CLI] Added support for entrypoint (#14552) * Added entrypoint * Enhance tests * Initial wslc settings support (#14548) * Various improvements to interactions with user-provided handles (#14486) * Save state * Add test coverage * Check the event under the lock * Apply PR feedback * Format * Apply PR feedback * Various improvements to PullImage() + custom registry support (#14549) * Save state * Save state * Handle issues during pull * Disable the pull tests * Update the CLI tests * Format * Fix legacy index logic * Add test coverage for failed PullImage() * Update test/windows/WSLCTests.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Use pre-imported container images in the tests to avoid hitting the API limit (#14561) * Use pre-imported container images in the tests to avoid hitting the API limit * Fix test case * Update test/windows/wslc/e2e/WSLCE2EImageBuildTests.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Correctly set FileOffsets in WriteHandle (#14562) * Correctly set FileOffsets in WriteHandle * Apply PR suggestions * Update Microsoft.WSL.DeviceHost to version 1.1.48-0 (#14575) Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Allow retrieving state and init process info after container auto remove (#14429) Allow clients to continue reading stdout/stderr and querying state from containers after they've been deleted, by caching the read-only data in the com wrapper. * Fix StateChangedAt mismatch during container recovery from storage (#14482) * Use Docker's FinishedAt timestamp in Transition() to fix StateChangedAt mismatch during container recovery * Extract GetDockerFinishedAt() helper * Address Feedback * Fail if Docker event time is missing * Address copilot feedback * Use Docker stop event timestamp instead of InspectContainer() * Apply copilot feedback * PR Feedback * [WSLC] add WSLC CMake file in NuGet (#14543) Add WSLC SDK Config.cmake file. Enable developers to consume the library with CMake. * win10: remove IsWslSupportInterfacePresent check from wslservice (#14546) Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Fix localization and notice pipelines to use full branch name (#14566) Build.SourceBranchName only returns the last segment after '/', breaking branches with slashes (e.g. user/benhill/loc_fix -> loc_fix). Use Build.SourceBranch with refs/heads/ stripped instead. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Remove the CTests targets from yaml-cpp (#14563) * Fix wrong enum type in DeleteExclusiveLockHeld (#40030) WSLCDeleteImageFlagsForce (from WSLCDeleteImageFlags enum) was used instead of WSLCDeleteFlagsForce (from WSLCDeleteFlags enum). Both values are currently 1, so no functional issue today, but this is a type confusion that will break silently if either enum changes. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Fix Content-Length truncation for Docker responses >4GB (#40033) std::stoul returns unsigned long (32-bit on Windows), truncating Content-Length values >4GB. This corrupts the socket stream for large image save/export operations. Replace with std::stoull to parse as 64-bit unsigned. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * wslc: resolve ContainerRecoveryFromStorage test issue by storing timestamp from docker, not the host (#40038) Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Reject port mappings exceeding 63-port limit in relay (#40035) WaitForMultipleObjects has a 64-handle limit. The accept thread uses 1 handle for the exit event, leaving room for 63 port mappings. Reject new port mappings with ERROR_TOO_MANY_OPEN_FILES when the limit is reached, rather than crashing the relay thread. Existing mappings continue to work. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Add networking mode setting as fallback (#14564) * Fix partial write data loss in TTY stdin relay (#40032) When the TtyMaster fd is non-blocking, write() can return fewer bytes than requested (partial write). The existing code only handled the EAGAIN/EWOULDBLOCK case by buffering into pendingStdin, but silently dropped data on successful partial writes. Add handling for 0 < bytesWritten < bytesRead to buffer the unwritten bytes into pendingStdin, matching the existing retry logic. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Fix deadlock between WatchForExitedProcesses and VMProcessControl destructor (#14567) Use weak_ptr in m_trackedProcesses so WatchForExitedProcesses can safely lock a reference and call OnExited() outside m_trackedProcessesLock. weak_ptr avoids a circular reference: the vector no longer prevents VMProcessControl destruction, so the destructor's OnProcessReleased cleanup runs normally. Expired entries are also pruned in OnProcessReleased. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Clean up Docker container on Create() failure (#40034) * Clean up Docker container on Create() failure If InspectContainer or the WSLCContainerImpl constructor throws after CreateContainer succeeds, the Docker container is permanently orphaned with no way to manage it through WSLC APIs. Add a scope_exit to delete the Docker container on failure, released on success. * pr feedback (capture by value) --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * CLI: Implement --session argument and session targeting (#14368) * Remove options that are not yet implemented (#40044) * Add cancellation to image build (#14453) * wslc: enable virtiofs by default for wslc.exe and wslcsdk (#14559) Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * wslc: fix TOCTOU race in cached COM wrapper getters (#40039) GetState/GetInitProcess/GetId/GetName could race with DisconnectComWrapper: the cache check saw empty, then disconnect populated the cache and nulled the impl, so the forwarded call failed with RPC_E_DISCONNECTED even though the cached value was now available. Fix by removing the upfront cache check and instead calling through to the impl first. If the call fails with RPC_E_DISCONNECTED, fall back to the cache which DisconnectComWrapper guarantees is populated. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Validate empty host path in VolumeMount::Parse (#40031) * Validate empty host path in VolumeMount::Parse When parsing volume mount specs without a mode suffix (e.g. ':container'), the host path validation was skipped, producing a VolumeMount with an empty host path. This would propagate to MountWindowsFolder('') and produce a confusing error. Add validation after computing m_hostPath to reject empty host paths with a clear error message in all cases. * add test coverage --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Update wslc build to look for Containerfile and Dockerfile (#40045) * Save state * Add test coverage * Rename method * Require input handle in the service * Apply PR feedback * Format * Apply PR feedback + update tests * Format * Implement WSLAContainer::Kill() (#14560) * Implement WSLAContainer::Kill() * Add test coverage * Format * Apply PR feedback * Fix a race in `container start -a` for short-lived containers. (#14558) * Localization change from build: 143374899 (#40049) Co-authored-by: WSL localization <noreply@microsoft.com> * [CLI] Add initial support for image saving feature (#14557) * Initial support for image save * Added e2e tests * Added session option * Clang format * Clang format * Addressed comments * CLang format * Change RegistryAuthenticationInformation to optional string (#40053) * Implement SDK CanRun (#14539) Implements `WslcCanRun` using existing primitives. Updates the component flag names to help better describe their meaning and adds comments to further do so. Also changes the error returned when we cannot create a `WSLASessionManager` and WSL is installed at a version below the minimum for WSLC support. * CLI: Create elevated and non-elevated sessions (#40047) * wslc Pretty-print JSON output for inspect and list commands (#40051) * Fix race condition in ContainerLogs test case (#40052) * Add support for build logs (#40054) * CLI: Table Output updates (#14534) * Use COM marshalling to exchange handles (#40056) * Prototype * Save state * Save state * Save state * Save state * Save state * Save state * Remove zeroing * Apply PR feedback * Add test coverage * Format * Add test coverage for null handles * wslc: ensure dns tunneling is disabled with virtioproxy networking mode (#40057) Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Implement SDK inspect and VHD management (#14545) Implements `WslcInspectContainer` and `WslcCreateSessionVhd`; adds `WslcSetContainerSettingsNamedVolumes` so that the VHDs are useful. * CLI: Add Terminate Session command (#40058) * List and inspect named volumes (#14555) * CLI: Add table support to always show header, default true (#40061) * Keep track of handle types in the service (#40062) * Keep track of handle types in the service * Only duplicate on return * Format * Switch DmesgOutput to a WSLCHandle (#40063) * Add repository, tag, ID and created columns to image list output (#40043) * Add port mappings to container list output (#14438) * HostFileShareMode (#40068) * Fix and improve interactive tests (#40069) * Add a DllMain method to wslcsdk and configure tracelogging (#40070) * Add a DllMain method to wslcsdk and configure tracelogging * Explicitely initialize WIL * Fix race condition in ElevatedTokenCanOpenNonElevatedHandles test case (#40071) * Suppress MSI-initiated reboots during Store updates (#40074) When the WSL MSIX package is updated via the Microsoft Store, the WslInstaller service automatically upgrades the MSI package by calling MsiInstallProduct. This call was made with INSTALLUILEVEL_NONE (silent install) but without setting the REBOOT=ReallySuppress property. Per Windows Installer documentation, when a silent install encounters files in use and REBOOT is not suppressed, the system reboots automatically without any user prompt. This could cause unexpected machine restarts after a Store update when WSL binaries (e.g. wslservice.exe) were in use during the upgrade. Every deployment script in the repo already passes /norestart to msiexec (deploy-to-host.ps1, deploy-to-vm.ps1, install-latest-wsl.ps1, test-setup.ps1), but the programmatic MsiInstallProduct path used by the WslInstaller service lacked the equivalent property. This change: - Always appends REBOOT=ReallySuppress to MsiInstallProduct arguments in UpgradeViaMsi, preventing Windows Installer from ever initiating a system restart during install/upgrade. - Switches UninstallViaMsi from MsiConfigureProduct to MsiConfigureProductEx so we can pass REBOOT=ReallySuppress during uninstall as well. - Propagates ERROR_SUCCESS_REBOOT_REQUIRED (3010) to callers instead of swallowing it. User-facing paths (wsl --update, wsl --uninstall) print a reboot-needed message to stderr. The background WslInstaller service silently treats 3010 as success since it has no console. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Don't fail to start a WSLCSession if anonymous volumes are present (#40077) * Don't fail to start a WSLCSession if anonymous volumes are present * Typo * Cleanup diff * Cleanup diff * devicehost: stop re-signing and rely on MSIRMSHUTDOWN for file-in-use handling (#40080) * devicehost: stop re-signing and fix MSI installer failing to replace wsldevicehost.dll * pr feedback * use MSIRMSHUTDOWN = 1 instead of custom action --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Use THROW_HR_WITH_USER_ERROR for TTY console check (#40084) Replace manual PrintMessage + THROW_HR(E_FAIL) with the standard THROW_HR_WITH_USER_ERROR pattern used throughout the codebase. This ensures the error message is properly captured by the ExecutionContext error collection system rather than being printed directly to stderr. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Add week/month/year support to FormatRelativeTime (#40083) * Add week/month/year support to FormatRelativeTime Previously only handled seconds, minutes, hours, and days. Containers older than a week would show e.g. '720 days ago' instead of '2 years ago'. Now matches Docker-style output with weeks, months, and years. Also refactors repeated pluralization logic into a lambda. * Handle timestamp==0 sentinel in FormatRelativeTime Callers pass 0 to mean 'unknown' (e.g. ImageTasks when Created <= 0). Previously this would produce '56 years ago'. Now returns empty string. --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Remove dead code: SetContainerTTYOptions and SetContainerArguments (#40087) Both static functions in ContainerService.cpp are defined but never called from anywhere in the codebase. They appear to be remnants of an earlier implementation before the WSLCContainerLauncher and WSLCProcessLauncher classes took over this responsibility. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Fix callback parameter shadow in CreateInternal (#40086) * Remove unused callback parameter from CreateInternal The IProgressCallback* parameter was always passed as nullptr by both callers. The auto-pull path creates its own PullImageCallback locally. Remove the dead parameter entirely per review feedback. * format --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Allow small timestamp difference in ContainerRecoveryFromStorage test (#40081) * Allow small timestamp difference in ContainerRecoveryFromStorage test * feedback * Localization change from build: 143632468 (#40094) Co-authored-by: WSL localization <noreply@microsoft.com> * Release COM DLLs before installer test MSI operations (#40093) * Release COM DLLs before installer test MSI operations Add PrepareForMsiOperation() that calls CoFreeUnusedLibrariesEx(0) before each msiexec invocation. This releases in-process COM DLLs like wslserviceproxystub.dll loaded by prior test classes, preventing the Restart Manager from detecting the test process as holding file locks and failing the install on older Server SKUs like ni_release. * fix CoFreeUnusedLibrariesEx --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Localize all user-facing wslc.exe CLI strings (#40089) * Localize all user-facing wslc.exe CLI strings Add localization entries for all hardcoded user-facing strings in the WSLC CLI tool: - 38 command description strings (Short + Long for all commands) - 30 argument description strings in ArgumentDefinitions.h - Settings reset confirmation string - All entries added to en-US/Resources.resw with {Locked=...} comments for CLI flags, product names, and technical terms E2E tests updated to use Localization:: calls and dynamic column formatting instead of hardcoded string copies, so they won't go stale when descriptions change. * formatting --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Validate container path is absolute in VolumeMount::Parse (#40085) * Validate container path is absolute in VolumeMount::Parse Add validation that non-empty container paths must start with '/' since they are Linux paths inside the container. This catches cases where Windows drive letter colons (e.g. C:\path) get misinterpreted as the host:container separator, producing invalid container paths like '\hostPath' instead of '/containerPath'. Previously, 'C:\hostPath:ro' would silently parse as host='C', container='\hostPath', mode=ro ΓÇö now it throws a clear error. Updated tests to reflect the new validation and moved previously 'valid' but semantically incorrect cases to the invalid test set. * Also validate host path is absolute, add format hint to error, cover forward-slash cases - Reject non-absolute host paths (catches C:/hostPath where host='C') - Add 'Expected format:' hint to container path error message for consistency - Add forward-slash drive letter cases to invalid test set - Move '::' case to invalid (host=':' is not absolute) * Remove host path is_absolute check to allow future named volumes Per review feedback: named volumes (e.g. 'test_vol:/path') would fail an is_absolute() check on the host path. Keep only the container path validation (must start with '/') which doesn't conflict with named volume identifiers. --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Localization change from build: 143644543 (#40098) Co-authored-by: WSL localization <noreply@microsoft.com> * Initialize user option support and added E2E tests (#40101) * Init user option support * Init e2e test * Added more E2E Tests * Fix test * Clang format * Fix test * Rename test to UnknownGroup_Fails to reflect actual failure mode Agent-Logs-Url: https://github.com/microsoft/WSL/sessions/0e095692-5673-4e8f-ad7f-47deccb74ec9 Co-authored-by: AmelBawa-msft <104940545+AmelBawa-msft@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> * Add logic to clean up images created during the tests (#40078) * Save state * Format * Log failures * Format * Add response size limit to Docker HTTP client (#40097) SendRequestAndReadResponse accumulates the entire response body into a std::string with no size limit. While all current callers expect small JSON metadata responses, a pathological or malformed response could cause unbounded memory growth. Add a 64 MB limit to prevent OOM. This is generous for JSON metadata (list/inspect/create responses are typically <1 MB) while still catching runaway responses early. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * [WSLC] Add --workdir / -w option to 'wslc exec' (#40041) * [WSLC] Add --workdir / -w option to 'wslc exec' Adds a --workdir (-w) argument to the exec command that sets the working directory inside the container for the executed process. Wires the value through ContainerOptions into WSLAProcessLauncher::SetWorkingDirectory. Co-authored-by: Pooja Trivedi <trivedipooja@microsoft.com> Co-Authored-By: Claude Sonnet 4.6 * Update test/windows/wslc/CommandLineTestCases.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update test/windows/wslc/CommandLineTestCases.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix clang formatting issues * Update test/windows/wslc/WSLCCLIExecutionUnitTests.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add E2E tests for wslc container exec, including --workdir option - Port existing exec E2E tests from feature branch - Add WSLCE2E_Container_Exec_WorkDir and WSLCE2E_Container_Exec_WorkDir_ShortAlias tests - Update help message in GetAvailableOptions to include -w,--workdir Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix clang formatting in WSLCE2EContainerExecTests.cpp Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Validate --workdir is non-empty; add unit and parse test cases - Reject empty or whitespace-only --workdir in Argument::Validate - Add ExecCommand_ParseWorkDirEmptyValue_ThrowsArgumentException unit test - Add empty-workdir failing case to CommandLineTestCases.h Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix clang formatting in CommandLineTestCases.h Co-Authored-By: Claude Sonnet 4.6 * Trim exec E2E tests to --workdir coverage only Remove tests that duplicate existing coverage in WSLCE2EContainerCreateTests.cpp. Keep only the help message test (validates --workdir appears in output) and the two workdir-specific E2E tests. Co-Authored-By: Claude Sonnet 4.6 * Missed change from merge conflict resolution * Fix --workdir whitespace validation to use std::iswspace for full Unicode coverage Agent-Logs-Url: https://github.com/microsoft/WSL/sessions/b21d1a57-bb3f-4a12-84cf-8e414a453890 Co-authored-by: ptrivedi <1638019+ptrivedi@users.noreply.github.com> * Use lambda with wint_t cast in iswspace call to avoid potential UB Agent-Logs-Url: https://github.com/microsoft/WSL/sessions/b21d1a57-bb3f-4a12-84cf-8e414a453890 Co-authored-by: ptrivedi <1638019+ptrivedi@users.noreply.github.com> * Missed change from merge conflict resolution * Address Copilot PR feedback - Revert Version ArgType alias from NO_ALIAS back to L"v" to preserve existing -v short option - Restore WSLCE2EContainerExecTests.cpp lost in merge conflict resolution Co-Authored-By: Pooja Trivedi * Update src/windows/wslc/services/ContainerService.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix ParserTest_StateMachine_PositionalForward: replace -v with -h in flag parse tests The -v short alias was removed from --verbose (changed to NO_ALIAS) to resolve a triple alias conflict with --version and --volume. The parser test cases in the Run argument set still used -v expecting it to resolve to --verbose, but since neither Version nor Volume are in the Run set, -v became unresolvable and caused unexpected parse failures. Replace -v with -h (help flag) in the flag parse test cases to preserve the same combined-flag parsing coverage with a valid short alias. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix E2E exec help test: add --user option after base branch merge After merging feature/wsl-for-apps, the --user argument is now active in ContainerExecCommand (from PR #40101). Update the expected exec help output to include -u,--user. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Pooja Trivedi <trivedipooja@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ptrivedi <1638019+ptrivedi@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Implement wslc session enter (#40088) * Save state * Save state * Save state * Cleanup * Prepare for PR * Cleanup * Validate flags * Apply PR suggestions * Add comment * Remove include * Cleanup diff * Format * Format * Restore session flags * Make VMPortMapping::Unmap idempotent (#40091) * Make VMPortMapping::Unmap idempotent Null out Vm after unmapping so a second call is a no-op. This prevents double-unmap errors when ReleaseRuntimeResources is called from both OnEvent(Stop) and a concurrent Delete(Force) path. * Clear Vm before re-throwing on UnmapPort failure Use a scope_exit to null Vm so that Unmap() is truly idempotent even when UnmapPort() throws. Previously Vm stayed non-null on the failure path, causing subsequent Unmap() calls to retry and throw again. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Correctly handle empty arguments & entrypoint when launching containers (#40122) * Correctly handle empty arguments & entrypoint when launching containers * Apply PR feedback * Add logic to reject invalid flags (#40100) * Add logic to reject invalid flags * Simplify tests * Use macros * Format * Format * wslc: implement dns tunneling for virtio proxy networking mode (#40104) * wslc: implement dns tunneling for virtio proxy networking mode This change implements dnstunneling for the virtio proxy networking mode. For now, this implementation uses the same socket-based approach, but in the fututure this will be moved over to the built-in dns support that is part of the wsldevicehost dll. * wslc: enable DNS tunneling by default for virtio proxy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove unused DNS test helpers VerifyDnsResolutionDig and VerifyDnsResolutionRecordTypes are dead code — VerifyDnsQueries is the superset that covers all record types and TCP/UDP modes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [WSLC] Add 'wslc version' subcommand (#14547) * [WSLC] Add 'wslc version' subcommand Adds a 'version' subcommand to the WSLC CLI as an alternative to the existing '--version' flag, following the subcommand pattern used by other WSLC commands. Includes unit tests for command structure and command-line parsing. Co-authored-by: Pooja Trivedi <trivedipooja@microsoft.com> Co-Authored-By: Claude Sonnet 4.6 * [WSLC] Fix E2E help/invalid-command tests after version subcommand addition Update expected help output in WSLCE2EGlobalTests to include the newly added 'version' subcommand, fixing WSLCE2E_HelpCommand and WSLCE2E_InvalidCommand_DisplaysErrorMessage test failures. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [WSLC] Add E2E version command test and strengthen unit test - Add WSLCE2E_VersionCommand E2E test in WSLCE2EGlobalTests verifying stdout, empty stderr, and exit code for 'wslc version' - Add assertion to VersionCommand_HasNoArguments unit test to verify only the auto-added --help argument is present Authored-By: Pooja Trivedi <trivedipooja@microsoft.com> Co-Authored-By: Claude Sonnet 4.6 * Mark 'context' as UNREFERENCED_PARAMETER in VersionCommand::ExecuteInternal to avoid build issues * Address PR feedback on wslc version command - Use Localization::WSLCCLI_VersionDesc/LongDesc instead of hard-coded strings; add entries to en-US Resources.resw - Centralize version printing in VersionCommand::PrintVersion(); reuse from RootCommand --version flag - Drop 'v' prefix from version output per OneBlue's feedback - Add 'version' entry to E2E GetAvailableCommands() to fix WSLCE2E_HelpCommand and WSLCE2E_InvalidCommand_DisplaysErrorMessage Co-Authored-By: Pooja Trivedi * Fix build: add missing using namespace wsl::shared Localization::WSLCCLI_VersionDesc() and WSLCCLI_VersionLongDesc() live in wsl::shared::Localization. Without this using directive the compiler cannot resolve the unqualified Localization:: references in VersionCommand.cpp. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Pooja Trivedi <trivedipooja@microsoft.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add E2E test for --version flag (#40125) * Add E2E test for --version flag Ensures both entry points (the 'version' subcommand and the '--version' flag) are covered by E2E tests, preventing the two paths from drifting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix newline --------- Co-authored-by: Pooja Trivedi <trivedipooja@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update WSLCCreateSession event to critical. (#40065) * Updated event to critical * Updated event * Apply suggestion from @craigloewen-msft * Revert "Apply suggestion from @craigloewen-msft" This reverts commit 7800a3558725c71ef58cb5229e74e98d97fa16b3. * [CLI] Initialize tmpfs support (#40128) * Init tmpfs support * Resolve copilot comment * Fail the session creation if the virtionet interface can't be initialized (#40124) * Fail the session creation if the virtionet interface can't be initialized * Apply PR feedback * Add DCOM LaunchPermission for WSLCSessionFactory (#40134) * Add DCOM LaunchPermission for WSLCSessionFactory WSLCSessionFactory (wslcsession.exe) was registered without an AppId or explicit DCOM LaunchPermission. When wslservice (SYSTEM) impersonates a non-interactive caller such as Network Service and calls CreateComServerAsUser to launch the per-user session factory, DCOM falls back to machine-default launch permissions which do not include service accounts. This causes E_ACCESSDENIED during session creation. Add an AppId with the same LaunchPermission and AccessPermission SDDL used by the other WSL COM servers (LxssUserSession, WSLCSessionManager, WslDeviceHost), granting launch/activate rights to Authenticated Users (AU), Principal Self (PS), and Local System (SY). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Extract DCOM permission blob into CMake variable The same binary security descriptor was duplicated 8 times across 4 AppId registrations. Extract it into a DCOM_PERMISSION CMake variable defined in msipackage/CMakeLists.txt so future permission changes only need one edit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix race condition when building multiple IDL files (#40130) * Fix race condition when building multiple IDL files * Use the header as output * Use a single target * Use a separate command to set the stamp file * Remove stamp file * CLI: Fix forwarded args beginning with '-' from being a parser error (#40131) * Refactor tests: use TAEF metadata for WSL version filtering (#40129) Replace runtime WSL2_TEST_ONLY()/WSL1_TEST_ONLY() skip macros with TAEF metadata-based test selection. Tests that don't apply to the current WSL version are now excluded by /select: queries at selection time rather than skipped at runtime, eliminating hundreds of 'skipped' results from test output. Changes: - Add WSL2_TEST_METHOD, WSL1_TEST_METHOD, WSLC_TEST_METHOD macros in Common.h that tag tests with WSLVersion metadata property - Convert ~430 test methods across 26 files to use new macros - Update run-tests.ps1 to auto-add /select: version filter - Update CloudTest XML configs with version selection queries - Remove WSL2_TEST_ONLY() from composite macros in NetworkTests.cpp - Update test README with new macro documentation Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix tmpfs tests to use WSLC_TEST_METHOD macro (#40143) Replace WSL2_TEST_ONLY() runtime skip macro (removed in #40129) with WSLC_TEST_METHOD metadata macro in tmpfs tests added by #40128. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove cloudtest notifications sent to lowdev (#40046) * Remove build notifications sent to lowdev * Use GH email * Use VSO email * Log email * Iterate * Remove notification entirely * CLI: Add initial support for image tag command (#14416) * Initial support for image tag command * Init test * Init e2e test * Adde E2E tests * Added more tests * Added more tests * Resolve copilot comment * Clang format * Clang format * Fix build * Update parser * Update loc * Fix test * Added more tests * Clang format * Loc * Addressed comments * Add support for mounting files and non-existing folders as volumes (#40137) * Save state * Prepare for PR * Apply PR feedback * Add test coverage for restored containers * Merge * Reset m_dockerdProcess when terminating a session (#40146) * Fix incorrect --detach description in container run help text (#40149) The help text for 'wslc container run' incorrectly stated that containers start in the background by default and --detach runs in the foreground. The actual behavior (confirmed by ContainerService.cpp) is the opposite: containers start attached (foreground) by default, and --detach runs them in the background. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add preview disclaimer to WSLC SDK header and NuGet package (#40153) Add preview notice to wslcsdk.h header comment block, NuGet README, and nuspec description to clearly communicate that the WSLC SDK API is subject to breaking changes during the preview period. Also fix incorrect header filename reference in README (WSLCApi.h -> wslcsdk.h). Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix API review issues in wslcsdk.h and wslcsdk.cpp (#40150) - Add missing SAL annotations to WslcGetContainerID (_In_, _Out_writes_) - Fix typo: 'recieve' -> 'receive' in WslcStdIOCallback documentation - Fix parameter naming: 'NameOrId' -> 'nameOrId' to match lowercase convention used by all other API parameters Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add wslcsession.exe to diagnostic log collection (#40151) Include the WSLC session service process in the list of executables that collect-wsl-logs.ps1 captures memory dumps for. This improves diagnostics for container-related issues where the session service state is relevant. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Initial prune image support in wslc service (#40132) * Improve errors returned from WSLCContainer (#40147) * Improve container state errors * Add wslc.exe test coverage * Fix localization file * Apply PR suggestions * Format * Add negative test cases for WSLC SDK error paths (#40152) Add 10 new WSLC SDK tests covering critical error handling gaps: - Null handle release/terminate (session, container, process) - Container creation with null session - Stop container with invalid signal value - Exec process on stopped container - Duplicate container name rejection - Delete running container without force flag - Delete non-existent image - Pull with invalid image name/registry URL These tests verify that the SDK properly rejects invalid inputs and returns appropriate error codes rather than crashing. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix SDK tests to use new WSLC_E_CONTAINER error codes (#40165) * Enhance and organize test execution (#40108) * container attach * Enhance tests * Move more tests to run * Init exec * Clang format * Resolve copilot comment * Replace S_OK with 0 * Added error messages * Resolve copilot comment * Clang format * Fix tests * Resolve copilot comment * Fix incorrect regeneration for IDL targets (#40148) * Add registry authentication in runtime (#40123) * wslc: prevent session name squatting for default WSLc sessions (#40144) * Prevent session name squatting for default WSLc sessions - Server now determines default session name and settings from caller's token, preventing malicious users from squatting reserved session names - CreateSession rejects explicit use of reserved names (wslc-cli, wslc-cli-admin) with case-insensitive E_ACCESSDENIED check - Null StoragePath remains valid for ephemeral sessions; empty string is rejected as E_INVALIDARG - Add dedicated EnterSession API with null/empty parameter validation - Early return optimization: skip YAML parse if default session exists - Inline BuildFeatureFlags into SessionSettings constructor - Extract UserSettings into shared wslcsettings library used by both wslc.exe and wslservice.exe - Move EnumVariantMap.h to common - Fix std::terminate crash in CustomDmesgOutput test when CreateSession fails by adding a scope_exit guard to join the reader thread - Add session name squatting E2E test with case-insensitive coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR feedback: per-user session names, custom error codes, impersonation - Append username to default session names so different users don't collide (e.g. wslc-cli-alice, wslc-cli-admin-bob) [OneBlue] - Impersonate caller when loading settings.yaml server-side [OneBlue] - Factor name resolution into ResolveDefaultSessionName helper [OneBlue] - Add WSLC_E_SESSION_RESERVED and WSLC_E_INVALID_SESSION_NAME error codes for better diagnosability [dkbennett] - Use prefix-based reserved name check (blocks all wslc-cli-* names) - Fix pre-existing HostFileShareMode namespace qualification bug - Remove unused variable in GetDefaultStoragePath test helper [Copilot] - Update all E2E tests for username-qualified session names Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix port conflict error message and cleanup on failure (#40102) * Don't return catch-all error messages for non-WSL specific error codes (#40163) * Don't return catch-all error messages for non-WSL specific error codes * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update test error message expectations to match actual behavior - ImportDistro: Remove 'Failed to create disk' wrapper since the error occurs before CreateVhd, producing only the raw system error message - ModernDistroInstall: Update to raw Win32 message for ERROR_ALREADY_EXISTS since the mapping was removed from GetErrorString and InstallDistro path doesn't use THROW_HR_WITH_USER_ERROR Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update test messages * Format * Update localization/strings/en-US/Resources.resw Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply new string name * Use the right error message for wsl1 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Richfr/ Add windowsAddress support to WslcCreateContainer() API (#40037) * Portmapping: add windowsAddress support * Portmapping: add windowsAddress support * Addressing PR feedback: update wslcsdk.cpp * Addressing clang formatting error * Providing default values for port bindings: wslcsdk.cpp * Added new function InetNtopToHresult() to map inet result error values to the correct HRESULTS * return family name if socket address is invalid * Removed unnecessasry variable bindingAddressStrings. Also, commented that convertedPorts must stay in same scope as containerOptions and moved the declaration to be with that var * Fixed clang formatting errors * Added WSLC Sdk API tests for WslcContainerPortMapping.windowsAddress for Ipv4 and Ipv6 local host. * Add unique name to containerSettings var to make test debugging from log reports easier. * Adjust AF_UNIX portmapping test so that it fails if value accepted * Return more accurate strncpy_s error result * Improved error return value of IP address verification * Provide better error reporting * Respond to misc copilot feedback * Resolving merge after syncing with origin * Adjusted error messages * Removed 2 unnecessary lines of code * Mark internal function as static to avoid external linkage --------- Co-authored-by: Richard Fricks <richfr@microsoft.com> * Add support for --no-cache in BuildImage() & wslc.exe (#40174) * Add support for --no-cache in BuildImage() & wslc * Update test/windows/WSLCTests.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add try / CATCH_LOG() to the child logic when handling a WSLC_FORK message (#40180) * Add try / CATCH_LOG() to the child logic when handling a WSLC_FORK message * Propagate exceptions on failure * wslc: add --workdir/-w support to container create and run (#40190) Extends the --workdir flag (already supported by container exec) to the container create and run commands, passing the working directory through to the container launcher. Adds CLI parse tests, unit tests, and E2E tests for both commands. Co-Authored-By: Claude Sonnet Co-authored-by: Pooja Trivedi <trivedipooja@microsoft.com> * Fix test cleanup by deleting a file left behind (#40191) * Update volume APIs to match docker arguments (#40181) * Always show container logs during image builds (#40186) * Add logic to keep track of COM callbacks and cancel them if the sessi… (#40183) * Add logic to keep track of COM callbacks and cancel them if the session is terminating * Apply PR feedback * Format * Reset the test session * Apply PR feedback * Apply PR suggestions * CLI: Relative volume path support, improved error detection and messaging, more volume tests (#40193) * Bump Microsoft.NETCore.App.Runtime to 10.0.6 (CVE-2026-32178) (#40203) Update Microsoft.NETCore.App.Runtime.win-x64 and Microsoft.NETCore.App.Runtime.win-arm64 from 10.0.4 to 10.0.6 to resolve CVE-2026-32178 (.NET Spoofing Vulnerability). Fixes Dependabot alerts #12 and #13. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Change image size to match docker image size calculation (#40210) * Add `wslc login`, `wslc logout`, and `wslc push` commands for registry authentication (#40173) * Remove unused Elevated parameter from SessionSettings::Default (#40205) The Elevated parameter was passed to SessionSettings::Default() but never referenced in the function body. The elevation state is already encoded in the resolved session name (via ResolveDefaultSessionName which uses DefaultAdminSessionName for elevated tokens), making this parameter redundant dead code. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add missing trailing newlines to source files (#40202) Several files were missing the POSIX-required trailing newline, causing 'No newline at end of file' warnings from git and some compilers. Files fixed: - WSLCVhdVolume.cpp - WSLCVhdVolume.h - WSLCVolumeMetadata.h - ImageTagCommand.cpp - wslutil.cpp - CMakeLists.txt (service/exe) Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix empty string UB in ParseSizeBytes and missing null check in PruneContainers (#40204) Two bugs found during deep review: 1. WSLCVhdVolume.cpp ParseSizeBytes: accessing value[0] without checking if the string is empty causes undefined behavior. An empty SizeBytes driver option would trigger a read past the end of the string. Add value.empty() guard before the sign check. 2. WSLCSession.cpp PruneContainers: the Result output parameter is dereferenced without null validation, unlike the analogous PruneImages method which correctly checks all output pointers. Add RETURN_HR_IF_NULL and ZeroMemory initialization to match the established pattern. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wslc: remove redundant short-alias E2E tests for --workdir (#40192) * wslc: add --workdir/-w support to container create and run Extends the --workdir flag (already supported by container exec) to the container create and run commands, passing the working directory through to the container launcher. Adds CLI parse tests, unit tests, and E2E tests for both commands. Co-Authored-By: Claude Sonnet * wslc: remove redundant short-alias E2E tests for --workdir Short-alias (-w) coverage is already provided by the help message test method and CLI parse tests. Remove the duplicate E2E tests for container create and run per reviewer feedback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Pooja Trivedi <trivedipooja@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add test coverage for LoadImage/ImportImage on process exit/session terminate (#40172) * Add test covergae for LoadImage/ImportImage on process exit/session terminate * fix handle read interupt * do not leak container * Add --pull and --target flags to image build (#40216) * Fix wslc port relay buffer size to match non-WSLC relay path (#40217) * CLI: Make image save output optional (#40194) * Image save optional output * Clang format * Update test/windows/wslc/e2e/WSLCE2EImageSaveTests.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix terminal detection in CI for WSLCE2E_Image_Save_ToTerminal_Fail test - Open CONOUT$ as child stdout in RunWslcAndRedirectToFile when no output path is given, so IsConsoleHandle() returns true in the child process regardless of how CI redirects the test runner's stdout - Fix SavedArchivePath.wstring() -> SavedArchivePath in WSLCE2E_Image_Save_ToStdout_Load - Quote output path in effectiveCommandLine for diagnostic clarity Agent-Logs-Url: https://github.com/microsoft/WSL/sessions/f94556e1-9acd-40f1-89bd-349e238e92f3 Co-authored-by: AmelBawa-msft <104940545+AmelBawa-msft@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> * wslc: add network create, delete, and list (#40179) * Change help flag (#40236) * Fix bugs found during code review of feature/wsl-for-apps (#40248) - Fix wrong variable in waitpid check (init.cpp): Used 'Result' (poll return value) instead of 'Pid' (waitpid return value), causing incorrect SIGCHLD handling that could miss child exits or fail to detect init termination. - Fix missing semicolon after LOG_ERROR (WSLCInit.cpp): Missing statement terminator would cause compilation failure on Linux builds. - Fix misleading test comment (WSLCTests.cpp): Comment referenced port 1234 but the test actually validates port 1236 cleanup after bind failure. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * CLI: TableOutput unit tests (#40095) * Initial prune image cli command (#40239) * Use WSLC_E_CONTAINER_NOT_FOUND when a container is not found (#40251) * Use WSLC_E_CONTAINER_NOT_FOUND when a container is not found * Format * Add DeleteVolumes flag to container deletion API (#40232) * Start containerd before dockerd (#40237) Instead of letting dockerd manage its own embedded containerd, this starts containerd as a separate process first and points dockerd at its socket (--containerd /run/containerd/containerd.sock). My testing shows ~1s improvement with this. * cp * clang * clang * clang3 * event name --------- Co-authored-by: Darshak Bhatti <dabhatti@micorsoft.com> * Merge latest master into feature/wsl-for-apps (#40254) * test: enable virtiofs tests and enable WSLG during testing (#14387) * test: enable virtiofs tests and enable WSLG during testing * test fix --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * chore(distributions): Almalinux auto-update - 20260311 14:52:02 (#14404) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Fix CVE-2026-26127: bump .NET runtime from 10.0.0 to 10.0.4 (#14421) Addresses Dependabot alerts #10 and #11. The Microsoft.NETCore.App.Runtime packages (win-x64 and win-arm64) at version 10.0.0 are vulnerable to a denial of service via out-of-bounds read when decoding malformed Base64Url input (CVSS 7.5 High). Bumped to 10.0.4 which includes the fix. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Notice change from build: 141806547 (#14423) Co-authored-by: WSL notice <noreply@microsoft.com> * Ship initrd.img in MSI using build-time generation via powershell script (#14424) * Ship initrd.img in MSI using build-time generation via tar.exe Replace the install-time CreateInitrd/RemoveInitrd custom actions with a build-time step that generates initrd.img using the Windows built-in tar.exe (libarchive/bsdtar) and ships it directly in the MSI. The install-time approach had a race condition: wsl.exe could launch before the CreateInitrd custom action completed, causing ERROR_FILE_NOT_FOUND for initrd.img. Changes: - Add CMake custom command to generate initrd.img via tar.exe --format=newc - Add initrd.img as a regular file in the MSI tools component - Remove CreateInitrd/RemoveInitrd custom actions from WiX, DllMain, and wslinstall.def - Remove CreateCpioInitrd helper and its tests (no longer needed) - Update pipeline build targets to build initramfs instead of init * pr feedback * more pr feedback * switch to using a powershell script instead of tar.exe * powershell script feedback * hopefully final pr feedback --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * virtiofs: update logic so querying virtiofs mount source does not require a call to the service (#14380) * virtiofs: update logic so querying virtiofs mount source does not require a call to the service * more pr feedback * use std::filesystem::read_symlink * pr feedback and use canonical path in virtiofs symlink * make sure canonical path is always used --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * virtio networking: add support for ipv6 (#14350) * VirtioProxy: Add IPv6 address, gateway, and route support - Add PreferredIpv6Address field and GetBestGatewayV6* methods to NetworkSettings - Extend GetHostEndpointSettings() to discover IPv6 unicast address and gateway - Add UpdateIpv6Address() using ModifyGuestEndpointSettingRequest<IPAddress> - Push IPv6 default route to guest via UpdateDefaultRoute(AF_INET6) - Remove AF_INET6 early return in ModifyOpenPorts, use INETADDR_PORT() - Add EndpointRoute::DefaultRoute() static factory - Pass client_ip_ipv6 in devicehost options (not yet parsed by devicehost) - Remove gateway_ip from devicehost options (only needed for DHCP) - Include IPv6 DNS servers in non-tunneling DNS settings - Add ConfigurationV6 and DnsResolutionAAAA tests * cleanup and add more ipv6 tests * added test coverage and minor updates * clang format * pr feedback * format source * pr feedback * test fixes --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Track `bind` syscall when port is 0 (#14333) * Initial work * . * pr feedback and add unit test * minor tweaks an fix use after free in logging statement * implement PR feedback * hopefully final pr feedback * pr feedback in test function * Address PR feedback: add try/catch to TrackPort and PortZeroBind queue push --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Add iptables to list of apps to install in WSL (#14459) There were instructions already on how to install tcpdump in WSL, but iptables are also needed for the log collection to be complete, so this PR adds instructions on how to also install iptables. Co-authored-by: Andre Muezerie <andremue@linux.microsoft.com> * Update Microsoft.WSL.DeviceHost to version 1.1.39-0 (#14460) Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Moves all Ubuntu distros to the tar-based format (#14463) * Move all supported Ubuntu images to the new format We backported the build pipeline so all current LTSes come out in the new tar-based format * Remove the appx based distros All WSL users can run tar-based distros by now, right? There is no benefit in maintaining both formats. * Enable DNS tunneling for VirtioProxy networking mode (#14461) - Allow VirtioProxy to keep EnableDnsTunneling=true in config, but clear socket-specific options (BestEffortDnsParsing, DnsTunnelingIpAddress) - Suppress dedicated DNS tunneling hvsocket for VirtioProxy; tunneling is handled through the VirtioNetworking device host instead - Set DnsTunneling flag on VirtioNetworkingFlags so the device host knows to tunnel DNS - Expand SWIOTLB kernel cmdline to cover VirtioFs and VirtioProxy - Bump DeviceHost package to 1.1.39-0 - Add VirtioProxy DNS test coverage for tunneling on/off - Skip GuestPortIsReleasedV6 on Windows 10 Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * test: disable LoopbackExplicit due to OS build 29555 regression (#14477) Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Refactor: trim unnecessary DLL deps from COMMON_LINK_LIBRARIES (#14426) * Refactor: trim unnecessary DLL deps from COMMON_LINK_LIBRARIES - Split MSI/Wintrust install functions from wslutil.cpp into install.cpp - Remove MI.lib, wsldeps.lib, msi.lib, Wintrust.lib, computecore.lib, computenetwork.lib, Iphlpapi.lib from COMMON_LINK_LIBRARIES - Add per-target MSI_LINK_LIBRARIES, HCS_LINK_LIBRARIES, SERVICE_LINK_LIBRARIES - Delay-load msi.dll and WINTRUST.dll for wsl.exe and wslg.exe - Result: wslhost, wslrelay, wslcsdk, testplugin lose msi/wintrust startup imports; wsl.exe and wslg.exe defer msi/wintrust loading until actually needed; wslservice is the only target that imports computecore/computenetwork/Iphlpapi * minor fixes to install.cpp that were caught during PR * move to wsl::windows::common::install namespace --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Fix wsl stuck when misconfigured cifs mount presents (#14466) * detach terminal before running mount -a * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * use _exit on error before execv in child process to avoid unintentional resource release * Add regression test * Fix clang format issue * fix all clang format issue * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * resolve ai comments * move test to unit test * Fix string literal * Overwrite fstab to resolve pipeline missing file issue --------- Co-authored-by: Feng Wang <wangfen@microsoft.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Update localization and notice scripts to target the branch that the pipeline is running on (#14492) * test: Add arm64 test distro support (#14500) * test: Add arm64 test distro support * update unit test baseline * more test baseline updates --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * test: remove duplicated DNS test coverage (#14522) * test: remove duplicated DNS test coverage * format source --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Fix: Fail and warn the user when --uninstall is given parameters (#14524) Fail and warn the user when --uninstall is given parameters. * Localization change from build: 142847827 (#14525) Co-authored-by: WSL localization <noreply@microsoft.com> * virito net: revert to previous DNS behavior while we debug an issue with DNS over TCP (#14532) Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * devicehost: update to latest devicehost nuget with tracing improvements (#14531) Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Localization change from build: 142949177 (#14542) Co-authored-by: WSL localization <noreply@microsoft.com> * Revert "test: enable virtiofs tests and enable WSLG during testing (#14387)" (#14538) * Revert "test: enable virtiofs tests and enable WSLG during testing (#14387)" * enable wslg for SystemdNoClearTmpUnit test --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Localization change from build: 143033415 (#14553) Co-authored-by: WSL localization <noreply@microsoft.com> * Update cgmanifest to match CMakeLists.txt (#14550) * Update cgmanifest to match CMakeLists.txt * Update CMakeLists.txt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Notice change from build: 143064110 (#14556) Co-authored-by: WSL notice <noreply@microsoft.com> * Update Microsoft.WSL.DeviceHost to version 1.1.48-0 (#40036) * Update Microsoft.WSL.DeviceHost to version 1.1.48-0 (#14575) Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Re-enable WSLG during testing. This reverts commit bf759a092be8ed481dce70bf4f01f16c1b9b54a8. * add back config change (will work with new default, but makes test explicit) --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Bump Kali to 2026.1 (#14574) Release notes: https://www.kali.org/blog/kali-linux-2026-1-release/ * socketshared: add maximum message size to avoid very large allocations (#40050) Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Fix: bind interrupted by seccomp signal race (#14554) * Use wait_for_completion_killable instead of the default wait_for_completion_interruptible * retry if kernel does not support the new flag * archlinux: Release 2026.04.01.162669 (#40059) This is an automated release [1]. [1] https://gitlab.archlinux.org/archlinux/archlinux-wsl/-/blob/main/.gitlab-ci.yml * triage: pass action inputs via env vars instead of inline expansion (#40060) Move inputs.comment, inputs.issue, and inputs.token into the env block, consistent with how inputs.previous_body is already handled. This avoids issues with special characters in input values being misinterpreted during shell evaluation. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Set Distro Env HOSTTYPE to aarch64 for ARM64 build (#40048) * Set Distro Env HOSTTYPE to aarch64 for ARM64 build --------- Co-authored-by: Xin Wang (from Dev Box) <xiwang4@microsoft.com> * devicehost: stop re-signing and fix MSI installer failing to replace wsldevicehost.dll (#40075) * devicehost: stop re-signing and fix MSI installer failing to replace wsldevicehost.dll * pr feedback * use MSIRMSHUTDOWN = 1 instead of custom action --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * docs: overhaul Copilot instructions with coding conventions and prompt files (#40113) * docs: overhaul Copilot instructions with coding conventions and prompt files Major update to .github/copilot-instructions.md: - Add coding conventions (naming, error handling, RAII, strings, headers, synchronization, localization, telemetry, formatting, IDL/COM, config) - Add test authoring summary pointing to detailed test.md prompt - Add namespace-to-directory map for top-level namespaces - Add key source files list (defs.h, WslTelemetry.h, wslc.idl, etc.) - Replace clang-format references with .\FormatSource.ps1 - Consolidate duplicate timing info into single reference table New files: - .github/copilot/review.md: Review prompt focused on high-risk areas (ABI breaks, missing localization, resource safety) - .github/copilot/test.md: Test generation prompt with TAEF patterns - .github/copilot/commit.md: Commit message guidelines - .editorconfig: Editor settings for non-C++ files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: address PR review feedback - Scope precomp.h guidance to Windows components (Linux doesn't use it) - Fix review.md reference to .github/copilot-instructions.md - Restore clang-format as Linux formatting option alongside FormatSource.ps1 - Note FormatSource.ps1 requires cmake . first - Fix en-us -> en-US casing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Suppress MSI-initiated reboots during Store updates (#40079) When the WSL MSIX package is updated via the Microsoft Store, the WslInstaller service automatically upgrades the MSI package by calling MsiInstallProduct. This call was made with INSTALLUILEVEL_NONE (silent install) but without setting the REBOOT=ReallySuppress property. Per Windows Installer documentation, when a silent install encounters files in use and REBOOT is not suppressed, the system reboots automatically without any user prompt. This could cause unexpected machine restarts after a Store update when WSL binaries (e.g. wslservice.exe) were in use during the upgrade. Every deployment script in the repo already passes /norestart to msiexec (deploy-to-host.ps1, deploy-to-vm.ps1, install-latest-wsl.ps1, test-setup.ps1), but the programmatic MsiInstallProduct path used by the WslInstaller service lacked the equivalent property. This change: - Always appends REBOOT=ReallySuppress to MsiInstallProduct arguments in UpgradeViaMsi, preventing Windows Installer from ever initiating a system restart during install/upgrade. - Switches UninstallViaMsi from MsiConfigureProduct to MsiConfigureProductEx so we can pass REBOOT=ReallySuppress during uninstall as well. - Propagates ERROR_SUCCESS_REBOOT_REQUIRED (3010) to callers instead of swallowing it. User-facing paths (wsl --update, wsl --uninstall) print a reboot-needed message to stderr. The background WslInstaller service silently treats 3010 as success since it has no console. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix UnicodeEncodeError in create-release.py on cp1252 consoles (#40127) * Fix UnicodeEncodeError in create-release.py on cp1252 consoles Reconfigure stdout/stderr with errors='backslashreplace' so commit messages containing characters outside the console code-page (e.g. U+2225) are escaped instead of crashing the script. Also redirect the 'failed to extract PR number' warning to stderr for consistency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix return type annotation for get_github_pr_message() Update the return annotation from str to tuple[str | None, str | None] to match the actual return values (pr_body, pr_number). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * build: add Source Link to embed GitHub source mappings in PDBs (#40055) * build: add Source Link to embed GitHub source mappings in PDBs * Gate Source Link on pipeline builds only Only generate sourcelink.json and pass /SOURCELINK to the linker when PIPELINE_BUILD_ID is defined (i.e. during CI pipeline builds). This avoids unnecessary Source Link artifacts in local developer builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add /attachdebugger option to automatically launch WinDbgX for test debugging (#40116) * Add /attachdebugger option to automatically launch WinDbgX for test debugging When /attachdebugger is passed to test.bat, run-tests.ps1 now: - Starts te.exe with /waitfordebugger in the background - Polls for the TE.ProcessHost.exe child process via WMI - Launches WinDbgX attached directly to the test host PID - With /inproc, attaches to TE.exe itself instead This replaces the manual workflow of running /waitfordebugger, reading the PID from the output, and launching WinDbgX separately. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * run-tests: use /inproc with /attachdebugger, simplify exit Per review feedback from @OneBlue: - Add /inproc when /attachdebugger is set so WinDbgX attaches directly to TE.exe instead of polling for TE.ProcessHost.exe - Simplify exit to pass through TE.exe exit code directly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: update /attachdebugger to reflect /inproc behavior The script now always adds /inproc, so update the README to match: WinDbgX attaches directly to TE.exe, no ProcessHost polling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Replace pre-commit hook with CMake-generated clang-format check (#40136) * Replace pre-commit hook with CMake-generated clang-format check Replace the old pre-commit hook that shelled out to PowerShell and never blocked commits (-NoFail) with a CMake-generated hook that calls clang-format directly on staged C/C++ files. - Add tools/hooks/pre-commit.in as a CMake template - CMake resolves the clang-format path at configure time via LLVM_INSTALL_DIR, matching the existing FormatSource.ps1.in pattern - Hook blocks commits on formatting errors, skips gracefully if clang-format is not available (cmake not yet run) - ~5x faster than the old PowerShell approach (~0.5s vs ~2.6s) * Make pre-commit hook behavior configurable via WSL_PRE_COMMIT_MODE Add WSL_PRE_COMMIT_MODE CMake cache variable with three modes: - warn (default): report formatting issues without blocking commit - error: block commit when formatting issues are found - fix: auto-format files and re-stage them Also addresses PR feedback: - Generate hook into build tree, copy to source tree for out-of-source builds - Use repo-local tools/clang-format.exe instead of LLVM_INSTALL_DIR path - Use @ONLY in configure_file to avoid shell variable substitution issues - Document modes in dev-loop.md and UserConfig.cmake.sample Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Split x64/arm64 builds into parallel pipeline stages (#14497) * Split x64/arm64 builds into parallel pipeline stages Restructure the CI/CD pipeline to build x64 and arm64 in parallel instead of sequentially, reducing end-to-end build time. Pipeline shapes: - PR: build_x64 ∥ build_arm64 → test (uses installer.msix directly) - Nightly: build_x64 ∥ build_arm64 → package → test (dev-cert bundle) - Release: build_x64 ∥ build_arm64 → package → test (ESRP-signed bundle) Key changes: - Extract shared build-job.yml template parameterized by platform - Add package-stage.yml that creates msixbundle from both platform artifacts, ESRP-signs for release, dev-cert signs for nightly - PR tests run immediately after x64 build using installer.msix (no package stage, no bundle needed) - Release/nightly tests wait for the package stage and test the real signed bundle that gets published - CloudTest configs are parameterized: release tests pull the bundle from the [package] artifact, PR tests use installer.msix from [drop] - arm64 + formatting checks always run in parallel with x64 but don't block the PR test gate - CodeQL runs in the arm64 stage (off the critical path) - flight-stage and nuget-stage updated for new stage names * formatting * Remove redundant runtime conditions on release-only tasks These tasks are already wrapped in compile-time conditionals which prevent them from being added to the pipeline definition for non-release builds. The runtime condition checks can never evaluate to false at that point, so they are pure noise. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review feedback from OneBlue - Remove wslcsdk NuGet staging (wslc is not in master) - Always include [package] provider in TestMap.xml.in instead of conditionally injecting via PACKAGE_PROVIDER_BLOCK cmake variable. PR builds simply use [drop] as TEST_PACKAGE_PROVIDER; the [package] provider exists but is unused. - Add BUNDLE_ONLY cmake option so the package pipeline stage reuses cmake's existing bundle target instead of forking makeappx logic. This locks the SDK version via cmake (CMAKE_SYSTEM_VERSION) and keeps bundle creation logic in one place (msixinstaller/CMakeLists.txt). The pipeline now copies msix files to expected paths, runs a fast cmake configure with -DBUNDLE_ONLY=TRUE, and builds the bundle target. - Remove dead NuGet binary restore step in package stage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review feedback - Fix PACKAGE_VERSION regex: escape dots so only A.B.C.D is accepted - Consolidate CMake defaults (build type, config types, output dir) before BUNDLE_ONLY block to avoid duplication - Nightly tests now use the full bundle from the package stage instead of installer.msix (new INCLUDE_PACKAGE_STAGE cmake variable) - Package stage reuses version output from build stage instead of recomputing it Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Mask console-getty.service to prevent multi-distro failures (#13595) (#14490) * Mask console-getty.service to prevent multi-distro failures (#13595) When multiple WSL distros run concurrently, /dev/tty devices are shared at the VM level. The second distro's console-getty.service fails because the tty is already held by the first, causing systemd to report failed units and triggering user@UID.service failures. Mask console-getty.service during WSL systemd unit generation, similar to the existing masking of networkd-wait-online. This service provides no value in WSL since users don't connect to the underlying tty. Fixes #13595 * format source * pr feedback --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * Fix random "ERROR_FILE_NOT_FOUND" when unmounting with absolute path (#40092) Fix ERROR_FILE_NOT_FOUND when unmounting a vhd with absolute path after the vm timeouts. * Refactor tests: use TAEF metadata for WSL version filtering (#40140) Cherry-pick WSL1/WSL2 test changes from 9c4dba91 (feature/wsl-for-apps). Replace runtime WSL1_TEST_ONLY()/WSL2_TEST_ONLY() skip macros with WSL1_TEST_METHOD()/WSL2_TEST_METHOD() TAEF metadata macros. This moves version filtering to the test runner level via /select: queries, so inapplicable tests are excluded entirely instead of appearing as skipped. Updated files: - test/windows/Common.h: New macros + removed old skip macros - test/windows/*.cpp: Converted all test methods - tools/test/run-tests.ps1: Auto-add /select: when no user filter - cloudtest/TestGroup.xml.in: Add version filter to TAEF args - test/README.md: Document new macros Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix mount test build: use WSL2_TEST_METHOD macro for AbsolutePathVhdUnmountAfterVMTimeout (#40162) Replace raw TEST_METHOD + WSL2_TEST_ONLY() with WSL2_TEST_METHOD macro, consistent with the refactor in #40140. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix various build issues if the repo path contains a space (#40160) * Add git entries for libarchive and boost in cgmanifest.json (#40155) The OSPO notice@0 task cannot resolve licenses for components registered with type 'other' (source tarballs). Add parallel type 'git' entries pointing at the GitHub repos with the exact commit SHAs for the release tags (v3.7.7 and boost-1.90.0). The existing 'other' entries are kept for accurate provenance tracking of the actual tarball downloads used by CMake FetchContent. The 'git' entries enable the notice generator to look up licenses from ClearlyDefined. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fixes to route mirroring (#40099) * route fixes from protonvpn testing * add unit test, fix log * ai code review * format with vs 2022 * review * remove optional has_value checks * harden contracts for to, via having or not having values * update IsOnlink check * fix constructor parameters * fix build --------- Co-authored-by: Catalin-Emil Fetoiu <cfetoiu@microsoft.com> * re enable (#40156) Co-authored-by: Catalin-Emil Fetoiu <cfetoiu@microsoft.com> * fix wrong variable checking (#40185) Co-authored-by: Xin Wang (from Dev Box) <xiwang4@microsoft.com> * Add warning to collect-wsl-logs.ps1 to be displayed when tool is missing (#14447) * Added warning message to collect-wsl-logs.ps1 * Add warning to collect-wsl-logs.ps1 to be displayed when tool is missing On executing the log collection script, it will first confirm the required tools tcpdump and iptables are installed. For each tool missing a warning is displayed, reminding the user that the tool should be installed prior to executing the script to get a more complete log collection. * Eliminated reduntant parameter and added handling for situation where WSL might be unavailable * Remove try/catch from function's implementation --------- Co-authored-by: Andre Muezerie <andremue@linux.microsoft.com> * Bump Microsoft.NETCore.App.Runtime to 10.0.6 (CVE-2026-32178) (#40207) Update Microsoft.NETCore.App.Runtime.win-x64 and Microsoft.NETCore.App.Runtime.win-arm64 from 10.0.4 to 10.0.6 to resolve CVE-2026-32178 (.NET Spoofing Vulnerability). Fixes Dependabot alerts #12 and #13. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix VHD ownership after cross-volume move to prevent E_ACCESSDENIED (#40159) * Fix VHD ownership after cross-volume move to prevent E_ACCESSDENIED When MoveDistribution moves a VHD across volumes, MoveFileEx copies the file and the new file's owner may not be the user's SID. This causes HcsGrantVmAccess to fail with E_ACCESSDENIED when later launching the distro, because the impersonated user lacks WRITE_DAC on the file (only implicitly granted to the owner). Fix by explicitly setting the VHD owner to the user's SID after the move, matching what CreateVhd already does at creation time. Uses handle-based SetSecurityInfo with FILE_FLAG_OPEN_REPARSE_POINT to avoid TOCTOU races and symlink following. Also fixes a pre-existing build break in MountTests.cpp from the test refactor (WSL2_TEST_ONLY -> WSL2_TEST_METHOD). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Preserve original VHD owner instead of using GetUserSid() Instead of unconditionally setting the VHD owner to the caller's SID after a cross-volume move, read the original owner before the move and restore it afterward. This avoids changing ownership to someone who didn't originally own the file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use {} format specifier instead of %s in Linux LOG_ERROR path (#40227) The Linux #else branch of SocketChannel.h uses LOG_ERROR which expects fmt-style {} placeholders, but the channel name was using printf-style %s. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update MoveVhdOwnership test to WSL2 only (#40223) * Initial plan * Update MoveVhdOwnership test to WSL2 only Agent-Logs-Url: https://github.com/microsoft/WSL/sessions/65d80936-791c-411b-8da2-d8c1bc06e651 Co-authored-by: benhillis <17727402+benhillis@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: benhillis <17727402+benhillis@users.noreply.github.com> * fix: use ssize_t for readlinkat return value in p9file.cpp (#40226) readlinkat() returns ssize_t, not int. On 64-bit systems this could silently truncate the return value for very long symlink targets. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * cleanup: extract SkipSignal helper to deduplicate signal skip lists (#40228) * cleanup: extract SkipSignal helper to deduplicate signal skip lists UtilSaveSignalHandlers and UtilSetSignalHandlers had identical switch statements for skipping non-settable signals. Extract a shared helper to keep the skip list in one place. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * address review: document SkipSignal signals and update function headers Add comments explaining why each signal is skipped (POSIX non-settable, NPTL internal signals 32-34, SIGHUP handled separately). Update UtilSaveSignalHandlers and UtilSetSignalHandlers descriptions to reference SkipSignal() instead of just mentioning SIGHUP. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use correct GetAddrInfoTestEntry handler for get_addr_info test (#40225) The get_addr_info test entry was incorrectly mapped to GetSetIdTestEntry instead of GetAddrInfoTestEntry, causing the wrong test handler to run. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Upload binaries when publishing symbols (#40208) * Experiment with symbols * Don't delete dlls * Cleanup diff * Fix clang-format violation in UnitTests.cpp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Include VirtioProxy in IsDnsTunnelingSupported assert The feature branch adds NetworkingMode::VirtioProxy which supports DNS tunneling, but master's assert in IsDnsTunnelingSupported() only expected Nat or Mirrored. This would fire in debug builds when VirtioProxy is selected (e.g. after NAT fallback). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add wslcsdk.dll and TestData to test artifacts in build-job.yml Master's pipeline refactor missed copying wslcsdk.dll (runtime dependency of wsltests.dll) and the Microsoft.WSL.TestData package to the test artifact staging area. This caused all test stages to fail with ERROR_MOD_NOT_FOUND when loading wsltests.dll. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use dynamic package path for WSLC tests The WSLC test group template was hardcoding the msixbundle path, which doesn't exist in PR builds where the package stage is skipped. Use the same TEST_PACKAGE_PROVIDER/TEST_PACKAGE_PATH/TEST_PACKAGE_FILE variables as the WSL1/WSL2 test template, so WSLC tests use installer.msix in PR builds and the bundle in release/nightly builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Stage wslcsdk.lib and wslcsdk.dll for Containers nuget package The package stage needs wslcsdk.lib and wslcsdk.dll to pack Microsoft.WSL.Containers.nuspec, but the split pipeline doesn't build wslcsdk in the package stage. Stage these files as build artifacts from both x64 and arm64 build jobs, then copy them to the expected bin paths in the package stage before nuget pack. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: AlmaLinux Autobot <107999298+almalinuxautobot@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Blue <OneBlue@users.noreply.github.com> Co-authored-by: WSL notice <noreply@microsoft.com> Co-authored-by: Daman Mulye <daman_mulye@hotmail.com> Co-authored-by: Andre Muezerie <108841174+andremueiot@users.noreply.github.com> Co-authored-by: Andre Muezerie <andremue@linux.microsoft.com> Co-authored-by: Carlos Nihelton <carlos.santanadeoliveira@canonical.com> Co-authored-by: Feng Wang <wang6922@outlook.com> Co-authored-by: Feng Wang <wangfen@microsoft.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: g0tmi1k <535942+g0tmi1k@users.noreply.github.com> Co-authored-by: Arch Linux Technical User <65091038+archlinux-github@users.noreply.github.com> Co-authored-by: wangxin12 <sbwap@vip.qq.com> Co-authored-by: Xin Wang (from Dev Box) <xiwang4@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: FetoiuCatalin <fetoiucatalin@gmail.com> Co-authored-by: Catalin-Emil Fetoiu <cfetoiu@microsoft.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: benhillis <17727402+benhillis@users.noreply.github.com> * CLI: Initial support for volume commands (#40139) * Init volume command * WIP * Create * CLang format * Volume command * Added E2E tests * Clang format * Resolve copilot comment * Revert test in run/create * Rename delete to remove * Resolve copilot comment * Resolve copilot comment * Fix e2e tests * Adding tests * Adding tests * Adding tests * Fix tests * Clang format * Integrated volume * Added integration tests * Clang format * Remove unused code * Added e2E tests * Addressed comments * Addressed comments * wslc: add network inspect (#40250) * Add "guest" volume driver (#40255) * CLI: Initial support for inspect command (#40211) * Remove options that are not yet implemented * Init inspect * Clang format * Try inspect * Added tests * Clang format * Loc * Revert format loc * Resolve copilot comment * Solve various issues when refreshing the virtionet connection (#40267) * Save state * Fix the threading model * Prepare for PR * Use GIT instead * Format * Initialize fields * SDK Install function implementation (#40266) Implements `WslcInstallWithDependencies` using existing functions to determine which components are required and to install the Virtual Machine component. Adds implementation of WU API installation flow. For testing purposes, the WU API implementation takes in an object factory and is broken into the largely independent steps of searching, downloading, and installing the WSL package. The actual use leverages a convenience method that implements the overall flow. * CLI: add support for hostname option (#40279) * Init hostname * CE * SDK API review changes (#40265) * CLI: Initialize domainname option (#40281) * Init domainname option * CLang format * Update settings defaults and warn on unknown keys (#40268) * CLI: Add DNS support in CLI (#40290) * Init DNS support * Fix build * Clang format * Add DNS CLI test cases to CommandLineTestCases.h Agent-Logs-Url: https://github.com/microsoft/WSL/sessions/b2ba2343-14c4-4af2-8a56-838b63b48f31 Co-authored-by: AmelBawa-msft <104940545+AmelBawa-msft@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> * Rename ContainerEventTracker to DockerEventTracker * Add guest volume driver, Docker event tracker with volume sync, and WSLCVolumes manager * Address feedback and fix formatting Co-authored-by: Copilot <copilot@github.com> * refactor * address comment * Address feedback * Add support for anonymous port binds and --publish-all (#40288) * Implement WSLCContainerFlagsPublishAll flag with CLI and tests When the PublishAll flag is set, WSLCContainerImpl::Create() inspects the container image to discover its ExposedPorts, creates TCP port mappings for each exposed port that doesn't already have an explicit mapping, and uses the resolved image ID for the container creation request to ensure the same image is used. CLI changes: - Add --publish-all / -P flag to 'container create' and 'container run' - Wire through ContainerOptions -> WSLCContainerFlagsPublishAll - Add localization string for the new argument Tests: - PublishAllExposedPorts: builds an image with EXPOSE 8080 and 9090, verifies both ports get auto-mapped and the HTTP server is reachable - PublishAllImageNotFound: verifies IMAGE_NOT_FOUND is returned when the flag is set with a nonexistent image Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Save state * Prepare for PR * Clean diff * Cleanup diff * Cleanup diff * Apply PR feedback --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Reuse virtiofs shares when possible (#40298) * Reuse virtiofs shares when possible * Use weakly_canonical and only delete in plan9 mode * Set `guest` voume driver as default (#40277) * Add gateway driverOpt support in CreateNetwork and invalid input tests (#40278) * Address copilot feedback Co-authored-by: Copilot <copilot@github.com> * wslc: update devicehost dll to a version that properly supports device removal (#40305) Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> * FIX ABBA deadlock Co-authored-by: Copilot <copilot@github.com> * Localize settings output (#40297) * Address feedback Co-authored-by: Copilot <copilot@github.com> * CLI: Unit test to ensure no argument name or alias collisions exist (#40312) * Validate opened containers Co-authored-by: Copilot <copilot@github.com> * Update src/windows/wslcsession/DockerEventTracker.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fixed leaked container in NamedVolumesVhdSessionRecovery causing test failures in OpenContainer (#40316) * Fixed leaked container in NamedVolumesVhdSessionRecovery causing test failures in OpenContainer * Update comment * Fix malformed WSL_E_PLUGIN_REQUIRES_UPDATE HRESULT constant (#40325) * Fix malformed WSL_E_PLUGIN_REQUIRES_UPDATE HRESULT constant MAKE_HRESULT expects a 16-bit code parameter, but the full 32-bit HRESULT value 0x8004032A was being passed as the code, producing a garbage HRESULT at runtime. Since this is a standalone plugin header without access to WSL_E_BASE, define the HRESULT as a direct literal matching MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x032A). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update src/windows/inc/WslPluginApi.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix missing return after accept4 failure in InteropServer::Accept (#40323) When accept4 fails, the function logged an error but continued to call setsockopt on an invalid file descriptor (-1) and returned the invalid fd to the caller. Add the missing early return to prevent operating on an invalid file descriptor. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * CLI: Enhance image and container inspection behavior (#40315) * Update image and container inspect behavior * Clang format * merge master -> feature/wsl-for-apps (#40317) * merge master -> feature/wsl-for-apps * Update WSLC init and VM to use transaction-based message protocol The merge from master introduced a transaction-based message protocol in SocketChannel.h, but the WSLC code paths were not updated. This caused crashes when creating WSLC sessions because the Linux-side init expected non-transaction messages while the Windows side sent transaction messages. Changes: - WSLCInit.cpp: Update all 13 HandleMessageImpl handlers to accept Transaction& and reply via Transaction.Send/SendResultMessage instead of Channel.SendMessage. Update ProcessMessages loop to use Channel.ReceiveTransaction(). - WSLCVirtualMachine.cpp: Convert remaining non-transaction SendMessage calls (WSLC_WATCH_PROCESSES, WSLC_TTY_RELAY, WSLC_EXEC) to use StartTransaction. Fix ConnectSocket Fd=-1 path to receive the second reply within the same transaction instead of a non-transaction receive. * Add .NET DLL for SDK projection to NuGet package (#40182) * Remove duplicate object inclusion for wslc executable (#40331) wslclib is an OBJECT library. Both \$<TARGET_OBJECTS:wslclib> in add_executable() and target_link_libraries(wslc wslclib) include the same object files. In CMake 3.12+, linking an OBJECT library brings both objects and transitive dependencies, making the generator expression redundant and potentially causing duplicate symbol issues. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix data race on crash flags in HcsVirtualMachine::OnCrash (#40328) m_crashLogCaptured and m_vmSavedStateCaptured were plain bool members accessed from concurrent HCS event callbacks (HcsEventSystemCrashInitiated and HcsEventSystemCrashReport) without synchronization. Change both to std::atomic<bool> and use exchange() to ensure only one thread performs WriteCrashLog or EnforceVmSavedStateFileLimit for the same crash event. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix UtilExec: remove premature Result check that breaks output capture (#40327) Result was initialized to -1 and checked (Result < 0) inside the fgets loop before ever being set to a success value. This caused the first iteration to immediately goto ErrorExit, making all output capture dead code. The Result variable is properly set after the loop completes via pclose(), so the in-loop check was erroneous. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use auto reset event for container stop Co-authored-by: Copilot <copilot@github.com> * fix formatting * Use manual reset event for waiting for destroyed event Co-authored-by: Copilot <copilot@github.com> * WaitForEvent aborts if SessionTerminating is fired Co-authored-by: Copilot <copilot@github.com> * fix formatting * Fix error message Co-authored-by: Copilot <copilot@github.com> * formatting * Clean-up delete event signaling story Co-authored-by: Copilot <copilot@github.com> * Move definition of TFM for NuGet (#40339) * Cancel awaiters on session exit Co-authored-by: Copilot <copilot@github.com> * Add destroy to event tracker Co-authored-by: Copilot <copilot@github.com> * Log timeoutes Co-authored-by: Copilot <copilot@github.com> * Fix missing break statement in WaitForEventOrSessionTerminating Co-authored-by: Copilot <copilot@github.com> * Set WSLC VM owner to WSLC-<DisplayName> for debuggability (#40346) Use the session display name in the HCS VM owner field so that hcsdiag list output shows which session owns each VM. This aids debugging and allows tests to reliably identify their VM. Falls back to 'WSLC' if DisplayName is null. Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Restore CAB file creation and signing steps in build pipeline (#40337) * Restore CAB file creation and signing steps in build pipeline The CAB generation and ESRP code signing steps were accidentally removed in commit 3c9c3e16 (Create NuGet for WSLC SDK). This restores both steps in build-job.yml (the shared build template that replaced the old build-stage.yml per-platform loop): - PowerShell task to create .cab from .msi via makecab.exe (with error handling) - ESRP code signing task for .cab files (release builds only) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove redundant runtime condition on CAB signing step The compile-time isRelease guard already prevents the step from being included in non-release builds. The other ESRP signing tasks in this template rely on the template-level if alone, so align CAB signing to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Implement exit code handling and signaling for Docker container processes Co-authored-by: Copilot <copilot@github.com> * Remove unreachable else branch in TTY stdin relay (#40330) * Remove unreachable else branch in TTY stdin relay write() on success returns a value in [0, count]. The condition (bytesWritten <= pendingStdin.size()) is therefore always true, making the else branch dead code. Simplify to unconditional erase. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add WI_ASSERT as defensive check for write() result Add a WI_ASSERT to verify bytesWritten <= pendingStdin.size() as a defensive measure, per PR feedback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix deadlock Co-authored-by: Copilot <copilot@github.com> * Stop with empty timestamp on wait timeout * Add a temporary fix to prevent VS project load failures due to 'AnyCpu' targets (#40348) * Add a temporary fix to prevent VS project load failures due to 'AnyCpu' targets * Default to false * Use auto reset event for container stop (#40338) * Dont log on wait timeouts Co-authored-by: Copilot <copilot@github.com> * refactor on stop Co-authored-by: Copilot <copilot@github.com> * Fix SignalExit Co-authored-by: Copilot <copilot@github.com> * Skip the LoadImage() test on server SKU's (#40350) * Skip the LoadImage() test on server SKU's * Update test/windows/WSLCTests.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * CLI: Add label option in wslc run/create (#40275) * Init label * CE * Fix build * Clang format * Update src/windows/wslcsession/WSLCContainer.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Addressed comments --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Build and stage wslcsdkcs.dll in CI pipeline (#40359) * Build and stage wslcsdkcs.dll in CI pipeline The Microsoft.WSL.Containers.nuspec template references wslcsdkcs.dll but the pipeline never passed -DWSL_BUILD_SDKCS=true to CMake, so the DLL was never built or staged as an artifact. This caused nuget pack to fail with NU5019 (file not found). - Add -DWSL_BUILD_SDKCS=true to the CMake configure step in build-job.yml - Stage wslcsdkcs.dll alongside wslcsdk.lib/dll in build artifacts - Copy wslcsdkcs.dll from artifacts in the package stage for nuget pack Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add wslcsdkcs to ESRP signing targets and pattern in build-stage.yml Agent-Logs-Url: https://github.com/microsoft/WSL/sessions/77af7340-0829-40b8-8fa8-7026f2bc6eb8 Co-authored-by: benhillis <17727402+benhillis@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: benhillis <17727402+benhillis@users.noreply.github.com> * Detect unexpected VM exit and terminate wslcsession.exe (#40158) * Detect unexpected VM exit and terminate wslcsession.exe Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR feedback: clarify m_terminating comment and increase test timeout - Improve comment on m_terminating to explain why the atomic is needed (prevents deadlock when OnVmExited races with external Terminate). - Increase WaitForSessionTermination timeout from 10s to 2min to handle slow test VMs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Guard m_vmExitedEvent.is_signaled() against null handle Terminate() can be called from the Initialize() error cleanup path before GetTerminationEvent() populates m_vmExitedEvent. Add a null check to avoid calling is_signaled() on an uninitialized event. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use a scope_exit to cleanup resources in ModuleCleanup (#40362) * Simplify the clang-format hook logic to generate pre-commit.in with the correct clang-format.exe path (#40364) * Update the clang-format hook logic to write to the repo root instead of the binary dir * Simplify * Fix ABBA deadlock between WSLCContainerImpl & COMImplClass (#40358) * Fix ABBA deadlock between WSLCContainerImpl & COMImplClass * Apply PR feedback * Apply PR feedback * Fix garbled UTF-8 progress bars in wslc build output (#40356) * Fix garbled UTF-8 progress bars in wslc build output The CRT locale was set to the system default (L"") which uses the ANSI codepage (e.g. Windows-1252) for narrow-to-wide string conversions. When Docker build callbacks print UTF-8 output via wprintf(L"%hs", status), multi-byte characters like block elements (U+2588) were decoded incorrectly, producing garbled output (e.g. garbled chars instead of solid blocks). Override LC_CTYPE to .UTF-8 (with .65001 fallback) so that all CRT narrow-to-wide conversions correctly handle UTF-8 encoded strings from Linux/container processes. The system default locale is preserved for all other categories (numeric, time, collation). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Guard LC_CTYPE UTF-8 override with Mode check Only set LC_CTYPE to .UTF-8 when the CRT output mode is _O_U8TEXT, making the function correct for potential future callers with other modes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix race condition in VM process tracking logic (#40351) * Fix a potential hang if synchronous IO is scheduled after CancelUserHandleIO() returns (#40357) * Fix a potential hang if synchronous IO is scheduled after CancelUserHandleIO() returns * Apply PR feedback * Format * Comment cleanup * Cleanup and address comments Co-authored-by: Copilot <copilot@github.com> * More cleanup Co-authored-by: Copilot <copilot@github.com> * Cleanup comments * Skip already processed volume events Co-authored-by: Copilot <copilot@github.com> * Add stress test Co-authored-by: Copilot <copilot@github.com> --------- Co-authored-by: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Co-authored-by: Blue <OneBlue@users.noreply.github.com> Co-authored-by: Flor Chacón <14323496+florelis@users.noreply.github.com> Co-authored-by: beena352 <beenachauhan@microsoft.com> Co-authored-by: JohnMcPMS <johnmcp@microsoft.com> Co-authored-by: Ben Hillis <benhillis@gmail.com> Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: WSL localization <noreply@microsoft.com> Co-authored-by: AlmaLinux Autobot <107999298+almalinuxautobot@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Daman Mulye <daman_mulye@hotmail.com> Co-authored-by: Andre Muezerie <108841174+andremueiot@users.noreply.github.com> Co-authored-by: Andre Muezerie <andremue@linux.microsoft.com> Co-authored-by: Carlos Nihelton <carlos.santanadeoliveira@canonical.com> Co-authored-by: Feng Wang <wang6922@outlook.com> Co-authored-by: Feng Wang <wangfen@microsoft.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: yao-msft <50888816+yao-msft@users.noreply.github.com> Co-authored-by: David Bennett <dbennett-msft@outlook.com> Co-authored-by: John Stephens <johnstep@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Pooja Trivedi <poojatrivedi@gmail.com> Co-authored-by: Pooja Trivedi <trivedipooja@microsoft.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: ptrivedi <1638019+ptrivedi@users.noreply.github.com> Co-authored-by: Craig Loewen <crloewen@microsoft.com> Co-authored-by: 1wizkid <richard.fricks@hotmail.com> Co-authored-by: Richard Fricks <richfr@microsoft.com> Co-authored-by: Darshak Bhatti <47045043+dabhattimsft@users.noreply.github.com> Co-authored-by: Darshak Bhatti <dabhatti@micorsoft.com> Co-authored-by: g0tmi1k <535942+g0tmi1k@users.noreply.github.com> Co-authored-by: Arch Linux Technical User <65091038+archlinux-github@users.noreply.github.com> Co-authored-by: wangxin12 <sbwap@vip.qq.com> Co-authored-by: Xin Wang (from Dev Box) <xiwang4@microsoft.com> Co-authored-by: FetoiuCatalin <fetoiucatalin@gmail.com> Co-authored-by: Catalin-Emil Fetoiu <cfetoiu@microsoft.com> Co-authored-by: benhillis <17727402+benhillis@users.noreply.github.com> Co-authored-by: Copilot <copilot@github.com>

Kevin Vega committed May 7, 2026 at 09:17 UTC 2bd965637b38356ccbbef80e5eece544d3264e4d
20 files changed +984 -609
src/windows/wslcsession/CMakeLists.txt
+4 -2
@@ -20,16 +20,17 @@ set(SOURCES
20 # Volume management
21 WSLCVhdVolume.cpp
22 WSLCGuestVolume.cpp
23 + WSLCVolumes.cpp
24
25 # Supporting classes
25 - ContainerEventTracker.cpp
26 + DockerEventTracker.cpp
27 DockerHTTPClient.cpp
28 IORelay.cpp
29 ServiceProcessLauncher.cpp
30 )
31
32 set(HEADERS
32 - ContainerEventTracker.h
33 + DockerEventTracker.h
34 DockerHTTPClient.h
35 IORelay.h
36 ServiceProcessLauncher.h
@@ -44,6 +45,7 @@ set(HEADERS
45 WSLCVirtualMachine.h
46 WSLCVhdVolume.h
47 WSLCGuestVolume.h
48 + WSLCVolumes.h
49 IWSLCVolume.h
50 WSLCVolumeMetadata.h)
51
src/windows/wslcsession/ContainerEventTracker.cpp deleted
-194
@@ -1,194 +0,0 @@
1 -/*++
2 -
3 -Copyright (c) Microsoft. All rights reserved.
4 -
5 -Module Name:
6 -
7 - ContainerEventTracker.cpp
8 -
9 -Abstract:
10 -
11 - Contains the implementation of ContainerEventTracker.
12 -
13 ---*/
14 -#include "precomp.h"
15 -#include "ContainerEventTracker.h"
16 -#include "WSLCVirtualMachine.h"
17 -#include <nlohmann/json.hpp>
18 -
19 -using wsl::windows::common::relay::MultiHandleWait;
20 -using wsl::windows::service::wslc::ContainerEventTracker;
21 -using wsl::windows::service::wslc::DockerHTTPClient;
22 -using wsl::windows::service::wslc::WSLCVirtualMachine;
23 -
24 -ContainerEventTracker::ContainerTrackingReference::ContainerTrackingReference(ContainerEventTracker* tracker, size_t id) noexcept :
25 - m_tracker(tracker), m_id(id)
26 -{
27 -}
28 -
29 -ContainerEventTracker::ContainerTrackingReference& ContainerEventTracker::ContainerTrackingReference::operator=(
30 - ContainerEventTracker::ContainerTrackingReference&& other) noexcept
31 -{
32 - Reset();
33 - m_id = other.m_id;
34 - m_tracker = other.m_tracker;
35 -
36 - other.m_tracker = nullptr;
37 - other.m_id = {};
38 -
39 - return *this;
40 -}
41 -
42 -void ContainerEventTracker::ContainerTrackingReference::Reset() noexcept
43 -{
44 - if (m_tracker != nullptr)
45 - {
46 - m_tracker->UnregisterContainerStateUpdates(m_id);
47 - m_tracker = nullptr;
48 - m_id = {};
49 - }
50 -}
51 -
52 -ContainerEventTracker::ContainerTrackingReference::ContainerTrackingReference(ContainerTrackingReference&& other) noexcept :
53 - m_id(other.m_id), m_tracker(other.m_tracker)
54 -{
55 - other.m_tracker = nullptr;
56 - other.m_id = {};
57 -}
58 -
59 -ContainerEventTracker::ContainerTrackingReference::~ContainerTrackingReference() noexcept
60 -{
61 - Reset();
62 -}
63 -
64 -ContainerEventTracker::ContainerEventTracker(DockerHTTPClient& dockerClient, ULONG sessionId, IORelay& relay) :
65 - m_sessionId(sessionId)
66 -{
67 - auto onChunk = [this](const gsl::span<char>& buffer) {
68 - if (!buffer.empty()) // docker inserts empty lines between events, skip those.
69 - {
70 - try
71 - {
72 - OnEvent(std::string_view(buffer.data(), buffer.size()));
73 - }
74 - catch (...)
75 - {
76 - WSL_LOG(
77 - "DockerEventParseError",
78 - TraceLoggingValue(buffer.data(), "Data"),
79 - TraceLoggingValue(wil::ResultFromCaughtException(), "Error"),
80 - TraceLoggingValue(m_sessionId, "SessionId"));
81 - }
82 - }
83 - };
84 -
85 - auto socket = dockerClient.MonitorEvents();
86 -
87 - relay.AddHandle(std::make_unique<common::relay::HTTPChunkBasedReadHandle>(std::move(socket), std::move(onChunk)));
88 -}
89 -
90 -ContainerEventTracker::~ContainerEventTracker()
91 -{
92 - // N.B. No callback should be left when the tracker is destroyed.
93 - WI_ASSERT(m_callbacks.empty());
94 -}
95 -
96 -void ContainerEventTracker::OnEvent(const std::string_view& event)
97 -{
98 - WSL_LOG(
99 - "DockerEvent",
100 - TraceLoggingCountedString(
101 - event.data(), static_cast<UINT16>(std::min(event.size(), static_cast<size_t>(USHRT_MAX))), "Data"),
102 - TraceLoggingValue(m_sessionId, "SessionId"));
103 -
104 - static std::map<std::string, ContainerEvent> events{
105 - {"start", ContainerEvent::Start}, {"die", ContainerEvent::Stop}, {"exec_die", ContainerEvent::ExecDied}};
106 -
107 - auto parsed = nlohmann::json::parse(event);
108 -
109 - auto action = parsed.find("Action");
110 - auto actor = parsed.find("Actor");
111 -
112 - THROW_HR_IF_MSG(
113 - E_INVALIDARG,
114 - action == parsed.end() || actor == parsed.end(),
115 - "Failed to parse json: %.*hs",
116 - static_cast<int>(event.size()),
117 - event.data());
118 -
119 - auto it = events.find(action->get<std::string>());
120 - if (it == events.end())
121 - {
122 - return; // Event is not tracked, dropped.
123 - }
124 -
125 - auto id = actor->find("ID");
126 - THROW_HR_IF_MSG(E_INVALIDARG, id == actor->end(), "Failed to parse json: %.*hs", static_cast<int>(event.size()), event.data());
127 -
128 - auto containerId = id->get<std::string>();
129 -
130 - std::optional<int> exitCode;
131 - std::optional<std::string> execId;
132 - auto attributes = actor->find("Attributes");
133 - if (attributes != actor->end())
134 - {
135 - auto exitCodeEntry = attributes->find("exitCode");
136 - if (exitCodeEntry != attributes->end())
137 - {
138 - exitCode = std::stoi(exitCodeEntry->get<std::string>());
139 - }
140 -
141 - auto execIdEntry = attributes->find("execID");
142 - if (execIdEntry != attributes->end())
143 - {
144 - execId = execIdEntry->get<std::string>();
145 - }
146 - }
147 -
148 - auto timeEntry = parsed.find("time");
149 - THROW_HR_IF_MSG(
150 - E_INVALIDARG, timeEntry == parsed.end(), "Failed to parse time from event: %.*hs", static_cast<int>(event.size()), event.data());
151 - std::uint64_t eventTime = timeEntry->get<std::uint64_t>();
152 -
153 - std::lock_guard lock{m_lock};
154 -
155 - for (const auto& e : m_callbacks)
156 - {
157 - if (e.ContainerId == containerId && (!e.ExecId.has_value() || e.ExecId == execId))
158 - {
159 - e.Callback(it->second, exitCode, eventTime);
160 - }
161 - }
162 -}
163 -
164 -ContainerEventTracker::ContainerTrackingReference ContainerEventTracker::RegisterContainerStateUpdates(
165 - const std::string& ContainerId, ContainerStateChangeCallback&& Callback) noexcept
166 -{
167 - std::lock_guard lock{m_lock};
168 -
169 - auto id = m_callbackId++;
170 - m_callbacks.emplace_back(id, ContainerId, std::optional<std::string>{}, std::move(Callback));
171 -
172 - return ContainerTrackingReference{this, id};
173 -}
174 -
175 -ContainerEventTracker::ContainerTrackingReference ContainerEventTracker::RegisterExecStateUpdates(
176 - const std::string& ContainerId, const std::string& ExecId, ContainerStateChangeCallback&& Callback) noexcept
177 -{
178 - std::lock_guard lock{m_lock};
179 -
180 - auto id = m_callbackId++;
181 - m_callbacks.emplace_back(id, ContainerId, ExecId, std::move(Callback));
182 -
183 - return ContainerTrackingReference{this, id};
184 -}
185 -
186 -void ContainerEventTracker::UnregisterContainerStateUpdates(size_t Id) noexcept
187 -{
188 - std::lock_guard lock{m_lock};
189 -
190 - auto remove = std::ranges::remove_if(m_callbacks, [Id](auto& entry) { return entry.CallbackId == Id; });
191 - WI_ASSERT(remove.size() == 1);
192 -
193 - m_callbacks.erase(remove.begin(), remove.end());
194 -}
\ No newline at end of file
src/windows/wslcsession/ContainerEventTracker.h deleted
-86
@@ -1,86 +0,0 @@
1 -/*++
2 -
3 -Copyright (c) Microsoft. All rights reserved.
4 -
5 -Module Name:
6 -
7 - ContainerEventTracker.h
8 -
9 -Abstract:
10 -
11 - Contains the definition for ContainerEventTracker.
12 -
13 ---*/
14 -
15 -#pragma once
16 -
17 -#include "DockerHTTPClient.h"
18 -#include "IORelay.h"
19 -
20 -namespace wsl::windows::service::wslc {
21 -
22 -class WSLCVirtualMachine;
23 -
24 -enum class ContainerEvent
25 -{
26 - Create,
27 - Start,
28 - Stop,
29 - Exit,
30 - Destroy,
31 - ExecDied
32 -};
33 -
34 -class ContainerEventTracker
35 -{
36 -public:
37 - NON_COPYABLE(ContainerEventTracker);
38 - NON_MOVABLE(ContainerEventTracker);
39 -
40 - struct ContainerTrackingReference
41 - {
42 - NON_COPYABLE(ContainerTrackingReference);
43 -
44 - ContainerTrackingReference() = default;
45 - ContainerTrackingReference(ContainerEventTracker* tracker, size_t id) noexcept;
46 - ContainerTrackingReference(ContainerTrackingReference&& other) noexcept;
47 - ~ContainerTrackingReference() noexcept;
48 -
49 - ContainerTrackingReference& operator=(ContainerTrackingReference&&) noexcept;
50 -
51 - void Reset() noexcept;
52 -
53 - size_t m_id;
54 - ContainerEventTracker* m_tracker = nullptr;
55 - };
56 -
57 - using ContainerStateChangeCallback = std::function<void(ContainerEvent, std::optional<int>, std::uint64_t)>;
58 -
59 - ContainerEventTracker(DockerHTTPClient& dockerClient, ULONG sessionId, IORelay& relay);
60 - ~ContainerEventTracker();
61 -
62 - void Stop();
63 -
64 - ContainerTrackingReference RegisterContainerStateUpdates(const std::string& ContainerId, ContainerStateChangeCallback&& Callback) noexcept;
65 - ContainerTrackingReference RegisterExecStateUpdates(const std::string& ContainerId, const std::string& ExecId, ContainerStateChangeCallback&& Callback) noexcept;
66 - void UnregisterContainerStateUpdates(size_t Id) noexcept;
67 -
68 -private:
69 - void OnEvent(const std::string_view& event);
70 - void Run(wil::unique_socket&& Socket);
71 -
72 - struct Callback
73 - {
74 - size_t CallbackId;
75 - std::string ContainerId;
76 - std::optional<std::string> ExecId;
77 - ContainerStateChangeCallback Callback;
78 - };
79 -
80 - std::vector<Callback> m_callbacks;
81 -
82 - ULONG m_sessionId{};
83 - std::recursive_mutex m_lock;
84 - std::atomic<size_t> m_callbackId{0};
85 -};
86 -} // namespace wsl::windows::service::wslc
\ No newline at end of file
src/windows/wslcsession/DockerEventTracker.cpp new
+301
@@ -0,0 +1,301 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + DockerEventTracker.cpp
8 +
9 +Abstract:
10 +
11 + Contains the implementation of DockerEventTracker.
12 +
13 +--*/
14 +#include "precomp.h"
15 +#include "DockerEventTracker.h"
16 +#include "WSLCSession.h"
17 +#include "WSLCVirtualMachine.h"
18 +#include <nlohmann/json.hpp>
19 +
20 +using wsl::windows::service::wslc::DockerEventTracker;
21 +using wsl::windows::service::wslc::DockerHTTPClient;
22 +using wsl::windows::service::wslc::WSLCSession;
23 +using wsl::windows::service::wslc::WSLCVirtualMachine;
24 +
25 +DockerEventTracker::EventTrackingReference::EventTrackingReference(DockerEventTracker* tracker, size_t id) noexcept :
26 + m_tracker(tracker), m_id(id)
27 +{
28 +}
29 +
30 +DockerEventTracker::EventTrackingReference& DockerEventTracker::EventTrackingReference::operator=(DockerEventTracker::EventTrackingReference&& other) noexcept
31 +{
32 + Reset();
33 + m_id = other.m_id;
34 + m_tracker = other.m_tracker;
35 +
36 + other.m_tracker = nullptr;
37 + other.m_id = {};
38 +
39 + return *this;
40 +}
41 +
42 +void DockerEventTracker::EventTrackingReference::Reset() noexcept
43 +{
44 + if (m_tracker != nullptr)
45 + {
46 + m_tracker->UnregisterCallback(m_id);
47 + m_tracker = nullptr;
48 + m_id = {};
49 + }
50 +}
51 +
52 +DockerEventTracker::EventTrackingReference::EventTrackingReference(EventTrackingReference&& other) noexcept :
53 + m_id(other.m_id), m_tracker(other.m_tracker)
54 +{
55 + other.m_tracker = nullptr;
56 + other.m_id = {};
57 +}
58 +
59 +DockerEventTracker::EventTrackingReference::~EventTrackingReference() noexcept
60 +{
61 + Reset();
62 +}
63 +
64 +DockerEventTracker::DockerEventTracker(DockerHTTPClient& dockerClient, WSLCSession& session, IORelay& relay) : m_session(session)
65 +{
66 + auto onChunk = [this](const gsl::span<char>& buffer) {
67 + if (!buffer.empty()) // docker inserts empty lines between events, skip those.
68 + {
69 + try
70 + {
71 + OnEvent(std::string_view(buffer.data(), buffer.size()));
72 + }
73 + catch (...)
74 + {
75 + WSL_LOG(
76 + "DockerEventParseError",
77 + TraceLoggingValue(buffer.data(), "Data"),
78 + TraceLoggingValue(wil::ResultFromCaughtException(), "Error"),
79 + TraceLoggingValue(m_session.Id(), "SessionId"));
80 + }
81 + }
82 + };
83 +
84 + auto socket = dockerClient.MonitorEvents();
85 +
86 + relay.AddHandle(std::make_unique<common::relay::HTTPChunkBasedReadHandle>(std::move(socket), std::move(onChunk)));
87 +}
88 +
89 +DockerEventTracker::~DockerEventTracker()
90 +{
91 + // N.B. No callback should be left when the tracker is destroyed.
92 + WI_ASSERT(m_containerCallbacks.empty());
93 + WI_ASSERT(m_volumeCallbacks.empty());
94 +}
95 +
96 +void DockerEventTracker::OnEvent(const std::string_view& event)
97 +{
98 + WSL_LOG(
99 + "DockerEvent",
100 + TraceLoggingCountedString(
101 + event.data(), static_cast<UINT16>(std::min(event.size(), static_cast<size_t>(USHRT_MAX))), "Data"),
102 + TraceLoggingValue(m_session.Id(), "SessionId"));
103 +
104 + auto parsed = nlohmann::json::parse(event);
105 +
106 + auto action = parsed.find("Action");
107 + THROW_HR_IF_MSG(E_INVALIDARG, action == parsed.end(), "Failed to parse json: %.*hs", static_cast<int>(event.size()), event.data());
108 +
109 + auto timeEntry = parsed.find("time");
110 + THROW_HR_IF_MSG(
111 + E_INVALIDARG, timeEntry == parsed.end(), "Failed to parse time from event: %.*hs", static_cast<int>(event.size()), event.data());
112 + std::uint64_t eventTime = timeEntry->get<std::uint64_t>();
113 +
114 + auto actionStr = action->get<std::string>();
115 +
116 + // Route events by Type field. Docker uses "container", "volume", "network", etc.
117 + auto type = parsed.find("Type");
118 + std::string typeStr = (type != parsed.end()) ? type->get<std::string>() : "container";
119 +
120 + if (typeStr == "container")
121 + {
122 + OnContainerEvent(parsed, actionStr, eventTime);
123 + }
124 + else if (typeStr == "volume")
125 + {
126 + OnVolumeEvent(parsed, actionStr, eventTime);
127 + }
128 +
129 + // Track object creation for WaitForObjectCreated.
130 + auto actor = parsed.find("Actor");
131 + if (actor != parsed.end())
132 + {
133 + auto id = actor->find("ID");
134 + if (id != actor->end())
135 + {
136 + auto objectId = id->get<std::string>();
137 + if (actionStr == "create")
138 + {
139 + std::lock_guard lock{m_lock};
140 + m_createdObjects.insert(objectId);
141 + m_objectCreated.SetEvent();
142 + }
143 + else if (actionStr == "destroy")
144 + {
145 + std::lock_guard lock{m_lock};
146 + m_createdObjects.erase(objectId);
147 + }
148 + }
149 + }
150 +}
151 +
152 +void DockerEventTracker::OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime)
153 +{
154 + static std::map<std::string, ContainerEvent> events{
155 + {"start", ContainerEvent::Start}, {"die", ContainerEvent::Stop}, {"destroy", ContainerEvent::Destroy}, {"exec_die", ContainerEvent::ExecDied}};
156 +
157 + auto actor = parsed.find("Actor");
158 + THROW_HR_IF_MSG(E_INVALIDARG, actor == parsed.end(), "Missing Actor in container event");
159 +
160 + auto id = actor->find("ID");
161 + THROW_HR_IF_MSG(E_INVALIDARG, id == actor->end(), "Missing Actor.ID in container event");
162 +
163 + auto containerId = id->get<std::string>();
164 +
165 + auto it = events.find(action);
166 + if (it == events.end())
167 + {
168 + return; // Event is not tracked, dropped.
169 + }
170 +
171 + std::optional<int> exitCode;
172 + std::optional<std::string> execId;
173 + auto attributes = actor->find("Attributes");
174 + if (attributes != actor->end())
175 + {
176 + auto exitCodeEntry = attributes->find("exitCode");
177 + if (exitCodeEntry != attributes->end())
178 + {
179 + exitCode = std::stoi(exitCodeEntry->get<std::string>());
180 + }
181 +
182 + auto execIdEntry = attributes->find("execID");
183 + if (execIdEntry != attributes->end())
184 + {
185 + execId = execIdEntry->get<std::string>();
186 + }
187 + }
188 +
189 + std::lock_guard lock{m_lock};
190 +
191 + for (const auto& e : m_containerCallbacks)
192 + {
193 + if (e.ContainerId == containerId && (!e.ExecId.has_value() || e.ExecId == execId))
194 + {
195 + e.Callback(it->second, exitCode, eventTime);
196 + }
197 + }
198 +}
199 +
200 +void DockerEventTracker::OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime)
201 +{
202 + static std::map<std::string, VolumeEvent> events{{"create", VolumeEvent::Create}, {"destroy", VolumeEvent::Destroy}};
203 +
204 + auto it = events.find(action);
205 + if (it == events.end())
206 + {
207 + return; // Event is not tracked, dropped.
208 + }
209 +
210 + auto actor = parsed.find("Actor");
211 + THROW_HR_IF_MSG(E_INVALIDARG, actor == parsed.end(), "Missing Actor in volume event");
212 +
213 + auto id = actor->find("ID");
214 + THROW_HR_IF_MSG(E_INVALIDARG, id == actor->end(), "Missing Actor.ID in volume event");
215 +
216 + auto volumeName = id->get<std::string>();
217 +
218 + std::lock_guard lock{m_lock};
219 +
220 + for (const auto& e : m_volumeCallbacks)
221 + {
222 + e.Callback(volumeName, it->second, eventTime);
223 + }
224 +}
225 +
226 +void DockerEventTracker::WaitForObjectCreated(const std::string& ObjectId)
227 +{
228 + constexpr auto c_timeout = std::chrono::seconds{60};
229 +
230 + while (true)
231 + {
232 + {
233 + std::lock_guard lock{m_lock};
234 + if (m_createdObjects.contains(ObjectId))
235 + {
236 + return;
237 + }
238 +
239 + // Reset under the lock so a concurrent OnEvent() that runs after we release the lock
240 + // and before the wait can re-signal the event and unblock us.
241 + m_objectCreated.ResetEvent();
242 + }
243 +
244 + THROW_HR_IF_MSG(
245 + HRESULT_FROM_WIN32(ERROR_TIMEOUT),
246 + !m_session.WaitForEventOrSessionTerminating(m_objectCreated.get(), c_timeout),
247 + "Timed out waiting for Docker create event for object '%hs'",
248 + ObjectId.c_str());
249 + }
250 +}
251 +
252 +DockerEventTracker::EventTrackingReference DockerEventTracker::RegisterContainerStateUpdates(
253 + const std::string& ContainerId, ContainerStateChangeCallback&& Callback) noexcept
254 +{
255 + std::lock_guard lock{m_lock};
256 +
257 + auto id = m_callbackId++;
258 + m_containerCallbacks.emplace_back(id, ContainerId, std::optional<std::string>{}, std::move(Callback));
259 +
260 + return EventTrackingReference{this, id};
261 +}
262 +
263 +DockerEventTracker::EventTrackingReference DockerEventTracker::RegisterExecStateUpdates(
264 + const std::string& ContainerId, const std::string& ExecId, ContainerStateChangeCallback&& Callback) noexcept
265 +{
266 + std::lock_guard lock{m_lock};
267 +
268 + auto id = m_callbackId++;
269 + m_containerCallbacks.emplace_back(id, ContainerId, ExecId, std::move(Callback));
270 +
271 + return EventTrackingReference{this, id};
272 +}
273 +
274 +DockerEventTracker::EventTrackingReference DockerEventTracker::RegisterVolumeUpdates(VolumeEventCallback&& Callback) noexcept
275 +{
276 + std::lock_guard lock{m_lock};
277 +
278 + auto id = m_callbackId++;
279 + m_volumeCallbacks.emplace_back(id, std::move(Callback));
280 +
281 + return EventTrackingReference{this, id};
282 +}
283 +
284 +void DockerEventTracker::UnregisterCallback(size_t Id) noexcept
285 +{
286 + std::lock_guard lock{m_lock};
287 +
288 + // Try container callbacks first.
289 + auto containerRemove = std::ranges::remove_if(m_containerCallbacks, [Id](auto& entry) { return entry.CallbackId == Id; });
290 + if (!containerRemove.empty())
291 + {
292 + WI_ASSERT(containerRemove.size() == 1);
293 + m_containerCallbacks.erase(containerRemove.begin(), containerRemove.end());
294 + return;
295 + }
296 +
297 + // Then volume callbacks.
298 + auto volumeRemove = std::ranges::remove_if(m_volumeCallbacks, [Id](auto& entry) { return entry.CallbackId == Id; });
299 + WI_ASSERT(volumeRemove.size() == 1);
300 + m_volumeCallbacks.erase(volumeRemove.begin(), volumeRemove.end());
301 +}
\ No newline at end of file
src/windows/wslcsession/DockerEventTracker.h new
+106
@@ -0,0 +1,106 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + DockerEventTracker.h
8 +
9 +Abstract:
10 +
11 + Contains the definition for DockerEventTracker.
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +#include "DockerHTTPClient.h"
18 +#include "IORelay.h"
19 +
20 +namespace wsl::windows::service::wslc {
21 +
22 +class WSLCSession;
23 +class WSLCVirtualMachine;
24 +
25 +enum class ContainerEvent
26 +{
27 + Create,
28 + Start,
29 + Stop,
30 + Exit,
31 + Destroy,
32 + ExecDied
33 +};
34 +
35 +enum class VolumeEvent
36 +{
37 + Create,
38 + Destroy
39 +};
40 +
41 +class DockerEventTracker
42 +{
43 +public:
44 + NON_COPYABLE(DockerEventTracker);
45 + NON_MOVABLE(DockerEventTracker);
46 +
47 + struct EventTrackingReference
48 + {
49 + NON_COPYABLE(EventTrackingReference);
50 +
51 + EventTrackingReference() = default;
52 + EventTrackingReference(DockerEventTracker* tracker, size_t id) noexcept;
53 + EventTrackingReference(EventTrackingReference&& other) noexcept;
54 + ~EventTrackingReference() noexcept;
55 +
56 + EventTrackingReference& operator=(EventTrackingReference&&) noexcept;
57 +
58 + void Reset() noexcept;
59 +
60 + size_t m_id;
61 + DockerEventTracker* m_tracker = nullptr;
62 + };
63 +
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);
68 + ~DockerEventTracker();
69 +
70 + EventTrackingReference RegisterContainerStateUpdates(const std::string& ContainerId, ContainerStateChangeCallback&& Callback) noexcept;
71 + EventTrackingReference RegisterExecStateUpdates(const std::string& ContainerId, const std::string& ExecId, ContainerStateChangeCallback&& Callback) noexcept;
72 + EventTrackingReference RegisterVolumeUpdates(VolumeEventCallback&& Callback) noexcept;
73 + void UnregisterCallback(size_t Id) noexcept;
74 +
75 + void WaitForObjectCreated(const std::string& ObjectId);
76 +
77 +private:
78 + void OnEvent(const std::string_view& event);
79 + void OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime);
80 + void OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::uint64_t eventTime);
81 +
82 + struct ContainerCallback
83 + {
84 + size_t CallbackId;
85 + std::string ContainerId;
86 + std::optional<std::string> ExecId;
87 + ContainerStateChangeCallback Callback;
88 + };
89 +
90 + struct VolumeCallback
91 + {
92 + size_t CallbackId;
93 + VolumeEventCallback Callback;
94 + };
95 +
96 + std::vector<ContainerCallback> m_containerCallbacks;
97 + std::vector<VolumeCallback> m_volumeCallbacks;
98 +
99 + _Guarded_by_(m_lock) std::unordered_set<std::string> m_createdObjects;
100 + _Guarded_by_(m_lock) wil::unique_event m_objectCreated { wil::EventOptions::ManualReset };
101 +
102 + WSLCSession& m_session;
103 + std::recursive_mutex m_lock;
104 + std::atomic<size_t> m_callbackId{0};
105 +};
106 +} // namespace wsl::windows::service::wslc
\ No newline at end of file
src/windows/wslcsession/DockerHTTPClient.cpp
+5
@@ -427,6 +427,11 @@ docker_schema::Volume DockerHTTPClient::CreateVolume(const docker_schema::Create
427 return Transaction<docker_schema::CreateVolume>(verb::post, URL::Create("/volumes/create"), Request);
428 }
429
430 +docker_schema::Volume DockerHTTPClient::InspectVolume(const std::string& Name)
431 +{
432 + return Transaction<docker_schema::EmptyRequest, docker_schema::Volume>(verb::get, URL::Create("/volumes/{}", Name));
433 +}
434 +
435 void DockerHTTPClient::RemoveVolume(const std::string& Name)
436 {
437 Transaction(verb::delete_, URL::Create("/volumes/{}", Name));
src/windows/wslcsession/DockerHTTPClient.h
+1
@@ -145,6 +145,7 @@ public:
145
146 // Volume management.
147 common::docker_schema::Volume CreateVolume(const common::docker_schema::CreateVolume& Request);
148 + common::docker_schema::Volume InspectVolume(const std::string& Name);
149 void RemoveVolume(const std::string& Name);
150 std::vector<common::docker_schema::Volume> ListVolumes();
151
src/windows/wslcsession/IWSLCVolume.h
+6
@@ -39,6 +39,12 @@ public:
39 // (e.g. detach/delete the VHD for VHD volumes). Throws on failure.
40 virtual void Delete() = 0;
41
42 + // Called when Docker has already destroyed the volume (e.g. container delete with -v).
43 + // Releases any host-side resources without contacting Docker. Default is a no-op.
44 + virtual void OnDeleted()
45 + {
46 + }
47 +
48 // Returns a JSON string for the COM-facing InspectVolume result.
49 virtual std::string Inspect() const = 0;
50
src/windows/wslcsession/WSLCContainer.cpp
+111 -88
@@ -21,6 +21,7 @@ Abstract:
21 #include "WSLCContainer.h"
22 #include "WSLCProcess.h"
23 #include "WSLCProcessIO.h"
24 +#include "WSLCVolumes.h"
25
26 using wsl::windows::common::COMServiceExecutionContext;
27 using wsl::windows::common::docker_schema::ErrorResponse;
@@ -360,10 +361,7 @@ std::string SerializeContainerMetadata(const WSLCContainerMetadataV1& metadata)
361 return wsl::shared::ToJson(wrapper);
362 }
363
363 -void ProcessNamedVolumes(
364 - const WSLCContainerOptions& containerOptions,
365 - const std::unordered_map<std::string, std::unique_ptr<IWSLCVolume>>& sessionVolumes,
366 - wsl::windows::common::docker_schema::CreateContainer& request)
364 +void ProcessNamedVolumes(const WSLCContainerOptions& containerOptions, wsl::windows::common::docker_schema::CreateContainer& request)
365 {
366 THROW_HR_IF(E_INVALIDARG, containerOptions.NamedVolumesCount > 0 && containerOptions.NamedVolumes == nullptr);
367
@@ -373,13 +371,8 @@ void ProcessNamedVolumes(
371 THROW_HR_IF_NULL_MSG(E_INVALIDARG, nv.Name, "NamedVolume at index %lu has null Name", i);
372 THROW_HR_IF_NULL_MSG(E_INVALIDARG, nv.ContainerPath, "NamedVolume at index %lu has null ContainerPath", i);
373
376 - std::string volumeName = nv.Name;
377 -
378 - THROW_HR_WITH_USER_ERROR_IF(
379 - WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(nv.Name), !sessionVolumes.contains(volumeName));
380 -
374 wsl::windows::common::docker_schema::Mount mount{};
382 - mount.Source = std::move(volumeName);
375 + mount.Source = std::string(nv.Name);
376 mount.Target = std::string(nv.ContainerPath);
377 mount.Type = "volume";
378 mount.ReadOnly = static_cast<bool>(nv.ReadOnly);
@@ -388,23 +381,6 @@ void ProcessNamedVolumes(
381 }
382 }
383
391 -void ValidateNamedVolumes(
392 - const std::vector<wsl::windows::common::docker_schema::Mount>& mounts,
393 - const std::unordered_map<std::string, std::unique_ptr<IWSLCVolume>>& sessionVolumes,
394 - const std::unordered_set<std::string>& anonymousVolumes)
395 -{
396 - for (const auto& mount : mounts)
397 - {
398 - if (mount.Type == "volume" && !mount.Name.empty())
399 - {
400 - THROW_HR_WITH_USER_ERROR_IF(
401 - WSLC_E_VOLUME_NOT_FOUND,
402 - Localization::MessageWslcVolumeNotFound(mount.Name),
403 - !sessionVolumes.contains(mount.Name) && !anonymousVolumes.contains(mount.Name));
404 - }
405 - }
406 -}
407 -
384 void ConfigureLdPathForGpu(std::vector<std::string>& Env)
385 {
386 static constexpr std::string_view ldLibraryPathPrefix = "LD_LIBRARY_PATH=";
@@ -498,7 +474,7 @@ WSLCContainerImpl::WSLCContainerImpl(
474 std::vector<ContainerPortMapping>&& ports,
475 std::map<std::string, std::string>&& labels,
476 std::function<void(const WSLCContainerImpl*)>&& onDeleted,
501 - ContainerEventTracker& EventTracker,
477 + DockerEventTracker& EventTracker,
478 DockerHTTPClient& DockerClient,
479 IORelay& Relay,
480 WSLCContainerState InitialState,
@@ -579,6 +555,24 @@ void WSLCContainerImpl::OnProcessReleased(DockerExecProcessControl* process) noe
555 m_processes.erase(remove.begin(), remove.end());
556 }
557
558 +void WSLCContainerImpl::SetExitCode(int ExitCode) noexcept
559 +{
560 + std::lock_guard processesLock{m_processesLock};
561 + if (m_initProcessControl != nullptr)
562 + {
563 + m_initProcessControl->SetExitCode(ExitCode);
564 + }
565 +}
566 +
567 +void WSLCContainerImpl::SignalInitProcessExit() noexcept
568 +{
569 + std::lock_guard processesLock{m_processesLock};
570 + if (m_initProcessControl != nullptr)
571 + {
572 + m_initProcessControl->SignalExit();
573 + }
574 +}
575 +
576 const std::string& WSLCContainerImpl::Image() const noexcept
577 {
578 return m_image;
@@ -718,7 +712,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, LPCSTR DetachKeys)
712 THROW_DOCKER_USER_ERROR_MSG(e, "Failed to attach to container '%hs' during start", m_id.c_str());
713 }
714
721 - auto control = std::make_unique<DockerContainerProcessControl>(*this, m_dockerClient, m_eventTracker);
715 + auto control = std::make_unique<DockerContainerProcessControl>(*this, m_dockerClient);
716
717 std::lock_guard processesLock{m_processesLock};
718 m_initProcessControl = control.get();
@@ -753,11 +747,14 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, LPCSTR DetachKeys)
747
748 void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional<int> exitCode, std::uint64_t eventTime)
749 {
750 + // We must release m_lock and m_stopLock before the wrapper's destructor calls
751 + // Disconnect(), so in-flight COM callers can drain from COMImplClass::m_callers.
752 unique_com_disconnect comWrapper;
753
754 if (event == ContainerEvent::Stop)
755 {
756 THROW_HR_IF(E_UNEXPECTED, !exitCode.has_value());
757 + SetExitCode(exitCode.value());
758
759 std::unique_lock stopGuard{m_stopLock, std::try_to_lock};
760
@@ -771,35 +768,24 @@ void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional<int> exitCod
768 }
769
770 auto lock = m_lock.lock_exclusive();
774 - auto previousState = m_state;
775 -
776 - // Don't run the deletion logic if the container is already in a stopped / deleted state.
777 - // This can happen if Delete() is called by the user.
778 - if (previousState == WslcContainerStateRunning)
779 - {
780 - Transition(WslcContainerStateExited, eventTime);
781 - ReleaseProcesses();
782 -
783 - ReleaseRuntimeResources();
784 -
785 - if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsRm))
786 - {
787 - comWrapper = DeleteExclusiveLockHeld(WSLCDeleteFlagsDeleteVolumes);
788 - }
789 - }
790 -
791 - // Release m_lock and m_stopLock before the wrapper's destructor calls
792 - // Disconnect(), so in-flight COM callers can drain from COMImplClass::m_callers.
793 - lock.reset();
794 - stopGuard.unlock();
771 + comWrapper = OnStopped(eventTime);
772 }
773 else if (event == ContainerEvent::Destroy)
774 {
775 + WI_ASSERT(!m_destroyEvent.is_signaled());
776 + m_destroyEvent.SetEvent();
777 +
778 auto lock = m_lock.lock_exclusive();
779 +
780 if (m_state != WslcContainerStateDeleted)
781 {
801 - Transition(WslcContainerStateDeleted);
782 + Transition(WslcContainerStateDeleted, eventTime);
783 + comWrapper = ReleaseResources();
784 }
785 +
786 + // Signal init exit after the state transition so awaiters observe state=Deleted
787 + // (and any post-delete cleanup) rather than the prior Running/Exited state.
788 + SignalInitProcessExit();
789 }
790
791 WSL_LOG(
@@ -809,30 +795,12 @@ void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional<int> exitCod
795 TraceLoggingValue((int)event, "Event"));
796 }
797
812 -bool WSLCContainerImpl::WaitForEvent(const wil::unique_event& Event, std::chrono::milliseconds Timeout) const
813 -{
814 - const HANDLE waitHandles[] = {Event.get(), m_wslcSession.SessionTerminatingEvent()};
815 - const DWORD waitResult = WaitForMultipleObjects(RTL_NUMBER_OF(waitHandles), waitHandles, FALSE, gsl::narrow<DWORD>(Timeout.count()));
816 -
817 - switch (waitResult)
818 - {
819 - case WAIT_OBJECT_0:
820 - return true;
821 - case WAIT_OBJECT_0 + 1:
822 - THROW_HR_MSG(E_ABORT, "Session %lu is terminating.", m_wslcSession.Id());
823 - case WAIT_TIMEOUT:
824 - return false;
825 - default:
826 - THROW_LAST_ERROR();
827 - }
828 -}
829 -
798 void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill)
799 {
832 - // N.B. comWrapper must be destructed after m_lock is released.
800 + // N.B. comWrapper must be destructed after m_lock and m_stopLock are released.
801 unique_com_disconnect comWrapper;
802
835 - std::lock_guard stopGuard{m_stopLock};
803 + std::unique_lock stopGuard{m_stopLock};
804 auto lock = m_lock.lock_exclusive();
805
806 if (m_state == WslcContainerStateExited && !Kill)
@@ -892,30 +860,68 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill)
860
861 // Wait for the stop event to get the Docker timestamp.
862 std::optional<std::uint64_t> stopTimestamp;
895 - if (WaitForEvent(m_stopNotification.Event, 60s))
863 + if (m_wslcSession.WaitForEventOrSessionTerminating(m_stopNotification.Event.get(), 60s))
864 {
865 stopTimestamp = m_stopNotification.EventTime.load(std::memory_order_acquire);
866 }
867
900 - Transition(WslcContainerStateExited, stopTimestamp);
868 + comWrapper = OnStopped(stopTimestamp);
869
902 - ReleaseProcesses();
870 + if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsRm))
871 + {
872 + // Release locks before waiting on the docker destroy event: OnEvent(Destroy) takes m_lock,
873 + // and the wrapper's destructor (Disconnect) must run after locks are released.
874 + lock.reset();
875 + stopGuard.unlock();
876 + m_wslcSession.WaitForEventOrSessionTerminating(m_destroyEvent.get(), 60s);
877 + }
878 +}
879
880 +__requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::OnStopped(std::optional<std::uint64_t> stopTimestamp)
881 +{
882 + unique_com_disconnect comWrapper;
883 +
884 + ReleaseProcesses();
885 ReleaseRuntimeResources();
886
906 - if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsRm))
887 + // Only drive state transition + auto-delete if we're still Running. A concurrent
888 + // Delete() may have already moved us to Deleted.
889 + if (m_state == WslcContainerStateRunning)
890 + {
891 + Transition(WslcContainerStateExited, stopTimestamp);
892 +
893 + if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsRm))
894 + {
895 + comWrapper = DeleteExclusiveLockHeld(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes);
896 + }
897 + }
898 +
899 + // For the Rm path, defer init-exit signaling to OnEvent(Destroy) so callers waiting
900 + // on init exit observe destroy-side cleanup first.
901 + if (WI_IsFlagClear(m_containerFlags, WSLCContainerFlagsRm))
902 {
908 - comWrapper = DeleteExclusiveLockHeld(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes);
903 + SignalInitProcessExit();
904 }
905 +
906 + return comWrapper;
907 }
908
909 void WSLCContainerImpl::Delete(WSLCDeleteFlags Flags)
910 {
914 - auto lock = m_lock.lock_exclusive();
915 - auto wrapper = DeleteExclusiveLockHeld(Flags);
916 - lock.reset();
917 -
911 // N.B. wrapper must be destroyed after m_lock is released, since its destructor calls Disconnect().
912 + unique_com_disconnect wrapper;
913 + {
914 + auto lock = m_lock.lock_exclusive();
915 + wrapper = DeleteExclusiveLockHeld(Flags);
916 +
917 + // Wait for the docker destroy event so anonymous volume cleanup is reflected in tracking by
918 + // the time we return. Safe to wait here: OnEvent() signals m_destroyEvent before
919 + // taking m_lock. Callers on the docker event-loop thread (OnEvent) must not wait.
920 + if (WI_IsFlagSet(Flags, WSLCDeleteFlagsDeleteVolumes))
921 + {
922 + m_wslcSession.WaitForEventOrSessionTerminating(m_destroyEvent.get(), 60s);
923 + }
924 + }
925 }
926
927 __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::DeleteExclusiveLockHeld(WSLCDeleteFlags Flags)
@@ -1216,10 +1222,9 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1222 const WSLCContainerOptions& containerOptions,
1223 WSLCSession& wslcSession,
1224 WSLCVirtualMachine& virtualMachine,
1219 - const std::unordered_map<std::string, std::unique_ptr<IWSLCVolume>>& sessionVolumes,
1225 const std::unordered_map<std::string, NetworkEntry>& sessionNetworks,
1226 std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
1222 - ContainerEventTracker& EventTracker,
1227 + DockerEventTracker& EventTracker,
1228 DockerHTTPClient& DockerClient,
1229 IORelay& IoRelay)
1230 {
@@ -1342,7 +1347,7 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1347 THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.Volumes, "Volumes is null with VolumesCount=%lu", containerOptions.VolumesCount);
1348 }
1349
1345 - // Build volume list from container options.
1350 + // Build bind mount list from container options.
1351 std::vector<WSLCVolumeMount> volumes;
1352 volumes.reserve(containerOptions.VolumesCount);
1353
@@ -1419,7 +1424,7 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1424 }
1425 }
1426
1422 - ProcessNamedVolumes(containerOptions, sessionVolumes, request);
1427 + ProcessNamedVolumes(containerOptions, request);
1428
1429 // Configure GPU support if requested.
1430 if (WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsGpu))
@@ -1538,6 +1543,11 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1543 // Inspect the container to fetch its generated name (if needed) and Docker's authoritative Created timestamp.
1544 auto inspectData = DockerClient.InspectContainer(result.Id);
1545
1546 + // Wait for the container create event to be delivered on the Docker event stream so that
1547 + // any events for objects created for the container (e.g. volumes) are delivered before we return
1548 + // from this function.
1549 + EventTracker.WaitForObjectCreated(result.Id);
1550 +
1551 auto container = std::make_unique<WSLCContainerImpl>(
1552 wslcSession,
1553 virtualMachine,
@@ -1565,17 +1575,30 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
1575 const common::docker_schema::ContainerInfo& dockerContainer,
1576 WSLCSession& wslcSession,
1577 WSLCVirtualMachine& virtualMachine,
1568 - const std::unordered_map<std::string, std::unique_ptr<IWSLCVolume>>& sessionVolumes,
1569 - const std::unordered_set<std::string>& anonymousVolumes,
1578 + WSLCVolumes& volumes,
1579 std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
1571 - ContainerEventTracker& EventTracker,
1580 + DockerEventTracker& EventTracker,
1581 DockerHTTPClient& DockerClient,
1582 IORelay& ioRelay)
1583 {
1584 // Extract container name from Docker's names list.
1585 std::string name = ExtractContainerName(dockerContainer.Names, dockerContainer.Id);
1586
1578 - ValidateNamedVolumes(dockerContainer.Mounts, sessionVolumes, anonymousVolumes);
1587 + // Validate that all named volumes mounted by the container were successfully recovered
1588 + // by the volumes manager. If any are missing (e.g. backing VHD removed while the service was
1589 + // down), refuse to open the container so it cannot enter a broken state.
1590 + for (const auto& mount : dockerContainer.Mounts)
1591 + {
1592 + if (mount.Type == "volume" && !mount.Name.empty())
1593 + {
1594 + THROW_HR_IF_MSG(
1595 + E_UNEXPECTED,
1596 + !volumes.ContainsVolume(mount.Name),
1597 + "Cannot open container %hs: referenced volume '%hs' is not available",
1598 + dockerContainer.Id.c_str(),
1599 + mount.Name.c_str());
1600 + }
1601 + }
1602
1603 auto labels(dockerContainer.Labels);
1604 auto metadataIt = labels.find(WSLCContainerMetadataLabel);
src/windows/wslcsession/WSLCContainer.h
+15 -11
@@ -16,7 +16,7 @@ Abstract:
16
17 #include "ServiceProcessLauncher.h"
18 #include "WSLCSession.h"
19 -#include "ContainerEventTracker.h"
19 +#include "DockerEventTracker.h"
20 #include "DockerHTTPClient.h"
21 #include "WSLCProcessControl.h"
22 #include "IORelay.h"
@@ -31,6 +31,7 @@ namespace wsl::windows::service::wslc {
31
32 class WSLCContainer;
33 class WSLCSession;
34 +class WSLCVolumes;
35
36 class unique_com_disconnect
37 {
@@ -79,7 +80,7 @@ public:
80 std::vector<ContainerPortMapping>&& ports,
81 std::map<std::string, std::string>&& labels,
82 std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
82 - ContainerEventTracker& EventTracker,
83 + DockerEventTracker& EventTracker,
84 DockerHTTPClient& DockerClient,
85 IORelay& Relay,
86 WSLCContainerState InitialState,
@@ -127,10 +128,9 @@ public:
128 const WSLCContainerOptions& Options,
129 WSLCSession& wslcSession,
130 WSLCVirtualMachine& virtualMachine,
130 - const std::unordered_map<std::string, std::unique_ptr<IWSLCVolume>>& SessionVolumes,
131 const std::unordered_map<std::string, NetworkEntry>& SessionNetworks,
132 std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
133 - ContainerEventTracker& EventTracker,
133 + DockerEventTracker& EventTracker,
134 DockerHTTPClient& DockerClient,
135 IORelay& Relay);
136
@@ -138,10 +138,9 @@ public:
138 const common::docker_schema::ContainerInfo& DockerContainer,
139 WSLCSession& wslcSession,
140 WSLCVirtualMachine& virtualMachine,
141 - const std::unordered_map<std::string, std::unique_ptr<IWSLCVolume>>& sessionVolumes,
142 - const std::unordered_set<std::string>& anonymousVolumes,
141 + WSLCVolumes& Volumes,
142 std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
144 - ContainerEventTracker& EventTracker,
143 + DockerEventTracker& EventTracker,
144 DockerHTTPClient& DockerClient,
145 IORelay& Relay);
146
@@ -151,13 +150,16 @@ private:
150 void AllocateBridgedModePorts();
151 void OnEvent(ContainerEvent event, std::optional<int> exitCode, std::uint64_t eventTime);
152
154 - bool WaitForEvent(const wil::unique_event& Event, std::chrono::milliseconds Timeout) const;
155 -
153 __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect ReleaseResources();
154 __requires_exclusive_lock_held(m_lock) void ReleaseRuntimeResources();
155 __requires_exclusive_lock_held(m_lock) void ReleaseProcesses();
156 __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect PrepareDisconnectComWrapper();
157
158 + __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect OnStopped(std::optional<std::uint64_t> stopTimestamp);
159 +
160 + void SetExitCode(int ExitCode) noexcept;
161 + void SignalInitProcessExit() noexcept;
162 +
163 std::unique_ptr<RelayedProcessIO> CreateRelayedProcessIO(wil::unique_handle&& stream, WSLCProcessFlags flags);
164
165 wsl::windows::common::wslc_schema::InspectContainer BuildInspectContainer(const wsl::windows::common::docker_schema::InspectContainer& dockerInspect) const;
@@ -182,6 +184,8 @@ private:
184 wil::unique_event Event{wil::EventOptions::None};
185 } m_stopNotification;
186
187 + wil::unique_event m_destroyEvent{wil::EventOptions::ManualReset};
188 +
189 // Serializes Stop() callers and signals OnEvent that a Stop is in flight.
190 // Must be acquired before m_lock when both are needed.
191 std::mutex m_stopLock;
@@ -196,8 +200,8 @@ private:
200 std::vector<WSLCVolumeMount> m_mountedVolumes;
201 std::map<std::string, std::string> m_labels;
202 Microsoft::WRL::ComPtr<WSLCContainer> m_comWrapper;
199 - ContainerEventTracker& m_eventTracker;
200 - ContainerEventTracker::ContainerTrackingReference m_containerEvents;
203 + DockerEventTracker& m_eventTracker;
204 + DockerEventTracker::EventTrackingReference m_containerEvents;
205 IORelay& m_ioRelay;
206 WSLCContainerNetworkType m_networkingMode{};
207 };
src/windows/wslcsession/WSLCGuestVolume.cpp
+4 -26
@@ -61,10 +61,6 @@ std::unique_ptr<WSLCGuestVolumeImpl> WSLCGuestVolumeImpl::Create(
61 {
62 ValidateDriverOpts(DriverOpts);
63
64 - WSLCVolumeMetadata metadata;
65 - metadata.Driver = WSLCGuestVolumeDriver;
66 - metadata.DriverOpts = DriverOpts;
67 -
64 docker_schema::CreateVolume request{};
65 if (Name != nullptr && Name[0] != '\0')
66 {
@@ -72,7 +68,6 @@ std::unique_ptr<WSLCGuestVolumeImpl> WSLCGuestVolumeImpl::Create(
68 }
69 request.Driver = "local";
70 request.DriverOpts = DriverOpts;
75 - request.Labels = {{WSLCVolumeMetadataLabel, wsl::shared::ToJson(metadata)}};
71
72 // Merge user labels into the Docker volume labels.
73 for (const auto& [key, value] : Labels)
@@ -92,14 +87,6 @@ std::unique_ptr<WSLCGuestVolumeImpl> WSLCGuestVolumeImpl::Create(
87
88 std::unique_ptr<WSLCGuestVolumeImpl> WSLCGuestVolumeImpl::Open(const wsl::windows::common::docker_schema::Volume& Volume, DockerHTTPClient& DockerClient)
89 {
95 - THROW_HR_IF(E_INVALIDARG, !Volume.Labels.has_value());
96 -
97 - auto metadataIt = Volume.Labels->find(WSLCVolumeMetadataLabel);
98 - THROW_HR_IF(E_INVALIDARG, metadataIt == Volume.Labels->end());
99 -
100 - auto metadata = wsl::shared::FromJson<WSLCVolumeMetadata>(metadataIt->second.c_str());
101 - THROW_HR_IF(E_INVALIDARG, metadata.Driver != WSLCGuestVolumeDriver);
102 -
90 THROW_HR_IF(E_INVALIDARG, Volume.Driver != "local");
91
92 if (Volume.Options.has_value())
@@ -107,20 +94,11 @@ std::unique_ptr<WSLCGuestVolumeImpl> WSLCGuestVolumeImpl::Open(const wsl::window
94 ValidateDriverOpts(Volume.Options.value());
95 }
96
110 - // Extract user labels (all labels except our internal metadata label).
111 - std::map<std::string, std::string> userLabels;
112 - for (const auto& [key, value] : *Volume.Labels)
113 - {
114 - if (key != WSLCVolumeMetadataLabel)
115 - {
116 - userLabels[key] = value;
117 - }
118 - }
119 -
120 - auto volume = std::make_unique<WSLCGuestVolumeImpl>(
121 - std::string{Volume.Name}, std::string{Volume.CreatedAt}, std::move(metadata.DriverOpts), std::move(userLabels), DockerClient);
97 + std::map<std::string, std::string> driverOpts = Volume.Options.value_or(std::map<std::string, std::string>{});
98 + std::map<std::string, std::string> labels = Volume.Labels.value_or(std::map<std::string, std::string>{});
99
123 - return volume;
100 + return std::make_unique<WSLCGuestVolumeImpl>(
101 + std::string{Volume.Name}, std::string{Volume.CreatedAt}, std::move(driverOpts), std::move(labels), DockerClient);
102 }
103
104 void WSLCGuestVolumeImpl::Delete()
src/windows/wslcsession/WSLCProcessControl.cpp
+23 -30
@@ -41,12 +41,8 @@ const wil::unique_event& WSLCProcessControl::GetExitEvent() const
41 return m_exitEvent;
42 }
43
44 -DockerContainerProcessControl::DockerContainerProcessControl(WSLCContainerImpl& Container, DockerHTTPClient& DockerClient, ContainerEventTracker& EventTracker) :
45 - m_container(&Container),
46 - m_client(DockerClient),
47 - m_trackingReference(EventTracker.RegisterContainerStateUpdates(
48 - Container.ID(),
49 - std::bind(&DockerContainerProcessControl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)))
44 +DockerContainerProcessControl::DockerContainerProcessControl(WSLCContainerImpl& Container, DockerHTTPClient& DockerClient) :
45 + m_container(&Container), m_client(DockerClient)
46 {
47 }
48
@@ -70,19 +66,23 @@ void DockerContainerProcessControl::ResizeTty(ULONG Rows, ULONG Columns)
66 m_client.ResizeContainerTty(m_container->ID(), Rows, Columns);
67 }
68
73 -void DockerContainerProcessControl::OnEvent(ContainerEvent Event, std::optional<int> ExitCode, std::uint64_t /*eventTime*/)
69 +void DockerContainerProcessControl::SetExitCode(int ExitCode)
70 {
75 - if (Event == ContainerEvent::Stop)
71 + std::lock_guard lock{m_lock};
72 + if (!m_exitedCode.has_value())
73 {
77 - std::lock_guard lock{m_lock};
78 - if (!m_exitEvent.is_signaled())
79 - {
80 - WSL_LOG("ContainerProcessStop");
81 - WI_ASSERT(ExitCode.has_value());
82 - WI_ASSERT(!m_exitedCode.has_value());
83 - m_exitedCode = ExitCode.value();
84 - m_exitEvent.SetEvent();
85 - }
74 + m_exitedCode = ExitCode;
75 + }
76 +}
77 +
78 +void DockerContainerProcessControl::SignalExit()
79 +{
80 + std::lock_guard lock{m_lock};
81 + if (!m_exitEvent.is_signaled())
82 + {
83 + WSL_LOG("ContainerProcessStop");
84 + WI_ASSERT(m_exitedCode.has_value());
85 + m_exitEvent.SetEvent();
86 }
87 }
88
@@ -93,17 +93,10 @@ int DockerContainerProcessControl::GetPid() const
93
94 void DockerContainerProcessControl::OnContainerReleased() noexcept
95 {
96 - {
97 - std::lock_guard lock{m_lock};
98 -
99 - WI_ASSERT(m_container != nullptr);
100 - m_container = nullptr;
101 - }
96 + std::lock_guard lock{m_lock};
97
103 - // N.B. The caller might keep a reference to the process even after the container is released.
104 - // If that happens, make sure that the state tracking can't outlive the session.
105 - // This is safe to call without the lock because removing the tracking reference is protected by the event tracker lock.
106 - m_trackingReference.Reset();
98 + WI_ASSERT(m_container != nullptr);
99 + m_container = nullptr;
100
101 // Signal the exit event to prevent callers from being blocked on it.
102 if (!m_exitEvent.is_signaled())
@@ -114,11 +107,11 @@ void DockerContainerProcessControl::OnContainerReleased() noexcept
107 }
108
109 DockerExecProcessControl::DockerExecProcessControl(
117 - WSLCContainerImpl& Container, const std::string& Id, DockerHTTPClient& DockerClient, ContainerEventTracker& EventTracker) :
110 + WSLCContainerImpl& Container, const std::string& Id, DockerHTTPClient& DockerClient, DockerEventTracker& EventTracker) :
111 m_container(&Container),
112 m_id(Id),
113 m_client(DockerClient),
121 - m_trackingReference(EventTracker.RegisterExecStateUpdates(
114 + m_eventTrackingReference(EventTracker.RegisterExecStateUpdates(
115 Container.ID(), Id, std::bind(&DockerExecProcessControl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)))
116 {
117 }
@@ -197,7 +190,7 @@ void DockerExecProcessControl::OnContainerReleased() noexcept
190 // If that happens, make sure that the state tracking can't outlive the session.
191 // This is safe to call without the lock because removing the tracking reference is protected by the event tracker lock.
192
200 - m_trackingReference.Reset();
193 + m_eventTrackingReference.Reset();
194
195 // Signal the exit event to prevent callers being blocked on it.
196 if (!m_exitEvent.is_signaled())
src/windows/wslcsession/WSLCProcessControl.h
+8 -7
@@ -15,7 +15,7 @@ Abstract:
15 #pragma once
16 #include "wslc.h"
17 #include "DockerHTTPClient.h"
18 -#include "ContainerEventTracker.h"
18 +#include "DockerEventTracker.h"
19
20 namespace wsl::windows::service::wslc {
21
@@ -42,26 +42,27 @@ protected:
42 class DockerContainerProcessControl : public WSLCProcessControl
43 {
44 public:
45 - DockerContainerProcessControl(WSLCContainerImpl& Container, DockerHTTPClient& DockerClient, ContainerEventTracker& EventTracker);
45 + DockerContainerProcessControl(WSLCContainerImpl& Container, DockerHTTPClient& DockerClient);
46 ~DockerContainerProcessControl();
47 void Signal(int Signal) override;
48 void ResizeTty(ULONG Rows, ULONG Columns) override;
49 int GetPid() const override;
50 void OnContainerReleased() noexcept;
51
52 -private:
53 - void OnEvent(ContainerEvent Event, std::optional<int> ExitCode, std::uint64_t eventTime);
52 + // Records the exit code observed from Docker. Idempotent: first call wins.
53 + void SetExitCode(int ExitCode);
54 + void SignalExit();
55
56 +private:
57 std::mutex m_lock;
58 DockerHTTPClient& m_client;
59 WSLCContainerImpl* m_container{};
58 - ContainerEventTracker::ContainerTrackingReference m_trackingReference;
60 };
61
62 class DockerExecProcessControl : public WSLCProcessControl
63 {
64 public:
64 - DockerExecProcessControl(WSLCContainerImpl& Container, const std::string& Id, DockerHTTPClient& DockerClient, ContainerEventTracker& EventTracker);
65 + DockerExecProcessControl(WSLCContainerImpl& Container, const std::string& Id, DockerHTTPClient& DockerClient, DockerEventTracker& EventTracker);
66 ~DockerExecProcessControl();
67 void Signal(int Signal) override;
68 void ResizeTty(ULONG Rows, ULONG Columns) override;
@@ -79,7 +80,7 @@ private:
80 std::optional<int> m_pid{};
81 DockerHTTPClient& m_client;
82 WSLCContainerImpl* m_container{};
82 - ContainerEventTracker::ContainerTrackingReference m_trackingReference;
83 + DockerEventTracker::EventTrackingReference m_eventTrackingReference;
84 };
85
86 class VMProcessControl : public WSLCProcessControl
src/windows/wslcsession/WSLCSession.cpp
+40 -132
@@ -261,15 +261,16 @@ try
261 m_dockerClient.emplace(std::move(channel), m_virtualMachine->TerminatingEvent(), m_virtualMachine->VmId(), 10 * 1000);
262
263 // Start the event tracker.
264 - m_eventTracker.emplace(m_dockerClient.value(), m_id, m_ioRelay);
264 + m_eventTracker.emplace(m_dockerClient.value(), *this, m_ioRelay);
265 +
266 + m_volumes.emplace(m_dockerClient.value(), m_virtualMachine.value(), m_eventTracker.value(), m_storageVhdPath.parent_path());
267
268 // Monitor for unexpected VM exit.
269 m_ioRelay.AddHandle(
270 std::make_unique<windows::common::relay::EventHandle>(m_vmExitedEvent.get(), std::bind(&WSLCSession::OnVmExited, this)));
271
270 - // Recover any existing containers from storage.
272 + // Recover any existing resources from storage.
273 RecoverExistingNetworks();
272 - RecoverExistingVolumes();
274 RecoverExistingContainers();
275
276 errorCleanup.release();
@@ -1634,13 +1635,12 @@ try
1635
1636 try
1637 {
1637 - std::scoped_lock lock(m_containersLock, m_volumesLock, m_networksLock);
1638 + std::scoped_lock lock(m_containersLock, m_networksLock);
1639
1640 auto& it = m_containers.emplace_back(WSLCContainerImpl::Create(
1641 *containerOptions,
1642 *this,
1643 m_virtualMachine.value(),
1643 - m_volumes,
1644 m_networks,
1645 std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1),
1646 m_eventTracker.value(),
@@ -1929,54 +1929,18 @@ try
1929 RETURN_HR_IF_NULL(E_POINTER, VolumeInfo);
1930 ZeroMemory(VolumeInfo, sizeof(*VolumeInfo));
1931
1932 - // Default driver to "guest" if not specified.
1933 - std::string driver = (Options->Driver != nullptr && *Options->Driver != '\0') ? Options->Driver : WSLCGuestVolumeDriver;
1934 -
1935 - THROW_HR_WITH_USER_ERROR_IF(
1936 - E_INVALIDARG, Localization::MessageWslcInvalidVolumeType(driver), driver != WSLCVhdVolumeDriver && driver != WSLCGuestVolumeDriver);
1937 -
1932 auto driverOpts = wslutil::ParseKeyValuePairs(Options->DriverOpts, Options->DriverOptsCount);
1933 auto labels = wslutil::ParseKeyValuePairs(Options->Labels, Options->LabelsCount, WSLCVolumeMetadataLabel);
1934
1935 auto lock = m_lock.lock_shared();
1942 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient);
1943 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
1944 -
1945 - std::lock_guard volumesLock(m_volumesLock);
1936 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes);
1937
1938 if (Options->Name != nullptr && Options->Name[0] != '\0')
1939 {
1940 ValidateName(Options->Name, WSLC_MAX_VOLUME_NAME_LENGTH);
1950 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), m_volumes.contains(Options->Name));
1951 - }
1952 -
1953 - std::unique_ptr<IWSLCVolume> volume;
1954 - if (driver == WSLCVhdVolumeDriver)
1955 - {
1956 - WSL_LOG("VolumeCreatedVhdDriver", TraceLoggingValue(Options->Name ? Options->Name : "", "VolumeName"));
1957 - volume = WSLCVhdVolumeImpl::Create(
1958 - Options->Name,
1959 - std::move(driverOpts),
1960 - std::move(labels),
1961 - m_storageVhdPath.parent_path(),
1962 - m_virtualMachine.value(),
1963 - m_dockerClient.value());
1941 }
1965 - else
1966 - {
1967 - WI_ASSERT(driver == WSLCGuestVolumeDriver);
1968 - volume = WSLCGuestVolumeImpl::Create(Options->Name, std::move(driverOpts), std::move(labels), m_dockerClient.value());
1969 - }
1970 -
1971 - const auto& name = volume->Name();
1972 - auto info = volume->GetVolumeInformation();
1942
1974 - auto [it, inserted] = m_volumes.insert({name, std::move(volume)});
1975 - WI_VERIFY(inserted);
1976 -
1977 - WSL_LOG("VolumeCreated", TraceLoggingValue(name.c_str(), "VolumeName"));
1978 -
1979 - *VolumeInfo = info;
1943 + *VolumeInfo = m_volumes->CreateVolume(Options->Name, Options->Driver, std::move(driverOpts), std::move(labels));
1944 return S_OK;
1945 }
1946 CATCH_RETURN();
@@ -1987,22 +1951,11 @@ try
1951 COMServiceExecutionContext context;
1952
1953 RETURN_HR_IF_NULL(E_POINTER, Name);
1990 - std::string name = Name;
1991 - ValidateName(name.c_str(), WSLC_MAX_VOLUME_NAME_LENGTH);
1954
1955 auto lock = m_lock.lock_shared();
1994 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient);
1995 - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
1996 -
1997 - std::lock_guard volumesLock(m_volumesLock);
1998 -
1999 - auto it = m_volumes.find(name);
2000 - THROW_HR_WITH_USER_ERROR_IF(WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(name), it == m_volumes.end());
2001 -
2002 - it->second->Delete();
2003 - m_volumes.erase(it);
2004 - WSL_LOG("VolumeDeleted", TraceLoggingValue(name.c_str(), "VolumeName"));
1956 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes);
1957
1958 + m_volumes->DeleteVolume(Name);
1959 return S_OK;
1960 }
1961 CATCH_RETURN();
@@ -2019,25 +1972,23 @@ try
1972 *Count = 0;
1973
1974 auto lock = m_lock.lock_shared();
2022 - std::lock_guard volumesLock(m_volumesLock);
1975 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes);
1976 +
1977 + auto volumeList = m_volumes->ListVolumes();
1978
2024 - if (m_volumes.empty())
1979 + if (volumeList.empty())
1980 {
1981 return S_OK;
1982 }
1983
2029 - auto output = wil::make_unique_cotaskmem<WSLCVolumeInformation[]>(m_volumes.size());
2030 -
2031 - ULONG index = 0;
2032 - for (const auto& [name, vol] : m_volumes)
1984 + auto output = wil::make_unique_cotaskmem<WSLCVolumeInformation[]>(volumeList.size());
1985 + for (size_t i = 0; i < volumeList.size(); i++)
1986 {
2034 - output[index] = vol->GetVolumeInformation();
2035 - index++;
1987 + output[i] = volumeList[i];
1988 }
1989
1990 + *Count = static_cast<ULONG>(volumeList.size());
1991 *Volumes = output.release();
2039 - *Count = index;
2040 -
1992 return S_OK;
1993 }
1994 CATCH_RETURN();
@@ -2056,14 +2007,9 @@ try
2007 ValidateName(name.c_str(), WSLC_MAX_VOLUME_NAME_LENGTH);
2008
2009 auto lock = m_lock.lock_shared();
2059 - std::lock_guard volumesLock(m_volumesLock);
2060 -
2061 - auto it = m_volumes.find(name);
2062 - THROW_HR_WITH_USER_ERROR_IF(WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(name), it == m_volumes.end());
2010 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes);
2011
2064 - const auto& volume = it->second;
2065 -
2066 - std::string json = volume->Inspect();
2012 + std::string json = m_volumes->InspectVolume(name);
2013 *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(json.c_str()).release();
2014
2015 return S_OK;
@@ -2330,6 +2276,25 @@ try
2276 }
2277 CATCH_RETURN();
2278
2279 +bool WSLCSession::WaitForEventOrSessionTerminating(HANDLE Event, std::chrono::milliseconds Timeout) const
2280 +{
2281 + const HANDLE waitHandles[] = {Event, m_sessionTerminatingEvent.get()};
2282 + const DWORD waitResult = WaitForMultipleObjects(RTL_NUMBER_OF(waitHandles), waitHandles, FALSE, gsl::narrow<DWORD>(Timeout.count()));
2283 +
2284 + switch (waitResult)
2285 + {
2286 + case WAIT_OBJECT_0:
2287 + return true;
2288 + case WAIT_OBJECT_0 + 1:
2289 + THROW_HR_MSG(E_ABORT, "Session %lu is terminating.", m_id);
2290 + break;
2291 + case WAIT_TIMEOUT:
2292 + return false;
2293 + default:
2294 + THROW_LAST_ERROR();
2295 + }
2296 +}
2297 +
2298 HRESULT WSLCSession::Terminate()
2299 try
2300 {
@@ -2387,11 +2352,10 @@ try
2352 WI_VERIFY(sessionLock);
2353
2354 std::lock_guard containersLock(m_containersLock);
2390 - std::lock_guard volumesLock(m_volumesLock);
2355 std::lock_guard networksLock(m_networksLock);
2356
2357 m_containers.clear();
2394 - m_volumes.clear();
2358 + m_volumes.reset();
2359 m_networks.clear();
2360
2361 // Stop the IO relay.
@@ -2695,8 +2659,7 @@ void WSLCSession::RecoverExistingContainers()
2659 dockerContainer,
2660 *this,
2661 m_virtualMachine.value(),
2698 - m_volumes,
2699 - m_anonymousVolumes,
2662 + *m_volumes,
2663 std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1),
2664 m_eventTracker.value(),
2665 m_dockerClient.value(),
@@ -2765,59 +2728,4 @@ void WSLCSession::RecoverExistingNetworks()
2728 TraceLoggingValue(m_networks.size(), "NetworkCount"));
2729 }
2730
2768 -void WSLCSession::RecoverExistingVolumes()
2769 -{
2770 - WI_ASSERT(m_dockerClient.has_value());
2771 - WI_ASSERT(m_virtualMachine.has_value());
2772 -
2773 - auto volumes = m_dockerClient->ListVolumes();
2774 -
2775 - std::lock_guard volumesLock(m_volumesLock);
2776 -
2777 - for (const auto& volume : volumes)
2778 - {
2779 - if (!volume.Labels.has_value() || !volume.Labels->contains(WSLCVolumeMetadataLabel))
2780 - {
2781 - m_anonymousVolumes.insert(volume.Name);
2782 - continue;
2783 - }
2784 -
2785 - try
2786 - {
2787 - WI_ASSERT(!m_volumes.contains(volume.Name));
2788 -
2789 - // Peek at the driver field to decide which implementation to use.
2790 - const auto& metadataJson = volume.Labels->at(WSLCVolumeMetadataLabel);
2791 - auto metadata = wsl::shared::FromJson<WSLCVolumeMetadata>(metadataJson.c_str());
2792 -
2793 - std::unique_ptr<IWSLCVolume> recovered;
2794 - if (metadata.Driver == WSLCVhdVolumeDriver)
2795 - {
2796 - recovered = WSLCVhdVolumeImpl::Open(volume, m_virtualMachine.value(), m_dockerClient.value());
2797 - }
2798 - else if (metadata.Driver == WSLCGuestVolumeDriver)
2799 - {
2800 - recovered = WSLCGuestVolumeImpl::Open(volume, m_dockerClient.value());
2801 - }
2802 - else
2803 - {
2804 - WSL_LOG(
2805 - "VolumeRecoverySkippedUnknownDriver",
2806 - TraceLoggingValue(volume.Name.c_str(), "VolumeName"),
2807 - TraceLoggingValue(metadata.Driver.c_str(), "Driver"));
2808 - continue;
2809 - }
2810 -
2811 - auto [_, inserted] = m_volumes.insert({volume.Name, std::move(recovered)});
2812 - WI_VERIFY(inserted);
2813 - }
2814 - CATCH_LOG_MSG("Failed to recover volume: %hs", volume.Name.c_str());
2815 - }
2816 -
2817 - WSL_LOG(
2818 - "VolumesRecovered",
2819 - TraceLoggingValue(m_displayName.c_str(), "SessionName"),
2820 - TraceLoggingValue(m_volumes.size(), "VolumeCount"));
2821 -}
2822 -
2731 } // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCSession.h
+10 -13
@@ -17,12 +17,9 @@ Abstract:
17 #include "wslc.h"
18 #include "WSLCVirtualMachine.h"
19 #include "WSLCContainer.h"
20 -#include "IWSLCVolume.h"
21 -#include "WSLCVhdVolume.h"
22 -#include "WSLCGuestVolume.h"
23 -#include "WSLCVolumeMetadata.h"
20 +#include "WSLCVolumes.h"
21 #include "WSLCNetworkMetadata.h"
25 -#include "ContainerEventTracker.h"
22 +#include "DockerEventTracker.h"
23 #include "DockerHTTPClient.h"
24 #include "IORelay.h"
25 #include <unordered_map>
@@ -166,6 +163,8 @@ public:
163 return m_id;
164 }
165
166 + bool WaitForEventOrSessionTerminating(HANDLE Event, std::chrono::milliseconds Timeout) const;
167 +
168 private:
169 ULONG m_id = 0;
170
@@ -185,7 +184,6 @@ private:
184 int StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs);
185 void ImportImageImpl(DockerHTTPClient::HTTPRequestContext& Request, const WSLCHandle ImageHandle);
186 void RecoverExistingContainers();
188 - void RecoverExistingVolumes();
187 void RecoverExistingNetworks();
188
189 void SaveImageImpl(std::pair<uint32_t, wil::unique_socket>& RequestCodePair, WSLCHandle OutputHandle, HANDLE CancelEvent);
@@ -193,20 +191,19 @@ private:
191
192 std::optional<DockerHTTPClient> m_dockerClient;
193 std::optional<WSLCVirtualMachine> m_virtualMachine;
196 - std::optional<ContainerEventTracker> m_eventTracker;
194 + std::optional<DockerEventTracker> m_eventTracker;
195 wil::unique_event m_dockerdReadyEvent{wil::EventOptions::ManualReset};
196 std::wstring m_displayName;
197 std::filesystem::path m_storageVhdPath;
198 std::filesystem::path m_swapVhdPath;
199
202 - // N.B. m_lock must be acquired before acquiring m_volumesLock, m_containersLock, or m_networksLock.
203 - // These locks protect m_volumes / m_containers without requiring an exclusive m_lock.
204 - // This allows independent operations to proceed while volume/container bookkeeping remains synchronized.
200 + // N.B. m_lock must be acquired before acquiring m_containersLock or m_networksLock.
201 + // These locks protect m_containers without requiring an exclusive m_lock.
202 + // This allows independent operations to proceed while container bookkeeping remains synchronized.
203 + // WSLCVolumes has its own internal srwlock and does not require m_lock.
204 std::mutex m_containersLock;
206 - std::mutex m_volumesLock;
205 std::vector<std::unique_ptr<WSLCContainerImpl>> m_containers;
208 - std::unordered_map<std::string, std::unique_ptr<IWSLCVolume>> m_volumes;
209 - std::unordered_set<std::string> m_anonymousVolumes; // TODO: Implement proper anonymous volume support.
206 + std::optional<WSLCVolumes> m_volumes;
207 std::mutex m_networksLock;
208 std::unordered_map<std::string, NetworkEntry> m_networks;
209 wil::unique_event m_sessionTerminatingEvent{wil::EventOptions::ManualReset};
src/windows/wslcsession/WSLCVhdVolume.h
+1 -1
@@ -78,7 +78,7 @@ public:
78 return m_virtualMachinePath;
79 }
80
81 - void OnDeleted();
81 + void OnDeleted() override;
82
83 private:
84 void Detach();
src/windows/wslcsession/WSLCVolumes.cpp new
+198
@@ -0,0 +1,198 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCVolumes.cpp
8 +
9 +Abstract:
10 +
11 + Contains the implementation of WSLCVolumes.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "WSLCVolumes.h"
17 +#include "WSLCVhdVolume.h"
18 +#include "WSLCGuestVolume.h"
19 +#include "WSLCVirtualMachine.h"
20 +#include "docker_schema.h"
21 +
22 +using wsl::shared::Localization;
23 +
24 +namespace wsl::windows::service::wslc {
25 +
26 +WSLCVolumes::WSLCVolumes(
27 + DockerHTTPClient& dockerClient, WSLCVirtualMachine& virtualMachine, DockerEventTracker& eventTracker, const std::filesystem::path& storagePath) :
28 + m_dockerClient(dockerClient), m_virtualMachine(virtualMachine), m_storagePath(storagePath)
29 +{
30 + // Hold m_lock exclusively across both callback registration and the recovery loop.
31 + // This ensures any volume events that arrive while recovering are queued behind us in OnVolumeEvent,
32 + // and dedup naturally against entries inserted by recovery (insert is a no-op for existing keys).
33 + auto lock = m_lock.lock_exclusive();
34 +
35 + m_volumeEventTracking = eventTracker.RegisterVolumeUpdates(
36 + std::bind(&WSLCVolumes::OnVolumeEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
37 +
38 + for (const auto& volume : dockerClient.ListVolumes())
39 + {
40 + try
41 + {
42 + OpenVolumeExclusiveLockHeld(volume);
43 + }
44 + CATCH_LOG_MSG("Failed to recover volume: %hs", volume.Name.c_str());
45 + }
46 +}
47 +
48 +__requires_lock_held(m_lock) void WSLCVolumes::OpenVolumeExclusiveLockHeld(const wsl::windows::common::docker_schema::Volume& vol)
49 +{
50 + THROW_HR_IF_MSG(E_UNEXPECTED, vol.Driver != "local", "Unrecognized volume driver: %hs", vol.Driver.c_str());
51 +
52 + if (vol.Labels.has_value() && vol.Labels->contains(WSLCVolumeMetadataLabel))
53 + {
54 + auto metadata = wsl::shared::FromJson<WSLCVolumeMetadata>(vol.Labels->at(WSLCVolumeMetadataLabel).c_str());
55 +
56 + if (metadata.Driver == WSLCVhdVolumeDriver)
57 + {
58 + m_volumes.insert({vol.Name, WSLCVhdVolumeImpl::Open(vol, m_virtualMachine, m_dockerClient)});
59 + return;
60 + }
61 + }
62 +
63 + m_volumes.insert({vol.Name, WSLCGuestVolumeImpl::Open(vol, m_dockerClient)});
64 +}
65 +
66 +void WSLCVolumes::OnVolumeEvent(const std::string& volumeName, VolumeEvent event, std::uint64_t)
67 +{
68 + auto lock = m_lock.lock_exclusive();
69 +
70 + // If this event matches the next self-initiated operation we are waiting to observe, the
71 + // map mutation has already been applied by CreateVolume / DeleteVolume. Just pop the event and
72 + // skip updating m_volumes.
73 + if (!m_expectedEvents.empty() && m_expectedEvents.front().first == volumeName && m_expectedEvents.front().second == event)
74 + {
75 + m_expectedEvents.pop_front();
76 + return;
77 + }
78 +
79 + if (event == VolumeEvent::Create)
80 + {
81 + OpenVolumeExclusiveLockHeld(volumeName);
82 + }
83 + else if (event == VolumeEvent::Destroy)
84 + {
85 + OnVolumeDeletedExclusiveLockHeld(volumeName);
86 + }
87 +}
88 +
89 +WSLCVolumeInformation WSLCVolumes::CreateVolume(
90 + LPCSTR Name, LPCSTR Driver, std::map<std::string, std::string>&& DriverOpts, std::map<std::string, std::string>&& Labels)
91 +{
92 + auto lock = m_lock.lock_exclusive();
93 +
94 + if (Name != nullptr && Name[0] != '\0')
95 + {
96 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), m_volumes.contains(Name));
97 + }
98 +
99 + std::string driver = (Driver != nullptr && Driver[0] != '\0') ? Driver : WSLCGuestVolumeDriver;
100 + std::unique_ptr<IWSLCVolume> volume;
101 +
102 + if (driver == WSLCVhdVolumeDriver)
103 + {
104 + volume = WSLCVhdVolumeImpl::Create(Name, std::move(DriverOpts), std::move(Labels), m_storagePath, m_virtualMachine, m_dockerClient);
105 + }
106 + else if (driver == WSLCGuestVolumeDriver)
107 + {
108 + volume = WSLCGuestVolumeImpl::Create(Name, std::move(DriverOpts), std::move(Labels), m_dockerClient);
109 + }
110 + else
111 + {
112 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcInvalidVolumeType(driver));
113 + }
114 +
115 + const auto& name = volume->Name();
116 + auto info = volume->GetVolumeInformation();
117 +
118 + auto [it, inserted] = m_volumes.insert({name, std::move(volume)});
119 + WI_VERIFY(inserted);
120 +
121 + // Record that we initiated this create so OnVolumeEvent ignores the matching docker event.
122 + m_expectedEvents.emplace_back(name, VolumeEvent::Create);
123 +
124 + return info;
125 +}
126 +
127 +void WSLCVolumes::DeleteVolume(LPCSTR Name)
128 +{
129 + THROW_HR_IF(E_POINTER, Name == nullptr);
130 +
131 + auto lock = m_lock.lock_exclusive();
132 +
133 + auto it = m_volumes.find(Name);
134 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(Name), it == m_volumes.end());
135 +
136 + it->second->Delete();
137 + m_volumes.erase(it);
138 +
139 + // Record that we initiated this destroy so OnVolumeEvent ignores the matching docker event.
140 + m_expectedEvents.emplace_back(Name, VolumeEvent::Destroy);
141 +}
142 +
143 +std::vector<WSLCVolumeInformation> WSLCVolumes::ListVolumes() const
144 +{
145 + auto lock = m_lock.lock_shared();
146 +
147 + std::vector<WSLCVolumeInformation> result;
148 + result.reserve(m_volumes.size());
149 +
150 + for (const auto& [name, vol] : m_volumes)
151 + {
152 + result.push_back(vol->GetVolumeInformation());
153 + }
154 +
155 + return result;
156 +}
157 +
158 +std::string WSLCVolumes::InspectVolume(const std::string& Name) const
159 +{
160 + auto lock = m_lock.lock_shared();
161 +
162 + auto it = m_volumes.find(Name);
163 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(Name), it == m_volumes.end());
164 +
165 + return it->second->Inspect();
166 +}
167 +
168 +bool WSLCVolumes::ContainsVolume(const std::string& Name) const
169 +{
170 + auto lock = m_lock.lock_shared();
171 + return m_volumes.contains(Name);
172 +}
173 +
174 +__requires_lock_held(m_lock) void WSLCVolumes::OpenVolumeExclusiveLockHeld(const std::string& volumeName)
175 +{
176 + if (volumeName.empty() || m_volumes.contains(volumeName))
177 + {
178 + return;
179 + }
180 +
181 + try
182 + {
183 + OpenVolumeExclusiveLockHeld(m_dockerClient.InspectVolume(volumeName));
184 + }
185 + CATCH_LOG_MSG("Failed to open volume: %hs", volumeName.c_str());
186 +}
187 +
188 +__requires_lock_held(m_lock) void WSLCVolumes::OnVolumeDeletedExclusiveLockHeld(const std::string& volumeName)
189 +{
190 + auto it = m_volumes.find(volumeName);
191 + if (it != m_volumes.end())
192 + {
193 + it->second->OnDeleted();
194 + m_volumes.erase(it);
195 + }
196 +}
197 +
198 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCVolumes.h new
+65
@@ -0,0 +1,65 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCVolumes.h
8 +
9 +Abstract:
10 +
11 + Contains the definition for WSLCVolumes.
12 +
13 +--*/
14 +
15 +#pragma once
16 +
17 +#include "IWSLCVolume.h"
18 +#include "WSLCVolumeMetadata.h"
19 +#include "DockerHTTPClient.h"
20 +#include "DockerEventTracker.h"
21 +
22 +namespace wsl::windows::service::wslc {
23 +
24 +class WSLCVirtualMachine;
25 +
26 +class WSLCVolumes
27 +{
28 +public:
29 + NON_COPYABLE(WSLCVolumes);
30 + NON_MOVABLE(WSLCVolumes);
31 +
32 + WSLCVolumes(DockerHTTPClient& dockerClient, WSLCVirtualMachine& virtualMachine, DockerEventTracker& eventTracker, const std::filesystem::path& storagePath);
33 + ~WSLCVolumes() = default;
34 +
35 + WSLCVolumeInformation CreateVolume(
36 + _In_opt_ LPCSTR Name,
37 + _In_opt_ LPCSTR Driver,
38 + _In_ std::map<std::string, std::string>&& DriverOpts,
39 + _In_ std::map<std::string, std::string>&& Labels);
40 +
41 + void DeleteVolume(_In_ LPCSTR Name);
42 +
43 + std::vector<WSLCVolumeInformation> ListVolumes() const;
44 + std::string InspectVolume(_In_ const std::string& Name) const;
45 +
46 + bool ContainsVolume(_In_ const std::string& Name) const;
47 +
48 +private:
49 + __requires_lock_held(m_lock) void OpenVolumeExclusiveLockHeld(const wsl::windows::common::docker_schema::Volume& vol);
50 + __requires_lock_held(m_lock) void OpenVolumeExclusiveLockHeld(const std::string& volumeName);
51 + __requires_lock_held(m_lock) void OnVolumeDeletedExclusiveLockHeld(const std::string& volumeName);
52 +
53 + void OnVolumeEvent(const std::string& volumeName, VolumeEvent event, std::uint64_t eventTime);
54 +
55 + mutable wil::srwlock m_lock;
56 + _Guarded_by_(m_lock) std::unordered_map<std::string, std::unique_ptr<IWSLCVolume>> m_volumes;
57 + _Guarded_by_(m_lock) std::deque<std::pair<std::string, VolumeEvent>> m_expectedEvents;
58 +
59 + DockerHTTPClient& m_dockerClient;
60 + WSLCVirtualMachine& m_virtualMachine;
61 + std::filesystem::path m_storagePath;
62 + DockerEventTracker::EventTrackingReference m_volumeEventTracking;
63 +};
64 +
65 +} // namespace wsl::windows::service::wslc
test/windows/WSLCTests.cpp
+80 -17
@@ -2000,21 +2000,21 @@ class WSLCTests
2000 VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build:latest"));
2001 ExpectImagePresent(*m_defaultSession, "wslc-test-build:latest");
2002
2003 - // Lists anonymous docker volume names via the VM's docker CLI.
2004 - // TODO: Add proper support so we can list via session's API instead.
2003 auto listAnonymousVolumes = [&]() {
2006 - auto result = ExpectCommandResult(
2007 - m_defaultSession.get(), {"/usr/bin/docker", "volume", "ls", "-q", "-f", "label=com.docker.volume.anonymous"}, 0);
2004 + wil::unique_cotaskmem_array_ptr<WSLCVolumeInformation> volumes;
2005 + VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address<ULONG>()));
2006 +
2007 std::vector<std::string> names;
2009 - std::stringstream ss(result.Output[1]);
2010 - std::string line;
2011 - while (std::getline(ss, line))
2008 +
2009 + // TODO: Replace with filter for anonymous volumes in ListVolumes API.
2010 + for (const auto& vol : volumes)
2011 {
2013 - if (!line.empty())
2012 + if (std::string(vol.Driver) == "guest")
2013 {
2015 - names.push_back(line);
2014 + names.push_back(vol.Name);
2015 }
2016 }
2017 +
2018 return names;
2019 };
2020
@@ -2022,16 +2022,15 @@ class WSLCTests
2022 {
2023 WSLCContainerLauncher launcher("wslc-test-build:latest", "wslc-test-anonymous-volume", {"test", "-d", "/volume"});
2024 auto container = launcher.Launch(*m_defaultSession);
2025 - auto result = container.GetInitProcess();
2025 + container.SetDeleteOnClose(false);
2026
2027 auto containerId = container.Id();
2028
2029 + auto result = container.GetInitProcess();
2030 ValidateProcessOutput(result, {});
2031
2032 ResetTestSession();
2033
2033 - container.SetDeleteOnClose(false);
2034 -
2034 // Manually cleanup the container and delete anonymous volumes since the session has been reset.
2035 auto containerCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2036 wil::com_ptr<IWSLCContainer> container;
@@ -2058,16 +2057,21 @@ class WSLCTests
2057 container.GetInitProcess().Wait();
2058 container.SetDeleteOnClose(false);
2059
2060 + // Clean up any leaked anonymous volumes when this block exits.
2061 + auto volumeCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2062 + auto volumes = listAnonymousVolumes();
2063 + for (const auto& name : volumes)
2064 + {
2065 + LOG_IF_FAILED(m_defaultSession->DeleteVolume(name.c_str()));
2066 + }
2067 + });
2068 +
2069 VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 1u);
2070
2071 VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
2072
2073 // Anonymous volume was NOT deleted by Docker.
2066 - auto leaked = listAnonymousVolumes();
2067 - VERIFY_ARE_EQUAL(leaked.size(), 1u);
2068 -
2069 - RunCommand(m_defaultSession.get(), {"/usr/bin/docker", "volume", "prune", "-f"});
2070 - VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 0u);
2074 + VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 1u);
2075 }
2076
2077 // Delete container with WSLCDeleteFlagsDeleteVolumes -> anonymous volume is cleaned up.
@@ -3775,6 +3779,65 @@ class WSLCTests
3779 ValidateNamedVolumeContract("guest", nullptr, 0);
3780 }
3781
3782 + WSLC_TEST_METHOD(NamedVolumesStress)
3783 + {
3784 + constexpr unsigned int c_threadCount = 8;
3785 + constexpr unsigned int c_iterationsPerThread = 50;
3786 + const std::string volumeName = "wslc-stress-vol";
3787 +
3788 + // Best-effort cleanup of any leftover volume from prior runs / on test exit.
3789 + auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str())); });
3790 +
3791 + std::atomic<unsigned int> failures = 0;
3792 + std::vector<std::thread> threads;
3793 + threads.reserve(c_threadCount);
3794 +
3795 + for (unsigned int t = 0; t < c_threadCount; ++t)
3796 + {
3797 + threads.emplace_back([&]() {
3798 + for (unsigned int i = 0; i < c_iterationsPerThread; ++i)
3799 + {
3800 + WSLCVolumeOptions volumeOptions{};
3801 + volumeOptions.Name = volumeName.c_str();
3802 + volumeOptions.Driver = "guest";
3803 +
3804 + WSLCVolumeInformation volInfo{};
3805 + HRESULT hrCreate = m_defaultSession->CreateVolume(&volumeOptions, &volInfo);
3806 + if (FAILED(hrCreate) && hrCreate != HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS))
3807 + {
3808 + LogError("CreateVolume(%hs) unexpected HR: 0x%08x", volumeName.c_str(), hrCreate);
3809 + ++failures;
3810 + }
3811 +
3812 + HRESULT hrDelete = m_defaultSession->DeleteVolume(volumeName.c_str());
3813 + if (FAILED(hrDelete) && hrDelete != WSLC_E_VOLUME_NOT_FOUND)
3814 + {
3815 + LogError("DeleteVolume(%hs) unexpected HR: 0x%08x", volumeName.c_str(), hrDelete);
3816 + ++failures;
3817 + }
3818 + }
3819 + });
3820 + }
3821 +
3822 + for (auto& thread : threads)
3823 + {
3824 + thread.join();
3825 + }
3826 +
3827 + VERIFY_ARE_EQUAL(failures.load(), 0u);
3828 +
3829 + // Every thread's iteration ends with a Delete, so the globally-last operation across
3830 + // all threads is guaranteed to be a Delete. The volume must therefore not exist in
3831 + // either our cache or docker -- if either disagrees, our state is desynced from docker.
3832 +
3833 + // Our cache view: InspectVolume must report not-found.
3834 + wil::unique_cotaskmem_ansistring inspectOutput;
3835 + VERIFY_ARE_EQUAL(m_defaultSession->InspectVolume(volumeName.c_str(), &inspectOutput), WSLC_E_VOLUME_NOT_FOUND);
3836 +
3837 + // Docker's view: `docker volume inspect` must also report not-found (non-zero exit).
3838 + ExpectCommandResult(m_defaultSession.get(), {"/usr/bin/docker", "volume", "inspect", volumeName}, 1);
3839 + }
3840 +
3841 // Verifies that a container using a named volume survives a session restart and the volume's data is preserved.
3842 void ValidateNamedVolumeRecoveryContract(std::string_view driver, const WSLCDriverOption* driverOpts, ULONG driverOptsCount)
3843 {
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+6 -2
@@ -632,13 +632,17 @@ class WSLCE2EContainerRunTests
632 result.Verify({.Stdout = L"WSLC Named Volume Test", .Stderr = L"", .ExitCode = 0});
633 }
634
635 - WSLC_TEST_METHOD(WSLCE2E_Container_Run_Volume_NamedVolume_NotFound_Fail)
635 + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Volume_NamedVolume_AutoCreate)
636 {
637 auto result = RunWslc(std::format(
638 L"container run --rm --volume {}:/data {} sh -c \"echo -n 'WSLC Named Volume Test' > /data/test.txt\"",
639 WslcVolumeName,
640 DebianImage.NameAndTag()));
641 - result.Verify({.Stderr = std::format(L"Volume not found: '{}'\r\nError code: WSLC_E_VOLUME_NOT_FOUND\r\n", WslcVolumeName), .ExitCode = 1});
641 + result.Verify({.Stderr = L"", .ExitCode = 0});
642 +
643 + // Verify the volume was auto-created by removing it (fails if it doesn't exist).
644 + result = RunWslc(std::format(L"volume rm {}", WslcVolumeName));
645 + result.Verify({.Stderr = L"", .ExitCode = 0});
646 }
647
648 WSLC_TEST_METHOD(WSLCE2E_Container_Run_WithLabel_Success)