mirror of
https://github.com/microsoft/WSL.git
synced 2026-06-01 01:49:36 -05:00
* 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 commitbf759a092b. * 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 from9c4dba91(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>
6655 lines
276 KiB
C++
6655 lines
276 KiB
C++
/*++
|
|
|
|
Copyright (c) Microsoft. All rights reserved.
|
|
|
|
Module Name:
|
|
|
|
UnitTests.cpp
|
|
|
|
Abstract:
|
|
|
|
This file contains unit tests for WSL.
|
|
|
|
--*/
|
|
|
|
#include "precomp.h"
|
|
|
|
#include "Common.h"
|
|
#include "install.h"
|
|
#include <AclAPI.h>
|
|
#include <fstream>
|
|
#include <filesystem>
|
|
#include "wslservice.h"
|
|
#include "registry.hpp"
|
|
#include "helpers.hpp"
|
|
#include "svccomm.hpp"
|
|
#include "lxfsshares.h"
|
|
#include <userenv.h>
|
|
#include <nlohmann/json.hpp>
|
|
#include "Distribution.h"
|
|
#include "WslCoreConfigInterface.h"
|
|
#include "CommandLine.h"
|
|
|
|
#define LXSST_TEST_USERNAME L"kerneltest"
|
|
|
|
#define LXSST_LXFS_TEST_DIR L"lxfstest"
|
|
#define LXSST_LXFS_MKDIR_COMMAND_LINE \
|
|
L"/bin/bash -c \"mkdir /" LXSST_LXFS_TEST_DIR "; chown 1000:1001 /" LXSST_LXFS_TEST_DIR L"\""
|
|
#define LXSST_LXFS_CLEANUP_COMMAND_LINE L"/bin/bash -c \"rm -rf /" LXSST_LXFS_TEST_DIR L"\""
|
|
#define LXSST_LXFS_TEST_SUB_DIR L"testdir"
|
|
|
|
#define LXSST_FSTAB_BACKUP_COMMAND_LINE L"/bin/bash -c 'cp /etc/fstab /etc/fstab.bak'"
|
|
#define LXSST_FSTAB_SETUP_COMMAND_LINE L"/bin/bash -c 'echo C:\\\\ /mnt/c drvfs metadata 0 0 >> /etc/fstab'"
|
|
#define LXSST_FSTAB_CLEANUP_COMMAND_LINE L"/bin/bash -c \"cp /etc/fstab.bak /etc/fstab\""
|
|
|
|
#define LXSST_TESTS_INSTALL_COMMAND_LINE L"/bin/bash -c 'cd /data/test; ./build_tests.sh'"
|
|
|
|
#define LXSST_IMPORT_DISTRO_TEST_DIR L"C:\\importtest\\"
|
|
|
|
#define LXSST_UID_ROOT 0
|
|
#define LXSST_GID_ROOT 0
|
|
#define LXSST_USERNAME_ROOT L"root"
|
|
|
|
#define LXSS_OOBE_COMPLETE_NAME L"OOBEComplete"
|
|
|
|
constexpr auto c_testDistributionEndpoint = L"http://127.0.0.1:12345/";
|
|
constexpr auto c_testDistributionJson =
|
|
LR"({
|
|
\"Distributions\":[
|
|
{
|
|
\"Name\": \"Debian\",
|
|
\"FriendlyName\": \"Debian\",
|
|
\"StoreAppId\": \"Dummy\",
|
|
\"Amd64\": true,
|
|
\"Arm64\": true,
|
|
\"Amd64PackageUrl\": null,
|
|
\"Arm64PackageUrl\": null,
|
|
\"PackageFamilyName\": \"Dummy\"
|
|
}
|
|
]})";
|
|
|
|
using wsl::windows::common::wslutil::GetSystemErrorString;
|
|
|
|
extern std::wstring g_testDistroPath;
|
|
|
|
namespace UnitTests {
|
|
class UnitTests
|
|
{
|
|
WSL_TEST_CLASS(UnitTests)
|
|
|
|
TEST_CLASS_SETUP(TestClassSetup)
|
|
{
|
|
VERIFY_ARE_EQUAL(LxsstuInitialize(FALSE), TRUE);
|
|
|
|
// Build the unit tests on the Linux side
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(LXSST_TESTS_INSTALL_COMMAND_LINE), (DWORD)0);
|
|
|
|
return true;
|
|
}
|
|
|
|
TEST_CLASS_CLEANUP(TestClassCleanup)
|
|
{
|
|
LxsstuLaunchWsl(LXSST_LXFS_CLEANUP_COMMAND_LINE);
|
|
LxsstuUninitialize(FALSE);
|
|
return true;
|
|
}
|
|
|
|
TEST_METHOD_CLEANUP(MethodCleanup)
|
|
{
|
|
LxssLogKernelOutput();
|
|
return true;
|
|
}
|
|
|
|
// Note: This test should run first since other test cases create files extended attributes, which causes bdstar to emit warnings during export.
|
|
TEST_METHOD(ExportDistro)
|
|
{
|
|
constexpr auto tarPath = L"exported-test-distro.tar";
|
|
constexpr auto vhdPath = L"exported-test-distro.vhdx";
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
|
|
LOG_IF_WIN32_BOOL_FALSE(DeleteFile(tarPath));
|
|
LOG_IF_WIN32_BOOL_FALSE(DeleteFile(vhdPath));
|
|
});
|
|
|
|
{
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {}", LXSS_DISTRO_NAME_TEST_L, tarPath));
|
|
|
|
VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
|
|
// Validate that the file is a valid tar
|
|
{
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"bash -c 'tar tf {} | grep -iF /root/.bashrc'", tarPath));
|
|
VERIFY_ARE_EQUAL(out, L"./root/.bashrc\n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
|
|
// Validate that gzip compression works
|
|
{
|
|
auto [out, err] =
|
|
LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format tar.gz", LXSS_DISTRO_NAME_TEST_L, tarPath));
|
|
|
|
VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"gzip -t {}", tarPath)), 0L);
|
|
}
|
|
|
|
// Verify that xzip compression works
|
|
{
|
|
auto [out, err] =
|
|
LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format tar.xz", LXSS_DISTRO_NAME_TEST_L, tarPath));
|
|
|
|
VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"xz -t {}", tarPath)), 0L);
|
|
}
|
|
|
|
// Validate that exporting as vhd works
|
|
if (LxsstuVmMode())
|
|
{
|
|
WslShutdown(); // TODO: detach disk when distribution is stopped to remove this requirement.
|
|
|
|
auto [out, err] =
|
|
LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format vhd", LXSS_DISTRO_NAME_TEST_L, vhdPath));
|
|
|
|
VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
|
|
auto [vhdType, _] = LxsstuLaunchPowershellAndCaptureOutput(std::format(L"(Get-VHD '{}').VhdType", vhdPath));
|
|
VERIFY_ARE_EQUAL(vhdType, L"Dynamic\r\n");
|
|
}
|
|
else
|
|
{
|
|
auto [out, err] =
|
|
LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format vhd", LXSS_DISTRO_NAME_TEST_L, vhdPath), -1);
|
|
|
|
VERIFY_ARE_EQUAL(out, L"This operation is only supported by WSL2.\r\nError code: Wsl/Service/WSL_E_WSL2_NEEDED\r\n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
}
|
|
|
|
WSL2_TEST_METHOD(SystemdSafeMode)
|
|
{
|
|
SKIP_TEST_UNSTABLE(); // TODO: Re-enable when this issue is solved in main.
|
|
|
|
auto revert = EnableSystemd();
|
|
|
|
// generate a new test config with safe mode enabled
|
|
WslConfigChange config(LxssGenerateTestConfig({.safeMode = true}));
|
|
|
|
// verify that even though systemd is enabled, safe mode prevents it from executing
|
|
VERIFY_IS_FALSE(IsSystemdRunning(L"--system", 1));
|
|
|
|
config.Update(L"");
|
|
|
|
// disable safe mode and verify that it systemd runs
|
|
VERIFY_IS_TRUE(IsSystemdRunning(L"--system"));
|
|
}
|
|
|
|
WSL2_TEST_METHOD(SystemdDisabled)
|
|
{
|
|
// tests that systemd does not run without the wsl.conf option enabled
|
|
// run and check the output of systemctl --system
|
|
VERIFY_IS_FALSE(IsSystemdRunning(L"--system", 1));
|
|
}
|
|
|
|
WSL2_TEST_METHOD(SystemdSystem)
|
|
{
|
|
auto cleanup = wil::scope_exit([] {
|
|
// clean up wsl.conf file
|
|
const std::wstring disableSystemdCmd(LXSST_REMOVE_DISTRO_CONF_COMMAND_LINE);
|
|
LxsstuLaunchWsl(disableSystemdCmd);
|
|
TerminateDistribution();
|
|
});
|
|
|
|
auto revert = EnableSystemd();
|
|
VERIFY_IS_TRUE(IsSystemdRunning(L"--system"));
|
|
|
|
// Validate that systemd-networkd-wait-online.service is masked.
|
|
std::wstring out;
|
|
std::wstring err;
|
|
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(L"systemctl show -p LoadState systemd-networkd-wait-online.service");
|
|
VERIFY_ARE_EQUAL(out, L"LoadState=masked\n");
|
|
|
|
// Validate that NetworkManager-wait-online.service is masked.
|
|
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(L"systemctl show -p LoadState NetworkManager-wait-online.service");
|
|
VERIFY_ARE_EQUAL(out, L"LoadState=masked\n");
|
|
|
|
// Validate that console-getty.service is masked (tty devices are shared at VM level across distros).
|
|
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(L"systemctl show -p LoadState console-getty.service");
|
|
VERIFY_ARE_EQUAL(out, L"LoadState=masked\n");
|
|
}
|
|
|
|
WSL2_TEST_METHOD(SystemdUser)
|
|
{
|
|
// enable systemd before creating the user.
|
|
// if not called first, the runtime directories needed for --user will not have been created
|
|
auto cleanup = EnableSystemd();
|
|
|
|
// create test user and run test as that user
|
|
ULONG TestUid;
|
|
ULONG TestGid;
|
|
CreateUser(LXSST_TEST_USERNAME, &TestUid, &TestGid);
|
|
auto userCleanup = wil::scope_exit([]() { LxsstuLaunchWsl(L"userdel " LXSST_TEST_USERNAME); });
|
|
|
|
auto validateUserSession = [&]() {
|
|
// verify that the user service is running
|
|
const std::wstring isServiceActiveCmd =
|
|
std::format(L"-u {} systemctl is-active user@{}.service ; exit 0", LXSST_TEST_USERNAME, TestUid);
|
|
std::wstring out;
|
|
std::wstring err;
|
|
|
|
try
|
|
{
|
|
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(isServiceActiveCmd.data());
|
|
}
|
|
CATCH_LOG();
|
|
|
|
Trim(out);
|
|
|
|
if (out.compare(L"active") != 0)
|
|
{
|
|
LogError(
|
|
"Unexpected output from systemd: %ls. Stderr: %ls, cmd: %ls", out.c_str(), err.c_str(), isServiceActiveCmd.c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
|
|
// Verify that /run/user/<uid> is a writable tmpfs mount visible in both mount namespaces.
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"touch /run/user/" + std::to_wstring(TestUid) + L"/dummy-test-file"), 0u);
|
|
auto command = L"mount | grep -iF 'tmpfs on /run/user/" + std::to_wstring(TestUid) + L" type tmpfs (rw'";
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(command), 0u);
|
|
|
|
const auto nonElevatedToken = GetNonElevatedToken();
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(command, nullptr, nullptr, nullptr, nonElevatedToken.get()), 0u);
|
|
};
|
|
|
|
// Validate user sessions state with gui apps disabled.
|
|
WslConfigChange config(LxssGenerateTestConfig({.guiApplications = false}));
|
|
{
|
|
validateUserSession();
|
|
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--user {} echo $DISPLAY", LXSST_TEST_USERNAME));
|
|
VERIFY_ARE_EQUAL(out, L"\n");
|
|
|
|
// N.B. The XDG_RUNTIME_DIR variable is always set by init even if gui apps are disabled.
|
|
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--user {} echo $XDG_RUNTIME_DIR", LXSST_TEST_USERNAME));
|
|
VERIFY_ARE_EQUAL(out, std::format(L"/run/user/{}\n", TestUid));
|
|
}
|
|
|
|
// Validate user sessions state with gui apps enabled.
|
|
{
|
|
config.Update(LxssGenerateTestConfig({.guiApplications = true}));
|
|
|
|
validateUserSession();
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--user {} echo $DISPLAY", LXSST_TEST_USERNAME));
|
|
VERIFY_ARE_EQUAL(out, L":0\n");
|
|
|
|
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--user {} echo $XDG_RUNTIME_DIR", LXSST_TEST_USERNAME));
|
|
VERIFY_ARE_EQUAL(out, std::format(L"/run/user/{}\n", TestUid));
|
|
}
|
|
|
|
// Create a 'broken' /run/user and validate that the warning is correctly displayed.
|
|
{
|
|
TerminateDistribution();
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"chmod 000 /run/user"), 0L);
|
|
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-u {} echo OK", LXSST_TEST_USERNAME));
|
|
|
|
VERIFY_ARE_EQUAL(out, L"OK\n");
|
|
VERIFY_ARE_EQUAL(
|
|
err, L"wsl: Failed to start the systemd user session for 'kerneltest'. See journalctl for more details.\n");
|
|
}
|
|
}
|
|
|
|
static bool IsSystemdRunning(const std::wstring& SystemdScope, int ExpectedExitCode = 0)
|
|
{
|
|
// run and check the output of systemctl --system
|
|
const auto systemctlCmd = std::format(L"systemctl '{}' is-system-running ; exit 0", SystemdScope);
|
|
std::wstring out;
|
|
std::wstring error;
|
|
|
|
// capture the output of systemctl and trim for good measure
|
|
try
|
|
{
|
|
std::tie(out, error) = LxsstuLaunchWslAndCaptureOutput(systemctlCmd.c_str(), ExpectedExitCode);
|
|
}
|
|
CATCH_LOG()
|
|
Trim(out);
|
|
|
|
// ensure that systemd is either running in a degraded or running state
|
|
if ((out.compare(L"degraded") == 0) || (out.compare(L"running") == 0))
|
|
{
|
|
return true;
|
|
}
|
|
LogInfo(
|
|
"Error when checking if systemd is running: %ls (scope: %ls, stderr: %ls)", out.c_str(), SystemdScope.c_str(), error.c_str());
|
|
return false;
|
|
}
|
|
|
|
WSL2_TEST_METHOD(SystemdNoClearTmpUnit)
|
|
{
|
|
// The X11 socket is only created when gui applications are enabled.
|
|
WslConfigChange config(LxssGenerateTestConfig({.guiApplications = true}));
|
|
|
|
// ensures that we don't leave state on exit
|
|
auto cleanup = EnableSystemd("initTimeout=0");
|
|
|
|
// Wait for systemd to be started
|
|
VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout<void>(
|
|
[&]() { THROW_HR_IF(E_UNEXPECTED, !IsSystemdRunning(L"--system")); }, std::chrono::seconds(1), std::chrono::minutes(1)));
|
|
|
|
// Validate that the X11 socket has not been deleted
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -d /tmp/.X11-unix"), 0L);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(SystemdBinfmtIsRestored)
|
|
{
|
|
// Override WSL's binfmt interpreter
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"mkdir -p /usr/lib/binfmt.d && echo ':WSLInterop:M::MZ::/bin/echo:PF' > /usr/lib/binfmt.d/dummy.conf"), 0L);
|
|
|
|
auto cleanupBinfmt = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
|
|
LxsstuLaunchWsl(L"rm /usr/lib/binfmt.d/dummy.conf");
|
|
WslShutdown(); // Required since this test registers a custom binfmt interpreter.
|
|
});
|
|
|
|
{
|
|
// Enable systemd (restarts distro).
|
|
auto cleanupSystemd = EnableSystemd();
|
|
|
|
auto validateBinfmt = []() {
|
|
// Validate that WSL's binfmt interpreter is still in place.
|
|
auto [cmdOutput, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo ok");
|
|
VERIFY_ARE_EQUAL(cmdOutput, L"ok\r\n");
|
|
};
|
|
|
|
validateBinfmt();
|
|
|
|
// Validate that this still works after restarting the distribution.
|
|
TerminateDistribution();
|
|
validateBinfmt();
|
|
|
|
// Validate that stopping or restarting systemd-binfmt doesn't break interop.
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl stop systemd-binfmt.service"), 0u);
|
|
validateBinfmt();
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl restart systemd-binfmt.service"), 0u);
|
|
validateBinfmt();
|
|
|
|
// Validate that the unit is regenerated after a daemon-reload.
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl daemon-reload && systemctl restart systemd-binfmt.service"), 0u);
|
|
validateBinfmt();
|
|
}
|
|
|
|
{
|
|
// Enable systemd (restarts distro).
|
|
auto cleanupSystemd = EnableSystemd("protectBinfmt=false");
|
|
|
|
// Validate that WSL's binfmt interpreter is overridden
|
|
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"cmd.exe /c echo ok");
|
|
VERIFY_IS_TRUE(wsl::shared::string::IsEqual(output, L"/mnt/c/Windows/system32/cmd.exe cmd.exe /c echo ok\n", true));
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(Dup)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests dup", L"Dup"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(Epoll)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests epoll", L"Epoll"));
|
|
}
|
|
|
|
TEST_METHOD(EventFd)
|
|
{
|
|
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests eventfd", L"EventFd"));
|
|
}
|
|
|
|
TEST_METHOD(Flock)
|
|
{
|
|
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests flock", L"Flock"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(Fork)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests fork", L"Fork"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(FsCommonLxFs)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests fscommon", L"fscommon_lxfs"));
|
|
}
|
|
|
|
TEST_METHOD(GetSetId)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests get_set_id", L"get_set_id"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(Inotify)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests inotify", L"INOTIFY"));
|
|
}
|
|
|
|
#if !defined(_ARM64_)
|
|
|
|
TEST_METHOD(ResourceLimits)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests resourcelimits", L"resourcelimits"));
|
|
}
|
|
|
|
TEST_METHOD(Select)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests select", L"Select"));
|
|
}
|
|
|
|
#endif
|
|
|
|
TEST_METHOD(Madvise)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests madvise", L"madvise"));
|
|
}
|
|
|
|
TEST_METHOD(Mprotect)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests mprotect", L"mprotect"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(Pipe)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests pipe", L"Pipe"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(Sched)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests sched", L"sched"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(SocketNonblocking)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests socket_nonblock", L"socket_nonblocking"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(Splice)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests splice", L"Splice"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(Sysfs)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests sysfs", L"SysFs"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(Tty)
|
|
{
|
|
auto OriginalHandles = UseOriginalStdHandles();
|
|
|
|
auto Restore = wil::scope_exit([&OriginalHandles]() { RestoreTestStdHandles(OriginalHandles); });
|
|
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests tty", L"tty"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(Utimensat)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests utimensat", L"Utimensat"));
|
|
}
|
|
|
|
TEST_METHOD(WaitPid)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests waitpid", L"WaitPid"));
|
|
}
|
|
|
|
TEST_METHOD(Brk)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests brk", L"brk"));
|
|
}
|
|
|
|
TEST_METHOD(Mremap)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests mremap", L"mremap"));
|
|
}
|
|
|
|
TEST_METHOD(VfsAccess)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests vfsaccess", L"vfsaccess"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(DevPt)
|
|
{
|
|
auto OriginalHandles = UseOriginalStdHandles();
|
|
|
|
auto Restore = wil::scope_exit([&OriginalHandles]() { RestoreTestStdHandles(OriginalHandles); });
|
|
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests dev_pt", L"dev_pt"));
|
|
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests dev_pt_2", L"dev_pt_2"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(Timer)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests timer", L"timer"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(SysInfo)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests sysinfo", L"Sysinfo"));
|
|
}
|
|
|
|
TEST_METHOD(TimerFd)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests timerfd", L"timerfd"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(Ioprio)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests ioprio", L"Ioprio"));
|
|
}
|
|
|
|
TEST_METHOD(Interop)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests interop", L"interop"));
|
|
|
|
//
|
|
// Run wsl.exe with a very long command line. This ensures that the buffer
|
|
// resizing logic that is used by the WSL init daemon is able to correctly
|
|
// handle very long messages.
|
|
//
|
|
// N.B. /bin/true ignores all arguments and always returns 0.
|
|
//
|
|
|
|
std::wstring Command{L"/bin/true "};
|
|
Command += std::wstring(0x1000, L'x');
|
|
VERIFY_IS_TRUE(LxsstuLaunchWsl(Command.c_str()) == 0);
|
|
|
|
// Validate that windows executable can run from the linux filesystem. See: https://github.com/microsoft/WSL/issues/10812
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"cp /mnt/c/Program\\ Files/WSL/wsl.exe /tmp"), 0L);
|
|
auto [out, _] =
|
|
LxsstuLaunchWslAndCaptureOutput(L"WSLENV=WSL_UTF8 WSL_UTF8=1 WSL_INTEROP=/run/WSL/1_interop /tmp/wsl.exe --version");
|
|
|
|
VERIFY_IS_TRUE(out.find(TEXT(WSL_PACKAGE_VERSION)) != std::string::npos);
|
|
}
|
|
|
|
static std::wstring FormUserCommandLine(_In_ const std::wstring& Username, _In_ ULONG Uid, _In_ ULONG Gid)
|
|
{
|
|
return std::format(L"/data/test/wsl_unit_tests user {} {} {}", Username, Uid, Gid);
|
|
}
|
|
|
|
TEST_METHOD(User)
|
|
{
|
|
//
|
|
// Create a test user and run the test as that user.
|
|
//
|
|
|
|
ULONG TestUid;
|
|
ULONG TestGid;
|
|
CreateUser(LXSST_TEST_USERNAME, &TestUid, &TestGid);
|
|
std::wstring CommandLine = FormUserCommandLine(LXSST_TEST_USERNAME, TestUid, TestGid);
|
|
LogInfo("Running test as user %s", LXSST_TEST_USERNAME);
|
|
VERIFY_NO_THROW(LxsstuRunTest(CommandLine.c_str(), L"user", LXSST_TEST_USERNAME));
|
|
|
|
//
|
|
// Add the user to 64 more groups to make sure > 32 groups is supported.
|
|
//
|
|
|
|
{
|
|
DistroFileChange groups(L"/etc/group", true);
|
|
CommandLine = std::format(L"-- for i in $(seq 1 64); do groupadd group$i; usermod -a -G group$i {}; done", LXSST_TEST_USERNAME);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(CommandLine), (DWORD)0);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"{} {} {}", WSL_USER_ARG_LONG, LXSST_TEST_USERNAME, "echo success")), (DWORD)0);
|
|
}
|
|
|
|
//
|
|
// Run the test as root.
|
|
//
|
|
|
|
ULONG RootUid;
|
|
ULONG RootGid;
|
|
CreateUser(LXSST_USERNAME_ROOT, &RootUid, &RootGid);
|
|
CommandLine = FormUserCommandLine(LXSST_USERNAME_ROOT, LXSST_UID_ROOT, LXSST_GID_ROOT);
|
|
LogInfo("Running test as user %s", LXSST_USERNAME_ROOT);
|
|
VERIFY_NO_THROW(LxsstuRunTest(CommandLine.c_str(), L"user", LXSST_USERNAME_ROOT));
|
|
|
|
//
|
|
// Set the default user to the newly created user.
|
|
//
|
|
// N.B. Modifying the default UID should cause the instance to be recreated and the plan9 server launched as the default user.
|
|
//
|
|
|
|
const auto wslSupport =
|
|
wil::CoCreateInstance<LxssUserSession, IWslSupport>(CLSCTX_LOCAL_SERVER | CLSCTX_ENABLE_CLOAKING | CLSCTX_ENABLE_AAA);
|
|
|
|
ULONG Version;
|
|
ULONG DefaultUid;
|
|
wil::unique_cotaskmem_array_ptr<wil::unique_cotaskmem_ansistring> DefaultEnvironment{};
|
|
ULONG WslFlags;
|
|
VERIFY_SUCCEEDED(wslSupport->GetDistributionConfiguration(
|
|
LXSS_DISTRO_NAME_TEST_L, &Version, &DefaultUid, DefaultEnvironment.size_address<ULONG>(), &DefaultEnvironment, &WslFlags));
|
|
|
|
VERIFY_SUCCEEDED(wslSupport->SetDistributionConfiguration(LXSS_DISTRO_NAME_TEST_L, TestUid, WslFlags));
|
|
auto cleanup = wil::scope_exit([&] {
|
|
try
|
|
{
|
|
VERIFY_SUCCEEDED(wslSupport->SetDistributionConfiguration(LXSS_DISTRO_NAME_TEST_L, DefaultUid, WslFlags));
|
|
}
|
|
catch (...)
|
|
{
|
|
LogError("Error while restoring default user");
|
|
}
|
|
});
|
|
|
|
//
|
|
// Create a new file using the 9p server.
|
|
//
|
|
|
|
const std::wstring Path = L"\\\\wsl.localhost\\" LXSS_DISTRO_NAME_TEST_L L"\\data\\test\\default_user_test";
|
|
const wil::unique_hfile File(CreateFile(
|
|
Path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL));
|
|
|
|
if (!File)
|
|
{
|
|
LogError("Failed to create file, error=%lu", GetLastError());
|
|
VERIFY_FAIL();
|
|
}
|
|
|
|
//
|
|
// Ensure the new file was created with the correct uid.
|
|
//
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
LxsstuLaunchWsl(L"stat -c %U /data/test/default_user_test | grep -iF kerneltest", nullptr, nullptr, nullptr, nullptr), 0u);
|
|
}
|
|
|
|
WSL1_TEST_METHOD(Execve)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests execve", L"Execve"));
|
|
}
|
|
|
|
TEST_METHOD(Xattr)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests xattr", L"xattr"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(Namespace)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests namespace", L"Namespace"));
|
|
}
|
|
|
|
TEST_METHOD(BinFmt)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests binfmt", L"BinFmt"));
|
|
|
|
//
|
|
// Perform a shutdown since the binfmt test modifies the binfmt config.
|
|
//
|
|
|
|
WslShutdown();
|
|
}
|
|
|
|
TEST_METHOD(Cgroup)
|
|
{
|
|
//
|
|
// For WSL1, run the cgroup unit test. For WSL2, ensure the cgroupv2 filesystem is mounted in the expected location.
|
|
//
|
|
|
|
if (!LxsstuVmMode())
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests cgroup", L"cgroup"));
|
|
}
|
|
else
|
|
{
|
|
VERIFY_ARE_EQUAL(
|
|
LxsstuLaunchWsl(
|
|
L"mount | grep -iF 'cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)'", nullptr, nullptr, nullptr, nullptr),
|
|
0u);
|
|
}
|
|
}
|
|
|
|
WSL1_TEST_METHOD(Netlink)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests netlink", L"Netlink"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(Random)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests random", L"random"));
|
|
}
|
|
|
|
TEST_METHOD(Keymgmt)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests keymgmt", L"Keymgmt"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(Shm)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests shm", L"shm"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(Sem)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests sem", L"sem"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(Ttys)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests ttys", L"Ttys"));
|
|
}
|
|
|
|
WSL1_TEST_METHOD(OverlayFs)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests overlayfs", L"OverlayFs"));
|
|
}
|
|
|
|
TEST_METHOD(Auxv)
|
|
{
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests auxv", L"auxv"));
|
|
}
|
|
|
|
TEST_METHOD(WslInfo)
|
|
{
|
|
if (LxsstuVmMode())
|
|
{
|
|
// Ensure the `-n` option to not print newline works by validating newline counts.
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode | wc -l | grep 1"), 0u);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode -n | wc -l | grep 0"), 0u);
|
|
|
|
// Ensure various wslinfo functionally works as expected.
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode | grep -iF 'nat'"), 0u);
|
|
|
|
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::None}));
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode | grep -iF 'none'"), 0u);
|
|
|
|
if (AreExperimentalNetworkingFeaturesSupported() && IsHyperVFirewallSupported())
|
|
{
|
|
config.Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode | grep -iF 'mirrored'"), 0u);
|
|
}
|
|
|
|
for (const auto enabled : {true, false})
|
|
{
|
|
config.Update(LxssGenerateTestConfig({.guiApplications = enabled}));
|
|
|
|
#ifdef WSL_DEV_INSTALL_PATH
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
LxsstuLaunchWsl(std::format(L"wslinfo --msal-proxy-path | grep -iF $(wslpath '{}')", TEXT(WSL_DEV_INSTALL_PATH))), 0u);
|
|
|
|
#else
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --msal-proxy-path | grep -iF '/mnt/c/Program Files/WSL/msal.wsl.proxy.exe'"), 0u);
|
|
|
|
#endif
|
|
}
|
|
}
|
|
else
|
|
{
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"wslinfo --networking-mode | grep -iF 'wsl1'"), 0u);
|
|
}
|
|
|
|
{
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"wslinfo --version");
|
|
VERIFY_ARE_EQUAL(out, std::format(L"{}\n", WSL_PACKAGE_VERSION));
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
|
|
{
|
|
// Ensure the old version query command still works.
|
|
const auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"wslinfo --wsl-version");
|
|
VERIFY_ARE_EQUAL(out, std::format(L"{}\n", WSL_PACKAGE_VERSION));
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
|
|
{
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"wslinfo --invalid", 1);
|
|
VERIFY_ARE_EQUAL(out, L"");
|
|
VERIFY_ARE_EQUAL(
|
|
err,
|
|
L"Invalid command line argument: --invalid\nPlease use 'wslinfo --help' to get a list of supported "
|
|
L"arguments.\n");
|
|
}
|
|
|
|
{
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"wslinfo --vm-id -n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
if (LxsstuVmMode())
|
|
{
|
|
// Ensure that the response from wslinfo has the VM ID.
|
|
auto guid = wsl::shared::string::ToGuid(out);
|
|
VERIFY_IS_TRUE(guid.has_value());
|
|
VERIFY_IS_FALSE(IsEqualGUID(guid.value(), GUID_NULL));
|
|
|
|
// Validate that the VM ID is not propagated to user commands.
|
|
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(L"echo -n \"$WSL2_VM_ID\"");
|
|
VERIFY_ARE_EQUAL(out, L"");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
else
|
|
{
|
|
VERIFY_ARE_EQUAL(out, L"wsl1");
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(FsTab)
|
|
{
|
|
//
|
|
// Revert the fstab file and restart the instance so everything is back in
|
|
// the default state after this test.
|
|
//
|
|
|
|
auto cleanup = wil::scope_exit([&] {
|
|
try
|
|
{
|
|
LxsstuLaunchWsl(LXSST_FSTAB_CLEANUP_COMMAND_LINE);
|
|
TerminateDistribution();
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"/bin/true"), 0u);
|
|
}
|
|
catch (...)
|
|
{
|
|
LogError("Error while cleaning up the fstab");
|
|
}
|
|
});
|
|
|
|
//
|
|
// Create an entry in the /etc/fstab file to explicitly mount C:.
|
|
//
|
|
|
|
VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(LXSST_FSTAB_BACKUP_COMMAND_LINE));
|
|
VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(LXSST_FSTAB_SETUP_COMMAND_LINE));
|
|
TerminateDistribution();
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"/bin/true"), 0u);
|
|
|
|
//
|
|
// The test will make sure /mnt/c is mounted with the options specified in
|
|
// /etc/fstab, and that it's mounted only once.
|
|
//
|
|
|
|
VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests fstab", L"fstab"));
|
|
}
|
|
|
|
TEST_METHOD(X11SocketOverTmpMount)
|
|
{
|
|
if (!LxsstuVmMode())
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto cleanup = wil::scope_exit([&] {
|
|
try
|
|
{
|
|
LxsstuLaunchWsl(LXSST_FSTAB_CLEANUP_COMMAND_LINE);
|
|
TerminateDistribution();
|
|
}
|
|
catch (...)
|
|
{
|
|
LogError("Error while cleaning up the fstab");
|
|
}
|
|
});
|
|
|
|
WslConfigChange configChange(LxssGenerateTestConfig({.guiApplications = true}));
|
|
|
|
//
|
|
// Create an entry in the /etc/fstab file to add a tmpfs over /tmp.
|
|
//
|
|
|
|
VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(LXSST_FSTAB_BACKUP_COMMAND_LINE));
|
|
VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(L"echo 'tmpfs /tmp tmpfs rw,nodev,nosuid,size=50M 0 0' > /etc/fstab"));
|
|
TerminateDistribution();
|
|
|
|
auto ValidateBindMount = [](HANDLE Token) {
|
|
//
|
|
// Validate that the bind mount is present.
|
|
//
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L" mount | grep -iF 'none on /tmp/.X11-unix type tmpfs'", nullptr, nullptr, nullptr, Token), 0u);
|
|
};
|
|
|
|
//
|
|
// Verify that /tmp is mounted in both namespaces.
|
|
//
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"mount | grep -iF 'tmpfs on /tmp type tmpfs'", nullptr, nullptr, nullptr, nullptr), 0u);
|
|
|
|
const auto nonElevatedToken = GetNonElevatedToken();
|
|
VERIFY_ARE_EQUAL(
|
|
LxsstuLaunchWsl(L"mount | grep -iF 'tmpfs on /tmp type tmpfs'", nullptr, nullptr, nullptr, nonElevatedToken.get()), 0u);
|
|
|
|
//
|
|
// Validate that the X11 bind mount is present and valid in both namespaces.
|
|
//
|
|
|
|
ValidateBindMount(nullptr);
|
|
ValidateBindMount(nonElevatedToken.get());
|
|
}
|
|
|
|
TEST_METHOD(ImportDistro)
|
|
{
|
|
const auto tarFileName = LXSST_IMPORT_DISTRO_TEST_DIR L"test.tar";
|
|
const auto rootfsDirectoryName = LXSST_IMPORT_DISTRO_TEST_DIR L"rootfs";
|
|
const auto vhdFileName = LXSST_IMPORT_DISTRO_TEST_DIR L"ext4.vhdx";
|
|
auto cleanup = wil::scope_exit([&] {
|
|
try
|
|
{
|
|
VERIFY_IS_TRUE(DeleteFileW(tarFileName));
|
|
VERIFY_IS_TRUE(RemoveDirectoryW(rootfsDirectoryName));
|
|
VERIFY_IS_TRUE(DeleteFileW(vhdFileName));
|
|
VERIFY_IS_TRUE(RemoveDirectoryW(LXSST_IMPORT_DISTRO_TEST_DIR));
|
|
}
|
|
catch (...)
|
|
{
|
|
LogError("Error during cleanup")
|
|
}
|
|
});
|
|
|
|
//
|
|
// Create a dummy tar file, rootfs folder, and vhdx. These will be used
|
|
// to ensure that the user cannot import a distribution over an existing one
|
|
// even if distro registration registry keys are not present.
|
|
//
|
|
|
|
VERIFY_IS_TRUE(CreateDirectoryW(LXSST_IMPORT_DISTRO_TEST_DIR, NULL));
|
|
VERIFY_IS_TRUE(CreateDirectoryW(rootfsDirectoryName, NULL));
|
|
|
|
{
|
|
const wil::unique_hfile tarFile{CreateFileW(
|
|
tarFileName, GENERIC_WRITE, (FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE), NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL)};
|
|
|
|
VERIFY_IS_FALSE(!tarFile);
|
|
|
|
const wil::unique_hfile vhdFile{CreateFileW(
|
|
vhdFileName, GENERIC_WRITE, (FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE), NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL)};
|
|
|
|
VERIFY_IS_FALSE(!vhdFile);
|
|
}
|
|
|
|
auto validateOutput = [](LPCWSTR commandLine, const std::wstring& expectedOutput, DWORD expectedExitCode = -1) {
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(commandLine, expectedExitCode);
|
|
VERIFY_ARE_EQUAL(expectedOutput, out);
|
|
VERIFY_ARE_EQUAL(L"", err);
|
|
};
|
|
|
|
auto version = LxsstuVmMode() ? 2 : 1;
|
|
auto commandLine = std::format(L"--import dummy {} {} --version {}", LXSST_IMPORT_DISTRO_TEST_DIR, tarFileName, version);
|
|
if (LxsstuVmMode())
|
|
{
|
|
validateOutput(
|
|
commandLine.c_str(),
|
|
std::format(
|
|
L"Failed to create disk '{}ext4.vhdx': The file exists. \r\n"
|
|
L"Error code: Wsl/Service/RegisterDistro/ERROR_FILE_EXISTS\r\n",
|
|
LXSST_IMPORT_DISTRO_TEST_DIR));
|
|
}
|
|
else
|
|
{
|
|
validateOutput(
|
|
commandLine.c_str(),
|
|
L"The file exists. \r\n"
|
|
L"Error code: Wsl/Service/RegisterDistro/ERROR_FILE_EXISTS\r\n");
|
|
}
|
|
|
|
commandLine = std::format(L"--import dummy {} {} --version {}", LXSST_IMPORT_DISTRO_TEST_DIR, vhdFileName, version);
|
|
validateOutput(commandLine.c_str(), L"This looks like a VHD file. Use --vhd to import a VHD instead of a tar.\r\n");
|
|
|
|
if (!LxsstuVmMode())
|
|
{
|
|
commandLine = std::format(L"--import dummy {} {} --vhd --version 1", LXSST_IMPORT_DISTRO_TEST_DIR, vhdFileName);
|
|
validateOutput(
|
|
commandLine.c_str(),
|
|
L"This operation is only supported by WSL2.\r\n"
|
|
L"Error code: Wsl/Service/RegisterDistro/WSL_E_WSL2_NEEDED\r\n");
|
|
}
|
|
|
|
//
|
|
// Verify that importing a distribution with a different name into the same path as an
|
|
// already registered distribution (test_distro) returns the path-already-exists error.
|
|
//
|
|
|
|
{
|
|
const auto distroKey = OpenDistributionKey(LXSS_DISTRO_NAME_TEST_L);
|
|
VERIFY_IS_TRUE(!!distroKey);
|
|
|
|
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
|
|
VERIFY_IS_FALSE(basePath.empty());
|
|
|
|
commandLine = std::format(L"--import path-conflict-distro \"{}\" \"{}\" --version {}", basePath, tarFileName, version);
|
|
validateOutput(
|
|
commandLine.c_str(),
|
|
L"The supplied install location is already in use.\r\n"
|
|
L"Error code: Wsl/Service/RegisterDistro/ERROR_FILE_EXISTS\r\n");
|
|
}
|
|
|
|
//
|
|
// Create and import a new distro that where /bin/sh is an absolute symlink.
|
|
//
|
|
|
|
auto newDistroName = L"symlink_distro";
|
|
auto newDistroTar = L"symlink_distro.tar";
|
|
validateOutput(
|
|
std::format(L"--export {} {}", LXSS_DISTRO_NAME_TEST_L, newDistroTar).c_str(),
|
|
L"The operation completed successfully. \r\n",
|
|
0);
|
|
|
|
auto deleteNewDistro = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
VERIFY_IS_TRUE(DeleteFileW(newDistroTar));
|
|
LxsstuLaunchWsl(std::format(L"--unregister {}", newDistroName));
|
|
});
|
|
|
|
validateOutput(
|
|
std::format(L"--import {} . {} --version {}", newDistroName, newDistroTar, version).c_str(),
|
|
L"The operation completed successfully. \r\n",
|
|
0);
|
|
validateOutput(std::format(L"-d {} -- ln -f -s /bin/bash /bin/sh", newDistroName).c_str(), L"", 0);
|
|
validateOutput(
|
|
std::format(L"--export {} {}", newDistroName, newDistroTar).c_str(), L"The operation completed successfully. \r\n", 0);
|
|
validateOutput(std::format(L"--unregister {}", newDistroName).c_str(), L"The operation completed successfully. \r\n", 0);
|
|
validateOutput(
|
|
std::format(L"--import {} . {} --version {}", newDistroName, newDistroTar, version).c_str(),
|
|
L"The operation completed successfully. \r\n",
|
|
0);
|
|
}
|
|
|
|
TEST_METHOD(ImportDistroInvalidTar)
|
|
{
|
|
const auto commandLine = std::format(
|
|
L"--import dummy {} C:\\windows\\system32\\drivers\\etc\\hosts --version {}", LXSST_IMPORT_DISTRO_TEST_DIR, LxsstuVmMode() ? 2 : 1);
|
|
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(commandLine.c_str(), -1);
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
out, L"Importing the distribution failed.\r\nError code: Wsl/Service/RegisterDistro/WSL_E_IMPORT_FAILED\r\n");
|
|
VERIFY_ARE_EQUAL(err, L"bsdtar: Error opening archive: Unrecognized archive format\n");
|
|
}
|
|
|
|
TEST_METHOD(AppxDistroDeletion)
|
|
{
|
|
// Create a dummy distro registration
|
|
const auto key = wsl::windows::common::registry::CreateKey(
|
|
HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Lxss\\{baa405ef-1822-4bbe-84e2-30e4c6330d41}");
|
|
|
|
wsl::windows::common::registry::WriteDword(key.get(), nullptr, L"State", 1);
|
|
wsl::windows::common::registry::WriteString(key.get(), nullptr, L"DistributionName", L"DistroToBeDeleted");
|
|
wsl::windows::common::registry::WriteString(
|
|
key.get(), nullptr, L"PackageFamilyName", L"Microsoft.AppThatIsntInstalledForSure.1.0.0.0_8wekyb3d8bbwe");
|
|
wsl::windows::common::registry::WriteDword(key.get(), nullptr, L"Version", 2);
|
|
|
|
const auto vhdDir = std::filesystem::current_path();
|
|
wsl::windows::common::registry::WriteString(key.get(), nullptr, L"BasePath", vhdDir.c_str());
|
|
wsl::windows::common::registry::WriteDword(key.get(), nullptr, L"DefaultUid", 0);
|
|
wsl::windows::common::registry::WriteDword(key.get(), nullptr, L"Flags", LXSS_DISTRO_FLAGS_VM_MODE);
|
|
|
|
// Create a dummy vhd
|
|
const auto vhdPath = vhdDir.string() + "\\ext4.vhdx";
|
|
|
|
wil::unique_handle vhdHandle(CreateFileA(vhdPath.c_str(), GENERIC_READ, 0, nullptr, CREATE_ALWAYS, 0, nullptr));
|
|
VERIFY_IS_TRUE(vhdHandle.is_valid());
|
|
vhdHandle.reset();
|
|
|
|
wsl::windows::common::SvcComm service;
|
|
auto isDistroListed = [&]() {
|
|
auto distros = service.EnumerateDistributions();
|
|
|
|
return std::find_if(distros.begin(), distros.end(), [&](const auto& e) {
|
|
return wsl::shared::string::IsEqual(e.DistroName, L"DistroToBeDeleted", false);
|
|
}) != distros.end();
|
|
};
|
|
|
|
// The distro should still be there, because the vhd exists.
|
|
VERIFY_IS_TRUE(isDistroListed());
|
|
|
|
// Delete the VHD
|
|
VERIFY_IS_TRUE(DeleteFileA(vhdPath.c_str()));
|
|
|
|
// Now the distro should be deleted.
|
|
VERIFY_IS_FALSE(isDistroListed());
|
|
}
|
|
|
|
// Validate that the default distribution is correctly displayed
|
|
TEST_METHOD(DefaultDistro)
|
|
{
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"--list");
|
|
|
|
VERIFY_IS_TRUE(out.find(std::format(L"{} (Default)", LXSS_DISTRO_NAME_TEST_L)) != std::wstring::npos);
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
|
|
// TODO: Add test coverage for the Linux => Windows code paths of $WSLENV
|
|
TEST_METHOD(WslEnv)
|
|
{
|
|
auto validateEnv = [&](const std::map<std::wstring, std::wstring>& inputVariables,
|
|
const std::map<std::wstring, std::wstring>& expectedOutput) {
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
for (const auto& e : inputVariables)
|
|
{
|
|
THROW_LAST_ERROR_IF(!SetEnvironmentVariable(e.first.c_str(), nullptr));
|
|
}
|
|
});
|
|
|
|
for (const auto& e : inputVariables)
|
|
{
|
|
THROW_LAST_ERROR_IF(!SetEnvironmentVariable(e.first.c_str(), e.second.c_str()));
|
|
}
|
|
|
|
for (const auto& e : expectedOutput)
|
|
{
|
|
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"echo -n $" + e.first);
|
|
|
|
VERIFY_ARE_EQUAL(e.second, output);
|
|
}
|
|
};
|
|
|
|
validateEnv({{L"a", L"b"}, {L"c", L"d"}, {L"WSLENV", L"a/u:c/u"}}, {{L"a", L"b"}, {L"c", L"d"}});
|
|
validateEnv(
|
|
{{L"a", L"C:\\Users"}, {L"b", L"C:\\Users"}, {L"WSLENV", L"a/l:b/p"}},
|
|
{{L"a", L"/mnt/c/Users"}, {L"b", L"/mnt/c/Users"}});
|
|
|
|
validateEnv(
|
|
{{L"a", L"C:\\Users;C:\\Windows"},
|
|
{L"b", L"C:\\Users;C:\\Windows"},
|
|
{L"c", L"C:\\Users;C:\\Windows"},
|
|
{L"d", L"C:\\Users;C:\\Windows"},
|
|
{L"WSLENV", L"a/l:b/p:c/pl:d/lp"}},
|
|
{{L"a", L"/mnt/c/Users:/mnt/c/Windows"},
|
|
{L"b", L"/mnt/c/Users:/mnt/c/Windows"},
|
|
{L"c", L"/mnt/c/Users:/mnt/c/Windows"},
|
|
{L"d", L"/mnt/c/Users:/mnt/c/Windows"}});
|
|
|
|
validateEnv(
|
|
{{L"a", L"C:\\Users;C:\\Windows\\System32"}, {L"b", L"C:\\Users;C:\\Windows"}, {L"WSLENV", L"a/l:b/l:a/l"}},
|
|
{{L"a", L"/mnt/c/Users:/mnt/c/Windows/System32"}, {L"b", L"/mnt/c/Users:/mnt/c/Windows"}});
|
|
|
|
validateEnv(
|
|
{{L"a", L"C:\\Users;C:\\Windows\\System32"}, {L"b", L"C:\\Users;C:\\Windows"}, {L"WSLENV", L"a/u:b/u:a/u"}},
|
|
{{L"a", L"C:\\Users;C:\\Windows\\System32"}, {L"b", L"C:\\Users;C:\\Windows"}});
|
|
|
|
validateEnv({{L"a", L"C:\\Users;C:\\Windows\\System32"}, {L"WSLENV", L"a/w"}}, {{L"a", L""}});
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
|
|
THROW_LAST_ERROR_IF(!SetEnvironmentVariable(L"Empty", nullptr));
|
|
THROW_LAST_ERROR_IF(!SetEnvironmentVariable(L"WSLENV", nullptr));
|
|
});
|
|
|
|
THROW_LAST_ERROR_IF(!SetEnvironmentVariable(L"Empty", L""));
|
|
THROW_LAST_ERROR_IF(!SetEnvironmentVariable(L"WSLENV", L"Empty/u"));
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"[ -z ${Empty+x} ]"), (DWORD)1);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"[ -z ${SanityCheck+x} ]"), (DWORD)0);
|
|
}
|
|
|
|
static void ValidateErrorMessage(
|
|
const std::wstring& Cmd,
|
|
const std::wstring& Message,
|
|
const std::wstring& Code,
|
|
const std::optional<std::wstring>& ExtraConfig = {},
|
|
LPCWSTR EntryPoint = WSL_BINARY_NAME,
|
|
bool ignoreCasing = false)
|
|
{
|
|
std::optional<std::wstring> previousConfig;
|
|
|
|
if (ExtraConfig.has_value())
|
|
{
|
|
previousConfig = LxssWriteWslConfig(L"[wsl2]\n" + ExtraConfig.value());
|
|
RestartWslService();
|
|
}
|
|
|
|
auto revertConfig = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
if (previousConfig.has_value())
|
|
{
|
|
LxssWriteWslConfig(previousConfig.value());
|
|
RestartWslService();
|
|
};
|
|
});
|
|
|
|
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(
|
|
Cmd.c_str(), wcscmp(EntryPoint, L"bash.exe") == 0 ? 1 : -1, nullptr, nullptr, EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, EntryPoint);
|
|
|
|
const auto expectedOutput = Message + L"\r\nError code: " + Code + L"\r\n";
|
|
|
|
if (!wsl::shared::string::IsEqual(output, expectedOutput, ignoreCasing))
|
|
{
|
|
LogError("Expected error message: '%ls', actual error message: '%ls'", expectedOutput.c_str(), output.c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
}
|
|
|
|
static void VerifyOutput(const std::wstring& Cmd, const std::wstring& ExpectedOutput, int ExpectedExitCode = 0, LPCWSTR EntryPoint = WSL_BINARY_NAME)
|
|
{
|
|
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(
|
|
Cmd.c_str(), ExpectedExitCode, nullptr, nullptr, EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, EntryPoint);
|
|
|
|
VERIFY_ARE_EQUAL(output, ExpectedOutput);
|
|
}
|
|
|
|
TEST_METHOD(ErrorMessages)
|
|
{
|
|
if (LxsstuVmMode()) // wsl --mount and bridged networking only exist in WSL2.
|
|
{
|
|
if (!wsl::shared::Arm64 && wsl::windows::common::helpers::GetWindowsVersion().BuildNumber >= 27653)
|
|
{
|
|
ValidateErrorMessage(
|
|
L"--mount DoesNotExist",
|
|
L"Failed to attach disk 'DoesNotExist' to WSL2: The system cannot find the file specified. ",
|
|
L"Wsl/Service/AttachDisk/MountDisk/HCS/ERROR_FILE_NOT_FOUND");
|
|
}
|
|
|
|
ValidateErrorMessage(
|
|
L"--unmount DoesNotExist",
|
|
GetSystemErrorString(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)),
|
|
L"Wsl/Service/DetachDisk/ERROR_FILE_NOT_FOUND");
|
|
|
|
ValidateErrorMessage(
|
|
WSL_MANAGE_ARG L" " LXSS_DISTRO_NAME_TEST L" " WSL_MANAGE_ARG_SET_SPARSE_OPTION_LONG L" false_",
|
|
L"false_ is not a valid boolean, <true|false>",
|
|
L"Wsl/E_INVALIDARG");
|
|
|
|
const std::wstring wslConfigPath = wsl::windows::common::helpers::GetWslConfigPath();
|
|
{
|
|
// Create a distro registration pointing to a vhdx that doesn't exist and validate that the error message reports that correctly.
|
|
|
|
const auto userKey = wsl::windows::common::registry::OpenLxssUserKey();
|
|
const auto distroKey =
|
|
wsl::windows::common::registry::CreateKey(userKey.get(), L"{baa405ef-1822-4bbe-84e2-30e4c6330d42}");
|
|
auto revert = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] {
|
|
wsl::windows::common::registry::DeleteKey(userKey.get(), L"{baa405ef-1822-4bbe-84e2-30e4c6330d42}");
|
|
});
|
|
|
|
wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"BasePath", L"C:\\DoesNotExit");
|
|
wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"DistributionName", L"DummyBrokenDistro");
|
|
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"DefaultUid", 0);
|
|
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"Version", LXSS_DISTRO_VERSION_2);
|
|
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"State", LxssDistributionStateInstalled);
|
|
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"Flags", LXSS_DISTRO_FLAGS_VM_MODE);
|
|
|
|
ValidateErrorMessage(
|
|
L"-d DummyBrokenDistro",
|
|
L"Failed to attach disk 'C:\\DoesNotExit\\ext4.vhdx' to WSL2: The system cannot find the path "
|
|
L"specified. ",
|
|
L"Wsl/Service/CreateInstance/MountDisk/HCS/ERROR_PATH_NOT_FOUND");
|
|
|
|
// Purposefully set an incorrect value type to validate registry error handling.
|
|
wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"Version", L"Broken");
|
|
|
|
const auto tokenInfo = wil::get_token_information<TOKEN_USER>();
|
|
const auto Sid = std::wstring(wsl::windows::common::wslutil::SidToString(tokenInfo->User.Sid).get());
|
|
|
|
// N.B. casing is ignored because the 'Software' key is sometimes uppercase, sometimes not.
|
|
ValidateErrorMessage(
|
|
L"-d DummyBrokenDistro",
|
|
L"An error occurred accessing the registry. Path: '\\REGISTRY\\USER\\" + Sid +
|
|
L"\\Software\\Microsoft\\Windows\\CurrentVersion\\Lxss\\{baa405ef-1822-4bbe-84e2-30e4c6330d42}"
|
|
L"\\Version'."
|
|
L" "
|
|
L"Error: Data of this type is not supported. ",
|
|
L"Wsl/Service/ReadDistroConfig/ERROR_UNSUPPORTED_TYPE",
|
|
{},
|
|
L"wsl.exe",
|
|
true);
|
|
}
|
|
|
|
ValidateErrorMessage(
|
|
L"echo ok",
|
|
std::format(L"Invalid mac address 'foo' for key 'wsl2.macAddress' in {}:2", wslConfigPath),
|
|
L"Wsl/Service/CreateInstance/CreateVm/ParseConfig/E_INVALIDARG",
|
|
L"macAddress=foo");
|
|
}
|
|
else
|
|
{
|
|
// wsl.exe --manage --resize requires WSL2.
|
|
ValidateErrorMessage(
|
|
L"--manage test_distro --resize 10GB",
|
|
L"This operation is only supported by WSL2.",
|
|
L"Wsl/Service/WSL_E_WSL2_NEEDED");
|
|
}
|
|
|
|
ValidateErrorMessage(
|
|
L"--import a b c", GetSystemErrorString(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)), L"Wsl/ERROR_FILE_NOT_FOUND");
|
|
|
|
ValidateErrorMessage(
|
|
L"-d DoesNotExist echo foo",
|
|
L"There is no distribution with the supplied name.",
|
|
L"Wsl/Service/WSL_E_DISTRO_NOT_FOUND");
|
|
|
|
ValidateErrorMessage(
|
|
L"--export DoesNotExist FileName",
|
|
L"There is no distribution with the supplied name.",
|
|
L"Wsl/Service/WSL_E_DISTRO_NOT_FOUND");
|
|
|
|
ValidateErrorMessage(
|
|
L"--import-in-place DoesNotExist FileName",
|
|
GetSystemErrorString(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)),
|
|
L"Wsl/ERROR_FILE_NOT_FOUND");
|
|
|
|
ValidateErrorMessage(
|
|
L"--set-default-version 3",
|
|
GetSystemErrorString(HRESULT_FROM_WIN32(ERROR_VERSION_PARSE_ERROR)),
|
|
L"Wsl/ERROR_VERSION_PARSE_ERROR");
|
|
|
|
ValidateErrorMessage(
|
|
L"--manage DoesNotExist --resize 10GB",
|
|
L"There is no distribution with the supplied name.",
|
|
L"Wsl/Service/WSL_E_DISTRO_NOT_FOUND");
|
|
|
|
ValidateErrorMessage(L"--manage test_distro --resize foo", L"Invalid size: foo", L"Wsl/E_INVALIDARG");
|
|
|
|
ValidateErrorMessage(
|
|
L"--install --distribution debian --no-distribution",
|
|
L"Arguments --no-distribution and --distribution can't be specified at same time.",
|
|
L"Wsl/E_INVALIDARG");
|
|
|
|
ValidateErrorMessage(
|
|
L"--install debian --from-file foo --distribution foo",
|
|
L"Arguments --from-file and --distribution can't be specified at same time.",
|
|
L"Wsl/E_INVALIDARG");
|
|
|
|
ValidateErrorMessage(
|
|
L"--install foo --fixed-vhd", L"Argument --fixed-vhd requires the --vhd-size argument.", L"Wsl/E_INVALIDARG");
|
|
|
|
{
|
|
UniqueWebServer server(c_testDistributionEndpoint, c_testDistributionJson);
|
|
RegistryKeyChange<std::wstring> keyChange(
|
|
HKEY_LOCAL_MACHINE, LXSS_REGISTRY_PATH, wsl::windows::common::distribution::c_distroUrlRegistryValue, c_testDistributionEndpoint);
|
|
ValidateErrorMessage(
|
|
L"--install -d DoesNotExist",
|
|
L"Invalid distribution name: 'DoesNotExist'.\r\nTo get a list of valid distributions, use 'wsl.exe --list "
|
|
L"--online'.",
|
|
L"Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND");
|
|
}
|
|
|
|
{
|
|
const auto lxssKey = wsl::windows::common::registry::OpenLxssMachineKey(KEY_READ | KEY_SET_VALUE);
|
|
std::optional<std::wstring> revertValue;
|
|
|
|
try
|
|
{
|
|
revertValue = wsl::windows::common::registry::ReadString(
|
|
lxssKey.get(), nullptr, wsl::windows::common::distribution::c_distroUrlRegistryValue);
|
|
}
|
|
catch (...)
|
|
{
|
|
// Expected if the value isn't set
|
|
}
|
|
|
|
auto revert = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
if (revertValue.has_value())
|
|
{
|
|
wsl::windows::common::registry::WriteString(
|
|
lxssKey.get(), nullptr, wsl::windows::common::distribution::c_distroUrlRegistryValue, revertValue->c_str());
|
|
}
|
|
else
|
|
{
|
|
wsl::windows::common::registry::DeleteValue(lxssKey.get(), wsl::windows::common::distribution::c_distroUrlRegistryValue);
|
|
}
|
|
});
|
|
|
|
wsl::windows::common::registry::WriteString(
|
|
lxssKey.get(), nullptr, wsl::windows::common::distribution::c_distroUrlRegistryValue, L"http://127.0.0.1:6666");
|
|
|
|
ValidateErrorMessage(
|
|
L"--install -d ubuntu",
|
|
L"Failed to fetch the distribution list from 'http://127.0.0.1:6666'. " +
|
|
GetSystemErrorString(HRESULT_FROM_WIN32(WININET_E_CANNOT_CONNECT)),
|
|
L"Wsl/InstallDistro/WININET_E_CANNOT_CONNECT");
|
|
|
|
ValidateErrorMessage(
|
|
L"--list --online",
|
|
L"Failed to fetch the distribution list from 'http://127.0.0.1:6666'. " +
|
|
GetSystemErrorString(HRESULT_FROM_WIN32(WININET_E_CANNOT_CONNECT)),
|
|
L"Wsl/WININET_E_CANNOT_CONNECT");
|
|
}
|
|
|
|
ValidateErrorMessage(
|
|
L"/u foo",
|
|
L"There is no distribution with the supplied name.",
|
|
L"WslConfig/Service/WSL_E_DISTRO_NOT_FOUND",
|
|
{},
|
|
L"wslconfig.exe");
|
|
|
|
ValidateErrorMessage(
|
|
L"e7bef681-c148-4687-8a0f-8c8be93bac93", // GUID for a distro that's not installed.
|
|
L"There is no distribution with the supplied name.",
|
|
L"Bash/Service/CreateInstance/ReadDistroConfig/WSL_E_DISTRO_NOT_FOUND",
|
|
{},
|
|
L"bash.exe");
|
|
|
|
VerifyOutput(L"--install --no-distribution", L"The operation completed successfully. \r\n");
|
|
|
|
{
|
|
std::wstring expectedUsageMessage;
|
|
for (auto e : wsl::shared::Localization::MessageWslUsage())
|
|
{
|
|
if (e == L'\n')
|
|
{
|
|
expectedUsageMessage += L'\r';
|
|
}
|
|
|
|
expectedUsageMessage += e;
|
|
}
|
|
|
|
VerifyOutput(L"--manage --move .", expectedUsageMessage + L"\r\n", -1);
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(CommandLineParsing)
|
|
{
|
|
VerifyOutput(L"echo -n \\\"", L"\"");
|
|
VerifyOutput(L"echo -n \\\'", L"\'");
|
|
VerifyOutput(L"echo -n \" \"", L" ");
|
|
VerifyOutput(L"echo -n $USER", L"root");
|
|
VerifyOutput(L"echo -n \"$USER\"", L"root");
|
|
VerifyOutput(L"echo -n '\"$USER\"'", L"\"$USER\"");
|
|
VerifyOutput(L"echo -n '\\\"$USER\\\"'", L"\\\"$USER\\\"");
|
|
VerifyOutput(L"echo -n '$USER'", L"$USER");
|
|
VerifyOutput(L"echo -n a \" \" b", L"a b");
|
|
VerifyOutput(L"echo -n a \"\" b", L"a b");
|
|
VerifyOutput(L"echo -n a b \"\"", L"a b ");
|
|
VerifyOutput(L"echo -n \"a\"\"b\"", L"ab");
|
|
|
|
VerifyOutput(L"--exec echo -n \"a\"", L"a");
|
|
VerifyOutput(L"--exec echo -n $USER", L"$USER");
|
|
VerifyOutput(L"--exec echo -n \\\"a\\\"", L"\"a\"");
|
|
VerifyOutput(L"--exec echo -n \\\"a\\\"", L"\"a\"");
|
|
VerifyOutput(L"--exec echo -n \"a\"\"b\"", L"a\"b");
|
|
VerifyOutput(L"--exec echo -n \\\"", L"\"");
|
|
}
|
|
|
|
// This test validates that the help messages for wsl.exe and wsl.config are correctly displayed.
|
|
// Notes:
|
|
// - This test will fail if the help messages are changed. If that's the case, simply update the below strings
|
|
// - This test assumes that English is the configured language.
|
|
TEST_METHOD(UsageMessages)
|
|
{
|
|
const std::wstring WslHelpMessage =
|
|
LR"""(Copyright (c) Microsoft Corporation. All rights reserved.
|
|
For privacy information about this product please visit https://aka.ms/privacy.
|
|
|
|
Usage: wsl.exe [Argument] [Options...] [CommandLine]
|
|
|
|
Arguments for running Linux binaries:
|
|
|
|
If no command line is provided, wsl.exe launches the default shell.
|
|
|
|
--exec, -e <CommandLine>
|
|
Execute the specified command without using the default Linux shell.
|
|
|
|
--shell-type <standard|login|none>
|
|
Execute the specified command with the provided shell type.
|
|
|
|
--
|
|
Pass the remaining command line as-is.
|
|
|
|
Options:
|
|
--cd <Directory>
|
|
Sets the specified directory as the current working directory.
|
|
If ~ is used the Linux user's home path will be used. If the path begins
|
|
with a / character, it will be interpreted as an absolute Linux path.
|
|
Otherwise, the value must be an absolute Windows path.
|
|
|
|
--distribution, -d <DistroName>
|
|
Run the specified distribution.
|
|
|
|
--distribution-id <DistroGuid>
|
|
Run the specified distribution ID.
|
|
|
|
--user, -u <UserName>
|
|
Run as the specified user.
|
|
|
|
--system
|
|
Launches a shell for the system distribution.
|
|
|
|
Arguments for managing Windows Subsystem for Linux:
|
|
|
|
--help
|
|
Display usage information.
|
|
|
|
--debug-shell
|
|
Open a WSL2 debug shell for diagnostics purposes.
|
|
|
|
--install [Distro] [Options...]
|
|
Install a Windows Subsystem for Linux distribution.
|
|
For a list of valid distributions, use 'wsl.exe --list --online'.
|
|
|
|
Options:
|
|
--enable-wsl1
|
|
Enable WSL1 support.
|
|
|
|
--fixed-vhd
|
|
Create a fixed-size disk to store the distribution.
|
|
|
|
--from-file <Path>
|
|
Install a distribution from a local file.
|
|
|
|
--legacy
|
|
Use the legacy distribution manifest.
|
|
|
|
--location <Location>
|
|
Set the install path for the distribution.
|
|
|
|
--name <Name>
|
|
Set the name of the distribution.
|
|
|
|
--no-distribution
|
|
Only install the required optional components, does not install a distribution.
|
|
|
|
--no-launch, -n
|
|
Do not launch the distribution after install.
|
|
|
|
--version <Version>
|
|
Specifies the version to use for the new distribution.
|
|
|
|
--vhd-size <MemoryString>
|
|
Specifies the size of the disk to store the distribution.
|
|
|
|
--web-download
|
|
Download the distribution from the internet instead of the Microsoft Store.
|
|
|
|
--manage <Distro> <Options...>
|
|
Changes distro specific options.
|
|
|
|
Options:
|
|
--move <Location>
|
|
Move the distribution to a new location.
|
|
|
|
--set-sparse, -s <true|false>
|
|
Set the VHD of distro to be sparse, allowing disk space to be automatically reclaimed.
|
|
|
|
--set-default-user <Username>
|
|
Set the default user of the distribution.
|
|
|
|
--resize <MemoryString>
|
|
Resize the disk of the distribution to the specified size.
|
|
|
|
--mount <Disk>
|
|
Attaches and mounts a physical or virtual disk in all WSL 2 distributions.
|
|
|
|
Options:
|
|
--vhd
|
|
Specifies that <Disk> refers to a virtual hard disk.
|
|
|
|
--bare
|
|
Attach the disk to WSL2, but don't mount it.
|
|
|
|
--name <Name>
|
|
Mount the disk using a custom name for the mountpoint.
|
|
|
|
--type <Type>
|
|
Filesystem to use when mounting a disk, if not specified defaults to ext4.
|
|
|
|
--options <Options>
|
|
Additional mount options.
|
|
|
|
--partition <Index>
|
|
Index of the partition to mount, if not specified defaults to the whole disk.
|
|
|
|
--set-default-version <Version>
|
|
Changes the default install version for new distributions.
|
|
|
|
--shutdown
|
|
Immediately terminates all running distributions and the WSL 2
|
|
lightweight utility virtual machine.
|
|
|
|
Options:
|
|
--force
|
|
Terminate the WSL 2 virtual machine even if an operation is in progress. Can cause data loss.
|
|
|
|
--status
|
|
Show the status of Windows Subsystem for Linux.
|
|
|
|
--unmount [Disk]
|
|
Unmounts and detaches a disk from all WSL2 distributions.
|
|
Unmounts and detaches all disks if called without argument.
|
|
|
|
--uninstall
|
|
Uninstalls the Windows Subsystem for Linux package from this machine.
|
|
|
|
--update
|
|
Update the Windows Subsystem for Linux package.
|
|
|
|
Options:
|
|
--pre-release
|
|
Download a pre-release version if available.
|
|
|
|
--version, -v
|
|
Display version information.
|
|
|
|
Arguments for managing distributions in Windows Subsystem for Linux:
|
|
|
|
--export <Distro> <FileName> [Options]
|
|
Exports the distribution to a tar file.
|
|
The filename can be - for stdout.
|
|
|
|
Options:
|
|
--format <Format>
|
|
Specifies the export format. Supported values: tar, tar.gz, tar.xz, vhd.
|
|
|
|
--import <Distro> <InstallLocation> <FileName> [Options]
|
|
Imports the specified tar file as a new distribution.
|
|
The filename can be - for stdin.
|
|
|
|
Options:
|
|
--version <Version>
|
|
Specifies the version to use for the new distribution.
|
|
|
|
--vhd
|
|
Specifies that the provided file is a .vhd or .vhdx file, not a tar file.
|
|
This operation makes a copy of the VHD file at the specified install location.
|
|
|
|
--import-in-place <Distro> <FileName>
|
|
Imports the specified VHD file as a new distribution.
|
|
This virtual hard disk must be formatted with the ext4 filesystem type.
|
|
|
|
--list, -l [Options]
|
|
Lists distributions.
|
|
|
|
Options:
|
|
--all
|
|
List all distributions, including distributions that are
|
|
currently being installed or uninstalled.
|
|
|
|
--running
|
|
List only distributions that are currently running.
|
|
|
|
--quiet, -q
|
|
Only show distribution names.
|
|
|
|
--verbose, -v
|
|
Show detailed information about all distributions.
|
|
|
|
--online, -o
|
|
Displays a list of available distributions for install with 'wsl.exe --install'.
|
|
|
|
--set-default, -s <Distro>
|
|
Sets the distribution as the default.
|
|
|
|
--set-version <Distro> <Version>
|
|
Changes the version of the specified distribution.
|
|
|
|
--terminate, -t <Distro>
|
|
Terminates the specified distribution.
|
|
|
|
--unregister <Distro>
|
|
Unregisters the distribution and deletes the root filesystem.
|
|
)""";
|
|
|
|
const std::wstring WslConfigHelpMessage =
|
|
LR"""(Performs administrative operations on Windows Subsystem for Linux
|
|
|
|
Usage:
|
|
/l, /list [Option]
|
|
Lists registered distributions.
|
|
/all - Optionally list all distributions, including distributions that
|
|
are currently being installed or uninstalled.
|
|
|
|
/running - List only distributions that are currently running.
|
|
|
|
/s, /setdefault <DistributionName>
|
|
Sets the distribution as the default.
|
|
|
|
/t, /terminate <DistributionName>
|
|
Terminates the distribution.
|
|
|
|
/u, /unregister <DistributionName>
|
|
Unregisters the distribution and deletes the root filesystem.
|
|
)""";
|
|
|
|
const std::wstring WslInstallHelpMessage =
|
|
LR"""(Invalid distribution name: 'foo'.
|
|
To get a list of valid distributions, use 'wsl.exe --list --online'.
|
|
Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND
|
|
)""";
|
|
|
|
auto AddCrlf = [](const std::wstring& Input) {
|
|
std::wstring MessageWithCrlf;
|
|
|
|
for (const auto e : Input)
|
|
{
|
|
if (e == '\n')
|
|
{
|
|
MessageWithCrlf += '\r';
|
|
}
|
|
MessageWithCrlf += e;
|
|
}
|
|
|
|
return MessageWithCrlf;
|
|
};
|
|
|
|
// Note: There is no easy way to validate wslg's help message, since it displays a blocking
|
|
// message box before exiting.
|
|
|
|
VerifyOutput(L"--help", AddCrlf(WslHelpMessage), -1);
|
|
VerifyOutput(L"--help", AddCrlf(WslConfigHelpMessage), -1, L"wslconfig.exe");
|
|
|
|
UniqueWebServer server(c_testDistributionEndpoint, c_testDistributionJson);
|
|
RegistryKeyChange<std::wstring> keyChange(
|
|
HKEY_LOCAL_MACHINE, LXSS_REGISTRY_PATH, wsl::windows::common::distribution::c_distroUrlRegistryValue, c_testDistributionEndpoint);
|
|
|
|
VerifyOutput(L"--install foo", AddCrlf(WslInstallHelpMessage), -1);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(TestExistingSwapVhd)
|
|
{
|
|
// Create a 100MB swap vhdx.
|
|
auto swapVhd = wil::GetCurrentDirectoryW<std::wstring>() + L"\\TestSwap.vhdx";
|
|
|
|
VIRTUAL_STORAGE_TYPE storageType{};
|
|
storageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX;
|
|
storageType.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT;
|
|
|
|
CREATE_VIRTUAL_DISK_PARAMETERS createVhdParameters{};
|
|
createVhdParameters.Version = CREATE_VIRTUAL_DISK_VERSION_2;
|
|
createVhdParameters.Version2.BlockSizeInBytes = 1024 * 1024;
|
|
createVhdParameters.Version2.MaximumSize = 100 * 1024 * 1024;
|
|
|
|
wil::unique_hfile vhd{};
|
|
VERIFY_ARE_EQUAL(
|
|
::CreateVirtualDisk(
|
|
&storageType, swapVhd.c_str(), VIRTUAL_DISK_ACCESS_NONE, nullptr, CREATE_VIRTUAL_DISK_FLAG_SUPPORT_COMPRESSED_VOLUMES, 0, &createVhdParameters, nullptr, &vhd),
|
|
0l);
|
|
|
|
vhd.reset();
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
WslShutdown();
|
|
DeleteFile(swapVhd.c_str());
|
|
});
|
|
|
|
// Update .wslconfig. Update the swapVhd path to replace single backslash
|
|
// with double backslashes so as to be compatible with .wslconfig parsing.
|
|
// The following regex replacement only works as intended if the path contains
|
|
// single backslashes. Negative lookahead can be used to handle paths with double
|
|
// backslashes but then the negative lookbehind case should also be used but the
|
|
// latter is not supported in std::regex.
|
|
swapVhd = std::regex_replace(swapVhd, std::wregex(L"\\\\"), L"\\\\");
|
|
WslConfigChange configChange(LxssGenerateTestConfig() + L"\nswap=256MB\nswapFile=" + swapVhd);
|
|
|
|
auto validateSwapSize = [](LPCWSTR Expected) {
|
|
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"swapon | awk 'END {print $3}'");
|
|
|
|
VERIFY_ARE_EQUAL(Expected + std::wstring(L"\n"), output);
|
|
};
|
|
|
|
validateSwapSize(L"256M");
|
|
|
|
// Validate that the vhdx is resized correctly if the swap size changes
|
|
configChange.Update(LxssGenerateTestConfig() + L"\nswap=200MB\nswapFile=" + swapVhd);
|
|
validateSwapSize(L"200M");
|
|
}
|
|
|
|
TEST_METHOD(InitDoesntBlockSignals)
|
|
{
|
|
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"grep -iF SigBlk < /proc/1/status");
|
|
VERIFY_ARE_EQUAL(L"SigBlk:\t0000000000000000\n", output);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(InitReadonly)
|
|
{
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L" grep '^rootfs /init rootfs ro,' /proc/self/mounts", nullptr, nullptr, nullptr, nullptr), 0u);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(GpuMounts)
|
|
{
|
|
auto ValidateGpuMounts = [](HANDLE Token) {
|
|
VERIFY_ARE_EQUAL(
|
|
LxsstuLaunchWsl(
|
|
L"mount | grep -iF 'none on /usr/lib/wsl/lib type overlay (rw,nosuid,nodev,noatime,lowerdir=/gpu_" TEXT(LXSS_GPU_PACKAGED_LIB_SHARE) L":/gpu_" TEXT(
|
|
LXSS_GPU_INBOX_LIB_SHARE) L",upperdir=/gpu_lib/rw/upper,workdir=/gpu_lib/rw/work,uuid=on)'",
|
|
nullptr,
|
|
nullptr,
|
|
nullptr,
|
|
Token),
|
|
0u);
|
|
|
|
// Ensure the lib directory is writable.
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L" touch /usr/lib/wsl/lib/foo && rm /usr/lib/wsl/lib/foo", nullptr, nullptr, nullptr, Token), 0u);
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
LxsstuLaunchWsl(
|
|
L"mount | grep -iF '" TEXT(
|
|
LXSS_GPU_DRIVERS_SHARE) L" on /usr/lib/wsl/drivers type 9p (ro,nosuid,nodev,noatime,aname=" TEXT(LXSS_GPU_DRIVERS_SHARE) L";fmask=222;dmask=222,cache=5,access=client,msize=65536,trans=fd,rfd=8,wfd=8)'",
|
|
nullptr,
|
|
nullptr,
|
|
nullptr,
|
|
Token),
|
|
0u);
|
|
};
|
|
|
|
auto cleanUp = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslShutdown(); });
|
|
|
|
// Validate that GPU mounts are present in both namespaces.
|
|
const auto nonElevatedToken = GetNonElevatedToken();
|
|
WslShutdown();
|
|
ValidateGpuMounts(nullptr);
|
|
ValidateGpuMounts(nonElevatedToken.get());
|
|
|
|
// Create a new instance with a non-elevated token as the creator.
|
|
WslShutdown();
|
|
ValidateGpuMounts(nonElevatedToken.get());
|
|
ValidateGpuMounts(nullptr);
|
|
}
|
|
|
|
TEST_METHOD(InteropCornerCases)
|
|
{
|
|
auto validateInterop = [](const std::wstring& binaryName) {
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LxsstuLaunchWsl(L"rm /tmp/'" + binaryName + L"'"); });
|
|
|
|
// The "|| echo fail" part is needed because bash will exec instead of forking() of only one non-builtin command is passed.
|
|
// If bash exec's then this test is useless since the binfmt interpreter would not be a child of a process with a weird name.
|
|
|
|
const std::wstring commandLine =
|
|
L"cp /bin/bash /tmp/'" + binaryName + L"' && '/tmp/" + binaryName +
|
|
L"' -c 'export WSL_INTEROP=\"\" && echo -n $WSL_INTEROP && cmd.exe /c \"echo ok\" || echo fail'";
|
|
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(commandLine);
|
|
|
|
VERIFY_ARE_EQUAL(output, L"ok\r\n");
|
|
};
|
|
|
|
validateInterop(L"bash with spaces");
|
|
validateInterop(L"bash )");
|
|
validateInterop(L"bash (");
|
|
validateInterop(L"(bash)");
|
|
validateInterop(L"(bash(");
|
|
validateInterop(L"()");
|
|
validateInterop(L"(");
|
|
validateInterop(L")");
|
|
}
|
|
|
|
TEST_METHOD(InteropPid1)
|
|
{
|
|
// Validate that interop works as pid 1.
|
|
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"unshare -pf --wd $(dirname $(which cmd.exe)) cmd.exe /c echo ok");
|
|
VERIFY_ARE_EQUAL(output, L"ok\r\n");
|
|
}
|
|
|
|
TEST_METHOD(Hostname)
|
|
{
|
|
auto cleanup = wil::scope_exit([] {
|
|
LxsstuLaunchWsl(LXSST_REMOVE_DISTRO_CONF_COMMAND_LINE);
|
|
|
|
TerminateDistribution();
|
|
});
|
|
|
|
auto validate = [](const std::string& input, const std::wstring& expectedOutput) {
|
|
LxssWriteWslDistroConfig("[network]\nhostname=" + input);
|
|
TerminateDistribution();
|
|
|
|
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"hostname");
|
|
VERIFY_ARE_EQUAL(output, expectedOutput + L"\n");
|
|
|
|
output = LxsstuLaunchWslAndCaptureOutput(L"cat /etc/hostname").first;
|
|
VERIFY_ARE_EQUAL(output, expectedOutput + L"\n");
|
|
};
|
|
|
|
validate("SimpleHostname", L"SimpleHostname");
|
|
validate("Simple-Hostname", L"Simple-Hostname");
|
|
validate("Simple_Hostname", L"SimpleHostname");
|
|
validate("-hostname", L"hostname");
|
|
validate("--hostname", L"hostname");
|
|
validate("hostname.-", L"hostname");
|
|
validate(".hostname", L"hostname");
|
|
validate("hostname.", L"hostname");
|
|
validate("host.name.", L"host.name");
|
|
validate("host..name", L"host.name");
|
|
validate("host|name", L"hostname");
|
|
validate(".a-", L"a");
|
|
validate(".a-b", L"a-b");
|
|
validate(".", L"localhost");
|
|
validate("-", L"localhost");
|
|
validate("-.-", L"localhost");
|
|
// Validate hostname is limited to 64 characters.
|
|
const std::string longHostName("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz");
|
|
validate(longHostName, wsl::shared::string::MultiByteToWide(longHostName.substr(0, 64)));
|
|
}
|
|
|
|
WSL2_TEST_METHOD(WslConfWarnings)
|
|
{
|
|
DistroFileChange configChange(L"/etc/wsl.conf", false);
|
|
|
|
auto validateWarnings = [&configChange](const std::wstring& config, const std::wstring& expectedWarnings) {
|
|
configChange.SetContent(config.c_str());
|
|
|
|
TerminateDistribution();
|
|
|
|
// This loop is here because of a race condition when starting WSL to get the warnings.
|
|
// If a p9rdr distribution startup notification arrives just before wsl.exe calls CreateInstance(),
|
|
// the warnings will be 'consumed' before wsl.exe can read them.
|
|
// To work around that, loop for up to 2 minutes while we don't get any warnings
|
|
|
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::minutes(2);
|
|
|
|
while (std::chrono::steady_clock::now() < deadline)
|
|
{
|
|
auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(L"-u root echo ok");
|
|
VERIFY_ARE_EQUAL(L"ok\n", output);
|
|
|
|
if (!warnings.empty() || expectedWarnings.empty())
|
|
{
|
|
VERIFY_ARE_EQUAL(expectedWarnings, warnings);
|
|
return;
|
|
}
|
|
|
|
LogInfo("Received empty warnings, trying again");
|
|
WslShutdown();
|
|
}
|
|
|
|
LogError("Timed out waiting for warnings. Expected warnings: %ls", expectedWarnings.c_str());
|
|
VERIFY_FAIL();
|
|
};
|
|
|
|
validateWarnings(L"[foo]\na=b", L"wsl: Unknown key 'foo.a' in /etc/wsl.conf:2\r\n");
|
|
validateWarnings(L"a=a\\m", L"wsl: Invalid escaped character: 'm' in /etc/wsl.conf:1\r\n");
|
|
validateWarnings(L"[=b", L"wsl: Invalid section name in /etc/wsl.conf:1\r\n");
|
|
validateWarnings(L"\r\n\r\n[foo]\r\na=b", L"wsl: Unknown key 'foo.a' in /etc/wsl.conf:5\r\n");
|
|
|
|
// Validate that CRLF is correctly handled
|
|
{
|
|
configChange.SetContent(L"[network]\r\nhostname=foo\r\n");
|
|
TerminateDistribution();
|
|
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"hostname");
|
|
VERIFY_ARE_EQUAL(out, L"foo\n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
}
|
|
|
|
WSL2_TEST_METHOD(Warnings)
|
|
{
|
|
WslConfigChange configChange(LxssGenerateTestConfig());
|
|
|
|
auto validateWarnings = [&configChange](
|
|
const std::wstring& config,
|
|
const std::wstring& expectedWarnings,
|
|
const std::wstring& prefix = LxssGenerateTestConfig(),
|
|
bool fnmatch = false) {
|
|
WEX::Logging::Log::Comment(config.c_str());
|
|
WEX::Logging::Log::Comment(expectedWarnings.c_str());
|
|
configChange.Update(prefix + config);
|
|
|
|
// This loop is here because of a race condition when starting WSL to get the warnings.
|
|
// If a p9rdr distribution startup notification arrives just before wsl.exe calls CreateInstance(),
|
|
// the warnings will be 'consumed' before wsl.exe can read them.
|
|
// To work around that, loop for up to 2 minutes while we don't get any warnings
|
|
|
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::minutes(2);
|
|
|
|
while (std::chrono::steady_clock::now() < deadline)
|
|
{
|
|
auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
|
|
VERIFY_ARE_EQUAL(L"ok\n", output);
|
|
|
|
if (!warnings.empty() || expectedWarnings.empty())
|
|
{
|
|
if (fnmatch)
|
|
{
|
|
if (!PathMatchSpec(warnings.c_str(), expectedWarnings.c_str()))
|
|
{
|
|
LogError("Warning '%ls' didn't match pattern '%ls'", warnings.c_str(), expectedWarnings.c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
VERIFY_ARE_EQUAL(expectedWarnings, warnings);
|
|
}
|
|
return;
|
|
}
|
|
|
|
LogInfo("Received empty warnings, trying again");
|
|
WslShutdown();
|
|
}
|
|
|
|
LogError("Timed out waiting for warnings. Expected warnings: %ls", expectedWarnings.c_str());
|
|
VERIFY_FAIL();
|
|
};
|
|
|
|
const std::wstring wslConfigPath = wsl::windows::common::helpers::GetWslConfigPath();
|
|
|
|
validateWarnings(L"a=b", std::format(L"wsl: Unknown key 'wsl2.a' in {}:21\r\n", wslConfigPath));
|
|
validateWarnings(L"[=b", std::format(L"wsl: Invalid section name in {}:21\r\n", wslConfigPath));
|
|
|
|
validateWarnings(
|
|
L"dhcpTimeout=NotANumber",
|
|
std::format(L"wsl: Invalid integer value 'NotANumber' for key 'wsl2.dhcpTimeout' in {}:21\r\n", wslConfigPath));
|
|
|
|
validateWarnings(L"ipv6=NotABoolean", std::format(L"wsl: Invalid boolean value 'NotABoolean' for key 'wsl2.ipv6' in {}:21\r\n", wslConfigPath));
|
|
|
|
validateWarnings(L"[sectionNotComplete", std::format(L"wsl: Expected ']' in {}:21\r\n", wslConfigPath));
|
|
validateWarnings(L"NoEqual", std::format(L"wsl: Expected '=' in {}:21\r\n", wslConfigPath));
|
|
validateWarnings(
|
|
L"networkingMode=InvalidMode",
|
|
std::format(L"wsl: Invalid value 'InvalidMode' for config key 'wsl2.networkingMode' in {}:2 (Valid values: Bridged, Mirrored, Nat, None, VirtioProxy)\r\n", wslConfigPath),
|
|
L"[wsl2]\n");
|
|
validateWarnings(
|
|
L"networkingMode=a\\m", std::format(L"wsl: Invalid escaped character: 'm' in {}:2\r\n", wslConfigPath), L"[wsl2]\n");
|
|
|
|
validateWarnings(
|
|
L"\nswap=200MB\nswapFile=C:\\\\DoesNotExist\\\\swap.vhdx",
|
|
L"wsl: Failed to create the swap disk in 'C:\\DoesNotExist\\swap.vhdx': The system cannot find the path "
|
|
L"specified. \r\n");
|
|
|
|
validateWarnings(L"\nswap=/", std::format(L"wsl: Invalid memory string '/' for .wslconfig entry 'wsl2.swap' in {}:22\r\n", wslConfigPath));
|
|
validateWarnings(L"\nswap=0GB", L"");
|
|
validateWarnings(L"\nswap=0foo", std::format(L"wsl: Invalid memory string '0foo' for .wslconfig entry 'wsl2.swap' in {}:22\r\n", wslConfigPath));
|
|
validateWarnings(L"safeMode=true", L"wsl: SAFE MODE ENABLED - many features will be disabled\r\n", L"[wsl2]\n");
|
|
validateWarnings(L"processors=", std::format(L"wsl: Invalid integer value '' for key 'wsl2.processors' in {}:21\r\n", wslConfigPath));
|
|
validateWarnings(L"memory=", std::format(L"wsl: Invalid memory string '' for .wslconfig entry 'wsl2.memory' in {}:21\r\n", wslConfigPath));
|
|
validateWarnings(L"debugConsole=", std::format(L"wsl: Invalid boolean value '' for key 'wsl2.debugConsole' in {}:21\r\n", wslConfigPath));
|
|
validateWarnings(
|
|
L"networkingMode=",
|
|
std::format(L"wsl: Invalid value '' for config key 'wsl2.networkingMode' in {}:21 (Valid values: Bridged, Mirrored, Nat, None, VirtioProxy)\r\n", wslConfigPath));
|
|
|
|
validateWarnings(
|
|
L"ipv6=true\nipv6=false",
|
|
std::format(L"wsl: Duplicated config key 'wsl2.ipv6' in {}:22 (Conflicting key: 'wsl2.ipv6' in {}:21)\r\n", wslConfigPath, wslConfigPath));
|
|
|
|
validateWarnings(
|
|
L"networkingMode=NAT\n[experimental]\nnetworkingMode=Mirrored",
|
|
std::format(L"wsl: Duplicated config key 'experimental.networkingMode' in {}:4 (Conflicting key: 'wsl2.networkingMode' in {}:2)\r\n", wslConfigPath, wslConfigPath),
|
|
L"[wsl2]\n");
|
|
|
|
validateWarnings(
|
|
L"networkingMode=bridged",
|
|
L"wsl: Bridged networking requires wsl2.vmSwitch to be set.\r\n"
|
|
L"Error code: CreateInstance/CreateVm/ConfigureNetworking/WSL_E_VMSWITCH_NOT_SET\r\n"
|
|
L"wsl: Failed to configure network (networkingMode Bridged), falling back to networkingMode None.\r\n",
|
|
L"[wsl2]\n");
|
|
|
|
validateWarnings(
|
|
L"networkingMode=bridged\nvmSwitch=DoesNotExist",
|
|
L"wsl: The VmSwitch 'DoesNotExist' was not found. Available switches:*\r\n"
|
|
L"Error code: CreateInstance/CreateVm/ConfigureNetworking/WSL_E_VMSWITCH_NOT_FOUND\r\n"
|
|
L"wsl: Failed to configure network (networkingMode Bridged), falling back to networkingMode None.\r\n",
|
|
L"[wsl2]\n",
|
|
true);
|
|
|
|
if (!AreExperimentalNetworkingFeaturesSupported())
|
|
{
|
|
validateWarnings(
|
|
L"[experimental]\nnetworkingMode=mirrored",
|
|
L"wsl: Experimental networking features are not supported, falling back to default settings\r\n",
|
|
L"[wsl2]\n");
|
|
|
|
validateWarnings(
|
|
L"[experimental]\ndnsTunneling=true",
|
|
L"wsl: Experimental networking features are not supported, falling back to default settings\r\n",
|
|
L"[wsl2]\n");
|
|
|
|
validateWarnings(
|
|
L"[experimental]\nfirewall=true",
|
|
L"wsl: Experimental networking features are not supported, falling back to default settings\r\n",
|
|
L"[wsl2]\n");
|
|
}
|
|
else
|
|
{
|
|
if (TryLoadDnsResolverMethods())
|
|
{
|
|
// Verify DNS tunneling settings are parsed correctly
|
|
validateWarnings(L"[experimental]\ndnsTunneling=true\nbestEffortDnsParsing=true", L"");
|
|
validateWarnings(L"[experimental]\ndnsTunneling=true\ndnsTunnelingIpAddress=10.255.255.1", L"");
|
|
|
|
validateWarnings(
|
|
L"[experimental]\ndnsTunneling=true\ndnsTunnelingIpAddress=1.2.3",
|
|
std::format(L"wsl: Invalid IP value '1.2.3' for key 'experimental.dnsTunnelingIpAddress' in {}:23\r\n", wslConfigPath));
|
|
}
|
|
}
|
|
|
|
validateWarnings(
|
|
L"[experimental]\nignoredPorts=NotANumber",
|
|
std::format(L"wsl: Invalid integer value 'NotANumber' for key 'experimental.ignoredPorts' in {}:22\r\n", wslConfigPath));
|
|
|
|
validateWarnings(
|
|
L"[experimental]\nignoredPorts=65536",
|
|
std::format(L"wsl: Invalid integer value '65536' for key 'experimental.ignoredPorts' in {}:22\r\n", wslConfigPath));
|
|
|
|
// Verify that the vhdSize setting is parsed correctly.
|
|
validateWarnings(L"[wsl2]\ndefaultVhdSize=64GB\n", L"");
|
|
|
|
auto maxProcessorCount = wsl::windows::common::wslutil::GetLogicalProcessorCount();
|
|
validateWarnings(
|
|
std::format(L"processors={}", maxProcessorCount + 1).c_str(),
|
|
std::format(L"wsl: wsl2.processors cannot exceed the number of logical processors on the system ({} > {})\r\n", maxProcessorCount + 1, maxProcessorCount));
|
|
|
|
// Exclusively open .wslconfig to make it unreadable
|
|
const wil::unique_handle wslConfig{
|
|
CreateFile(wslConfigPath.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
|
|
VERIFY_IS_NOT_NULL(wslConfig);
|
|
|
|
WslShutdown();
|
|
auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
|
|
VERIFY_ARE_EQUAL(L"ok\n", output);
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
std::format(L"wsl: Failed to open config file {}, The process cannot access the file because it is being used by another process. \r\n", wslConfigPath),
|
|
warnings);
|
|
|
|
{
|
|
DistroFileChange fstab(L"/etc/fstab");
|
|
fstab.SetContent(L"invalid fs tab content");
|
|
TerminateDistribution();
|
|
|
|
std::tie(output, warnings) = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
|
|
VERIFY_ARE_EQUAL(L"ok\n", output);
|
|
VERIFY_ARE_EQUAL(L"wsl: Processing /etc/fstab with mount -a failed.\n", warnings);
|
|
}
|
|
|
|
// Validate that WSL_DISABLE_WARNINGS silence the stderr output
|
|
ScopedEnvVariable disableWarnings(L"WSL_DISABLE_WARNINGS", L"1");
|
|
WslShutdown();
|
|
|
|
std::tie(output, warnings) = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
|
|
VERIFY_ARE_EQUAL(L"ok\n", output);
|
|
VERIFY_ARE_EQUAL(L"", warnings);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(Processors)
|
|
{
|
|
WslConfigChange configChange(LxssGenerateTestConfig() + L"\nprocessors=1");
|
|
|
|
auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(L"nproc --all");
|
|
VERIFY_ARE_EQUAL(L"1\n", output);
|
|
VERIFY_ARE_EQUAL(L"", warnings);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(GuiApplications)
|
|
{
|
|
auto validateEnvironment = [&](bool systemdEnabled) {
|
|
WslConfigChange configChange(LxssGenerateTestConfig({.guiApplications = true}));
|
|
|
|
// Validate that running the system distro works.
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--system true"), 0L);
|
|
|
|
// Validate that $DISPLAY and $WAYLAND_DISPLAY are set
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"env | grep DISPLAY="), 0L);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"env | grep WAYLAND_DISPLAY="), 0L);
|
|
|
|
// Validate the X11 socket is in the expected location and that we can connect to it.
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -d /tmp/.X11-unix"), 0L);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"socat - UNIX-CONNECT:/tmp/.X11-unix/X0 < /dev/null"), 0L);
|
|
|
|
// Validate the runtime dir exists and the wayland-0 socket is in the expected location.
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"env | grep XDG_RUNTIME_DIR="), 0L);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -d $XDG_RUNTIME_DIR"), 0L);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -S $XDG_RUNTIME_DIR/wayland-0"), 0L);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/wayland-0 < /dev/null"), 0L);
|
|
|
|
// Validate that WSLg can be disabled.
|
|
configChange.Update(LxssGenerateTestConfig({.guiApplications = false}));
|
|
|
|
// Validate that WSL starts successfully
|
|
auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
|
|
VERIFY_ARE_EQUAL(L"ok\n", output);
|
|
VERIFY_ARE_EQUAL(L"", warnings);
|
|
|
|
// Validate that WSLg-related environment variables are not present.
|
|
//
|
|
// N.B. XDG_RUNTIME_DIR is set when systemd is enabled even if GUI apps are disabled.
|
|
std::vector<std::wstring> variables = {L"$DISPLAY", L"$WAYLAND_DISPLAY"};
|
|
if (!systemdEnabled)
|
|
{
|
|
variables.emplace_back(L"$XDG_RUNTIME_DIR");
|
|
}
|
|
|
|
for (const auto& variable : variables)
|
|
{
|
|
std::tie(output, warnings) = LxsstuLaunchWslAndCaptureOutput(L"echo -n " + variable);
|
|
VERIFY_ARE_EQUAL(L"", output);
|
|
VERIFY_ARE_EQUAL(L"", warnings);
|
|
}
|
|
|
|
// Validate that wsl --system does not start
|
|
std::tie(output, warnings) = LxsstuLaunchWslAndCaptureOutput(L"--system echo not ok", -1);
|
|
|
|
const std::wstring configPath = wsl::windows::common::helpers::GetWslConfigPath();
|
|
const auto expectedOutput =
|
|
L"GUI application support is disabled via " + configPath +
|
|
L" or /etc/wsl.conf.\r\nError code: Wsl/Service/CreateInstance/WSL_E_GUI_APPLICATIONS_DISABLED\r\n";
|
|
|
|
VERIFY_ARE_EQUAL(output, expectedOutput);
|
|
VERIFY_ARE_EQUAL(L"", warnings);
|
|
};
|
|
|
|
LogInfo("Validate WSLg state with systemd disabled.");
|
|
validateEnvironment(false);
|
|
|
|
LogInfo("Validate WSLg state with systemd enabled.");
|
|
auto revert = EnableSystemd();
|
|
VERIFY_IS_TRUE(IsSystemdRunning(L"--system"));
|
|
validateEnvironment(true);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(GuiApplicationsSystemd)
|
|
{
|
|
DistroFileChange wslConf(L"/etc/wsl.conf", false);
|
|
wslConf.SetContent(L"[boot]\nsystemd=true\n");
|
|
WslConfigChange config{LxssGenerateTestConfig({.guiApplications = true})};
|
|
|
|
auto validateSocketExists = [](bool exists) {
|
|
LxsstuLaunchWsl(L"ls -a /tmp/.X11-unix/");
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /tmp/.X11-unix/X0"), exists ? 0L : 1L);
|
|
};
|
|
|
|
// Validate that wslg.service restores the socket if it's deleted.
|
|
{
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -f /run/systemd/generator/wslg.service"), 0L);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /run/systemd/generator/default.target.wants/wslg.service"), 0L);
|
|
|
|
validateSocketExists(true);
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"umount /tmp/.X11-unix"), 0L);
|
|
|
|
validateSocketExists(false);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"systemctl restart wslg.service"), 0L);
|
|
validateSocketExists(true);
|
|
}
|
|
|
|
// Validate that the unit isn't create when GUI apps are disabled
|
|
{
|
|
config.Update(LxssGenerateTestConfig({.guiApplications = false}));
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /run/systemd/generator/wslg.service"), 1L);
|
|
}
|
|
|
|
// Validate that the unit isn't create when GUI apps are disabled inside the distro.
|
|
{
|
|
wslConf.SetContent(L"[boot]\nsystemd=true\n[general]\nguiApplications=false");
|
|
TerminateDistribution();
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"test -e /run/systemd/generator/wslg.service"), 1L);
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(RegistryKeys)
|
|
{
|
|
auto openKey = [&](LPCWSTR keyName) {
|
|
LogInfo("OpenKey(HKEY_LOCAL_MACHINE, %ls, KEY_READ)", keyName);
|
|
return wsl::windows::common::registry::OpenKey(HKEY_LOCAL_MACHINE, keyName, KEY_READ);
|
|
};
|
|
|
|
// Keys that are created by the optional component and the service.
|
|
const std::vector<LPCWSTR> inboxKeys{
|
|
L"SOFTWARE\\Classes\\CLSID\\{B2B4A4D1-2754-4140-A2EB-9A76D9D7CDC6}",
|
|
L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Desktop\\NameSpace\\{B2B4A4D1-2754-4140-A2EB-"
|
|
L"9A76D9D7CDC6}",
|
|
L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\IdListAliasTranslations\\WSL",
|
|
L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\IdListAliasTranslations\\WSLLegacy",
|
|
L"SOFTWARE\\Classes\\Directory\\shell\\WSL",
|
|
L"SOFTWARE\\Classes\\Directory\\Background\\shell\\WSL",
|
|
L"SOFTWARE\\Classes\\Drive\\shell\\WSL"};
|
|
|
|
for (const auto* keyName : inboxKeys)
|
|
{
|
|
auto key = openKey(keyName);
|
|
VERIFY_IS_TRUE(!!key);
|
|
}
|
|
|
|
// Keys that are only created by the MSI.
|
|
const std::vector<LPCWSTR> serviceKeys{
|
|
L"SOFTWARE\\Microsoft\\Terminal Server Client\\Default\\OptionalAddIns\\WSLDVC_PACKAGE",
|
|
L"SOFTWARE\\Classes\\CLSID\\{7e6ad219-d1b3-42d5-b8ee-d96324e64ff6}",
|
|
L"SOFTWARE\\Classes\\AppID\\{17696EAC-9568-4CF5-BB8C-82515AAD6C09}"};
|
|
|
|
for (const auto* keyName : serviceKeys)
|
|
{
|
|
auto key = openKey(keyName);
|
|
VERIFY_IS_TRUE(!!key);
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(BinariesAreSigned)
|
|
{
|
|
if (!wsl::shared::OfficialBuild)
|
|
{
|
|
LogSkipped("Build is not signed, skipping test");
|
|
return;
|
|
}
|
|
|
|
auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath();
|
|
VERIFY_IS_TRUE(installPath.has_value());
|
|
|
|
size_t signedFiles = 0;
|
|
|
|
for (const auto& e : std::filesystem::recursive_directory_iterator(installPath.value()))
|
|
{
|
|
if (wsl::windows::common::string::IsPathComponentEqual(e.path().extension().native(), L".dll") ||
|
|
wsl::windows::common::string::IsPathComponentEqual(e.path().extension().native(), L".exe"))
|
|
{
|
|
LogInfo("Validating signature for: %ls", e.path().c_str());
|
|
|
|
wsl::windows::common::install::ValidateFileSignature(e.path().c_str());
|
|
signedFiles++;
|
|
}
|
|
}
|
|
|
|
// Sanity check
|
|
VERIFY_ARE_NOT_EQUAL(signedFiles, 0);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(CorruptedVhd)
|
|
{
|
|
// Create a 100MB vhd without a filesystem.
|
|
auto distroPath = std::filesystem::weakly_canonical(wil::GetCurrentDirectoryW<std::wstring>());
|
|
auto vhdPath = distroPath / L"CorruptedTest.vhdx";
|
|
|
|
VIRTUAL_STORAGE_TYPE storageType{};
|
|
storageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX;
|
|
storageType.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT;
|
|
|
|
CREATE_VIRTUAL_DISK_PARAMETERS createVhdParameters{};
|
|
createVhdParameters.Version = CREATE_VIRTUAL_DISK_VERSION_2;
|
|
createVhdParameters.Version2.BlockSizeInBytes = 1024 * 1024;
|
|
createVhdParameters.Version2.MaximumSize = 100 * 1024 * 1024;
|
|
|
|
wil::unique_hfile vhd{};
|
|
VERIFY_ARE_EQUAL(
|
|
::CreateVirtualDisk(
|
|
&storageType, vhdPath.c_str(), VIRTUAL_DISK_ACCESS_NONE, nullptr, CREATE_VIRTUAL_DISK_FLAG_SUPPORT_COMPRESSED_VOLUMES, 0, &createVhdParameters, nullptr, &vhd),
|
|
0l);
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
vhd.reset();
|
|
DeleteFileW(vhdPath.c_str());
|
|
});
|
|
|
|
auto validateOutput = [&](const std::wstring& command, const std::wstring& expectedOutput) {
|
|
auto [output, _] = LxsstuLaunchWslAndCaptureOutput(command.c_str(), -1);
|
|
VERIFY_ARE_EQUAL(output, expectedOutput);
|
|
};
|
|
|
|
// Attempt to import a vhd with an open handle.
|
|
validateOutput(
|
|
std::format(L"--import-in-place test-distro-corrupted \"{}\"", vhdPath.wstring()),
|
|
std::format(
|
|
L"Failed to attach disk '\\\\?\\{}' to WSL2: The process cannot access the file because it is being used by "
|
|
L"another process. \r\n"
|
|
L"Error code: Wsl/Service/RegisterDistro/MountDisk/HCS/ERROR_SHARING_VIOLATION\r\n",
|
|
vhdPath.wstring()));
|
|
|
|
vhd.reset();
|
|
|
|
// Create a broken distribution registration
|
|
{
|
|
const auto userKey = wsl::windows::common::registry::OpenLxssUserKey();
|
|
const auto distroKey =
|
|
wsl::windows::common::registry::CreateKey(userKey.get(), L"{baa405ef-1822-4bbe-84e2-30e4c6330d42}");
|
|
|
|
auto revert = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] {
|
|
wsl::windows::common::registry::DeleteKey(userKey.get(), L"{baa405ef-1822-4bbe-84e2-30e4c6330d42}");
|
|
});
|
|
|
|
wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"BasePath", distroPath.c_str());
|
|
wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"VhdFileName", L"CorruptedTest.vhdx");
|
|
wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"DistributionName", L"BrokenDistro");
|
|
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"DefaultUid", 0);
|
|
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"Version", LXSS_DISTRO_VERSION_2);
|
|
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"State", LxssDistributionStateInstalled);
|
|
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"Flags", LXSS_DISTRO_FLAGS_VM_MODE);
|
|
|
|
// Validate that starting the distribution fails with the correct error code.
|
|
validateOutput(
|
|
L"-d BrokenDistro echo ok",
|
|
L"The distribution failed to start because its virtual disk is corrupted.\r\n"
|
|
L"Error code: Wsl/Service/CreateInstance/WSL_E_DISK_CORRUPTED\r\n");
|
|
|
|
// Validate that trying to export the distribution fails with the correct error code.
|
|
validateOutput(
|
|
L"--export BrokenDistro dummy.tar",
|
|
L"The distribution failed to start because its virtual disk is corrupted.\r\n"
|
|
L"Error code: Wsl/Service/WSL_E_DISK_CORRUPTED\r\n");
|
|
|
|
// Shutdown WSL to force the disk to detach.
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--shutdown"), 0L);
|
|
}
|
|
|
|
// Import a corrupted vhd.
|
|
validateOutput(
|
|
std::format(L"--import-in-place test-distro-corrupted \"{}\"", vhdPath.wstring()),
|
|
L"The distribution failed to start because its virtual disk is corrupted.\r\n"
|
|
L"Error code: Wsl/Service/RegisterDistro/WSL_E_DISK_CORRUPTED\r\n");
|
|
|
|
// Ensure the VHD can be deleted to make sure it was properly ejected from the VM.
|
|
VERIFY_ARE_EQUAL(DeleteFileW(vhdPath.c_str()), TRUE);
|
|
}
|
|
|
|
static void ValidateDistributionShortcut(LPCWSTR DistroName, HANDLE ExpectedIcon)
|
|
{
|
|
auto distroKey = OpenDistributionKey(DistroName);
|
|
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
|
|
auto shellLink = wil::CoCreateInstance<IShellLink>(CLSID_ShellLink);
|
|
auto startMenu = wsl::windows::common::filesystem::GetKnownFolderPath(FOLDERID_StartMenu, KF_FLAG_CREATE);
|
|
|
|
// Validate that the shortcut is actually in the start menu
|
|
VERIFY_IS_TRUE(shortcutPath.find(startMenu) != std::string::npos);
|
|
|
|
auto storage = shellLink.query<IPersistFile>();
|
|
|
|
VERIFY_SUCCEEDED(storage->Load(shortcutPath.c_str(), 0));
|
|
|
|
std::wstring target(MAX_PATH, '\0');
|
|
|
|
WIN32_FIND_DATA findData{};
|
|
VERIFY_SUCCEEDED(shellLink->GetPath(target.data(), static_cast<int>(target.size()), &findData, SLGP_RAWPATH));
|
|
target.resize(wcslen(target.c_str()));
|
|
|
|
static auto wslExePath = wsl::windows::common::wslutil::GetMsiPackagePath().value() + L"wsl.exe";
|
|
VERIFY_ARE_EQUAL(target, wslExePath);
|
|
|
|
std::wstring arguments(MAX_PATH, '\0');
|
|
VERIFY_SUCCEEDED(shellLink->GetArguments(arguments.data(), static_cast<int>(arguments.size())));
|
|
arguments.resize(wcslen(arguments.c_str()));
|
|
|
|
auto distroId = GetDistributionId(DistroName);
|
|
VERIFY_IS_TRUE(distroId.has_value());
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
std::format(L"{} {} {} {}", WSL_DISTRIBUTION_ID_ARG, wsl::shared::string::GuidToString<wchar_t>(distroId.value()), WSL_CHANGE_DIRECTORY_ARG, WSL_CWD_HOME),
|
|
arguments);
|
|
|
|
std::wstring iconLocation(MAX_PATH, '\0');
|
|
int id{};
|
|
THROW_IF_FAILED(shellLink->GetIconLocation(iconLocation.data(), static_cast<int>(iconLocation.size()), &id));
|
|
iconLocation.resize(wcslen(iconLocation.c_str()));
|
|
|
|
if (ExpectedIcon == nullptr)
|
|
{
|
|
VERIFY_ARE_EQUAL(iconLocation, wslExePath);
|
|
}
|
|
else
|
|
{
|
|
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
|
|
|
|
// Validate that the icon is under the distribution folder.
|
|
VERIFY_IS_TRUE(iconLocation.find(basePath) != std::string::npos);
|
|
|
|
// Validate that the icon has the content we expect.
|
|
wil::unique_handle distroIcon{CreateFile(iconLocation.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr)};
|
|
VERIFY_ARE_EQUAL(GetFileSize(ExpectedIcon, nullptr), GetFileSize(distroIcon.get(), nullptr));
|
|
}
|
|
}
|
|
|
|
static std::pair<nlohmann::json, std::wstring> ValidateDistributionTerminalProfile(const std::wstring& DistroName, bool defaultIcon)
|
|
{
|
|
using namespace wsl::windows::common::wslutil;
|
|
using namespace wsl::windows::common::string;
|
|
|
|
auto distroKey = OpenDistributionKey(DistroName.c_str());
|
|
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
|
|
|
|
auto distroId = GetDistributionId(DistroName.c_str());
|
|
VERIFY_IS_TRUE(distroId.has_value());
|
|
|
|
auto distroIdString = wsl::shared::string::GuidToString<wchar_t>(distroId.value());
|
|
auto distributionProfileId =
|
|
wsl::shared::string::GuidToString<wchar_t>(CreateV5Uuid(WslTerminalNamespace, std::as_bytes(std::span{distroIdString})));
|
|
|
|
auto profilePath = wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"Microsoft" / L"Windows Terminal" /
|
|
L"Fragments" / L"Microsoft.WSL" / (distributionProfileId + L".json");
|
|
|
|
std::ifstream file{profilePath};
|
|
VERIFY_IS_TRUE(file.good());
|
|
|
|
nlohmann::json json;
|
|
VERIFY_IS_TRUE((file >> json).good());
|
|
|
|
VERIFY_IS_TRUE(json.is_object());
|
|
|
|
auto profiles = json.find("profiles");
|
|
VERIFY_ARE_NOT_EQUAL(profiles, json.end());
|
|
VERIFY_IS_TRUE(profiles->is_array());
|
|
|
|
VERIFY_IS_TRUE(profiles->size() >= 2);
|
|
const auto profileHide = profiles->at(0);
|
|
|
|
auto expectedHideGuid = wsl::shared::string::GuidToString<wchar_t>(
|
|
CreateV5Uuid(GeneratedProfilesTerminalNamespace, std::as_bytes(std::span{DistroName})));
|
|
VERIFY_ARE_EQUAL(profileHide["updates"], wsl::shared::string::WideToMultiByte(expectedHideGuid));
|
|
VERIFY_ARE_EQUAL(profileHide["hidden"], true);
|
|
|
|
const auto launchProfile = profiles->at(1);
|
|
|
|
auto expectedId =
|
|
wsl::shared::string::GuidToString<wchar_t>(CreateV5Uuid(WslTerminalNamespace, std::as_bytes(std::span{distroIdString})));
|
|
VERIFY_ARE_EQUAL(launchProfile["guid"].get<std::string>(), wsl::shared::string::WideToMultiByte(expectedId));
|
|
VERIFY_ARE_EQUAL(launchProfile["name"].get<std::string>(), wsl::shared::string::WideToMultiByte(DistroName));
|
|
VERIFY_ARE_EQUAL(launchProfile["pathTranslationStyle"].get<std::string>(), "wsl");
|
|
|
|
std::wstring systemDir;
|
|
wil::GetSystemDirectoryW(systemDir);
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
std::format("{}\\{} {} {}", systemDir, WSL_BINARY_NAME, WSL_DISTRIBUTION_ID_ARG, distroIdString),
|
|
launchProfile["commandline"].get<std::string>());
|
|
|
|
// Verify that startingDirectory is set to home directory
|
|
VERIFY_ARE_EQUAL(launchProfile["startingDirectory"].get<std::string>(), "~");
|
|
|
|
auto iconLocation = wsl::shared::string::MultiByteToWide(launchProfile["icon"].get<std::string>());
|
|
if (defaultIcon)
|
|
{
|
|
static auto wslExePath = wsl::windows::common::wslutil::GetMsiPackagePath().value() + L"wsl.exe";
|
|
VERIFY_ARE_EQUAL(iconLocation, wslExePath);
|
|
}
|
|
else
|
|
{
|
|
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
|
|
|
|
// Validate that the icon is under the distribution folder.
|
|
VERIFY_IS_TRUE(iconLocation.find(basePath) == 0);
|
|
}
|
|
|
|
return std::make_pair(json, profilePath);
|
|
}
|
|
|
|
TEST_METHOD(ConvertDistro)
|
|
{
|
|
std::wstring originalVersion;
|
|
std::wstring targetVersion;
|
|
if (LxsstuVmMode())
|
|
{
|
|
originalVersion = L"2";
|
|
targetVersion = L"1";
|
|
}
|
|
else
|
|
{
|
|
originalVersion = L"1";
|
|
targetVersion = L"2";
|
|
}
|
|
|
|
auto cleanup =
|
|
wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LxsstuLaunchWsl(L"--set-version test_distro " + originalVersion); });
|
|
|
|
// Convert the test distribution to the target version and back to the original.
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--set-version test_distro " + targetVersion), 0u);
|
|
ValidateDistributionShortcut(LXSS_DISTRO_NAME_TEST_L, nullptr);
|
|
ValidateDistributionTerminalProfile(LXSS_DISTRO_NAME_TEST_L, true);
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--set-version test_distro " + originalVersion), 0u);
|
|
ValidateDistributionShortcut(LXSS_DISTRO_NAME_TEST_L, nullptr);
|
|
ValidateDistributionTerminalProfile(LXSS_DISTRO_NAME_TEST_L, true);
|
|
|
|
// Do not convert the test distribution if it is already in the original version.
|
|
cleanup.release();
|
|
}
|
|
|
|
WSL2_TEST_METHOD(ManualDistroShutdown)
|
|
{
|
|
// Terminate a distribution from within WSL. This command should be terminated by the VM terminating
|
|
LxsstuLaunchWsl(L"echo foo > /dev/shm/bar ; reboot -f ; sleep 1d");
|
|
|
|
// Wait for distribution to be terminated to avoid running the next command as it shuts down
|
|
auto pred = []() {
|
|
const auto commandLine = LxssGenerateWslCommandLine(L"--list --running");
|
|
wsl::windows::common::SubProcess process(nullptr, commandLine.c_str());
|
|
|
|
// Don't check the exit code since that command returns -1 when no distros are running.
|
|
const auto output = process.RunAndCaptureOutput();
|
|
THROW_HR_IF(E_ABORT, output.Stdout.find(LXSS_DISTRO_NAME_TEST_L) != std::string::npos);
|
|
};
|
|
|
|
wsl::shared::retry::RetryWithTimeout<void>(pred, std::chrono::seconds(1), std::chrono::minutes(2));
|
|
|
|
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"test -f /dev/shm/bar2 || echo -n ok");
|
|
VERIFY_ARE_EQUAL(out, L"ok");
|
|
}
|
|
|
|
WSL2_TEST_METHOD(KernelModules)
|
|
{
|
|
// Get the kernel version and strip off everything after the first dash.
|
|
std::wstring kernelVersion{TEXT(KERNEL_VERSION)};
|
|
auto position = kernelVersion.find_first_of(L"-");
|
|
if (position != kernelVersion.npos)
|
|
{
|
|
kernelVersion = kernelVersion.substr(0, position);
|
|
}
|
|
|
|
kernelVersion += L"-microsoft-standard-WSL2";
|
|
|
|
// Ensure the kernel modules folder is mounted correctly.
|
|
std::wstring command = std::format(
|
|
L"mount | grep -iF 'none on /usr/lib/modules/{} type overlay "
|
|
L"(rw,nosuid,nodev,noatime,lowerdir=/modules,upperdir=/lib/modules/{}/rw/upper,workdir=/lib/modules/{}/rw/"
|
|
L"work,uuid=on)'",
|
|
kernelVersion,
|
|
kernelVersion,
|
|
kernelVersion);
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(command.c_str(), nullptr, nullptr, nullptr, nullptr), 0u);
|
|
|
|
// Update .wslconfig and ensure an error is displayed if nonexistent kernel or modules is specified.
|
|
const std::wstring wslConfigPath = wsl::windows::common::helpers::GetWslConfigPath();
|
|
const std::wstring nonExistentFile = L"DoesNotExist";
|
|
WslConfigChange configChange(LxssGenerateTestConfig({.kernel = nonExistentFile.c_str()}));
|
|
ValidateOutput(
|
|
L"echo ok",
|
|
std::format(
|
|
L"{}\r\nError code: Wsl/Service/CreateInstance/CreateVm/WSL_E_CUSTOM_KERNEL_NOT_FOUND\r\n",
|
|
wsl::shared::Localization::MessageCustomKernelNotFound(wslConfigPath, nonExistentFile)),
|
|
L"");
|
|
|
|
configChange.Update(LxssGenerateTestConfig({.kernelModules = nonExistentFile.c_str()}));
|
|
ValidateOutput(
|
|
L"echo ok",
|
|
std::format(
|
|
L"{}\r\nError code: Wsl/Service/CreateInstance/CreateVm/WSL_E_CUSTOM_KERNEL_NOT_FOUND\r\n",
|
|
wsl::shared::Localization::MessageCustomKernelModulesNotFound(wslConfigPath, nonExistentFile)),
|
|
L"");
|
|
|
|
#ifdef WSL_DEV_INSTALL_PATH
|
|
|
|
std::wstring kernelPath = WSL_DEV_INSTALL_PATH L"/kernel";
|
|
std::wstring kernelModulesPath = WSL_DEV_INSTALL_PATH L"/modules.vhd";
|
|
|
|
#else
|
|
|
|
auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath();
|
|
VERIFY_IS_TRUE(installPath.has_value());
|
|
|
|
std::filesystem::path wslInstallPath(installPath.value());
|
|
|
|
std::wstring kernelPath = wslInstallPath / "tools" / "kernel";
|
|
std::wstring kernelModulesPath = wslInstallPath / "tools" / "modules.vhd";
|
|
|
|
#endif
|
|
|
|
// Verify that no modules are mounted for a custom kernel with no modules specified.
|
|
kernelPath = std::regex_replace(kernelPath, std::wregex(L"\\\\"), L"\\\\");
|
|
configChange.Update(LxssGenerateTestConfig({.kernel = kernelPath.c_str()}));
|
|
ValidateOutput(command.c_str(), L"", L"", 1);
|
|
|
|
// Verify the error message if custom kernel modules are used with the default kernel.
|
|
kernelModulesPath = std::regex_replace(kernelModulesPath, std::wregex(L"\\\\"), L"\\\\");
|
|
configChange.Update(LxssGenerateTestConfig({.kernelModules = kernelModulesPath.c_str()}));
|
|
ValidateOutput(
|
|
L"echo ok",
|
|
std::format(
|
|
L"{}\r\nError code: Wsl/Service/CreateInstance/CreateVm/WSL_E_CUSTOM_KERNEL_NOT_FOUND\r\n",
|
|
wsl::shared::Localization::MessageMismatchedKernelModulesError()),
|
|
L"");
|
|
|
|
configChange.Update(LxssGenerateTestConfig());
|
|
|
|
// Validate that tun is loaded by default.
|
|
ValidateOutput(L"grep -i '^tun' /proc/modules | wc -l", L"1\n", L"", 0);
|
|
|
|
// Validate a VM can boot with no extra additional kernel modules.
|
|
configChange.Update(LxssGenerateTestConfig({.loadDefaultKernelModules = false}));
|
|
ValidateOutput(L"grep -i '^tun' /proc/modules | wc -l", L"0\n", L"", 0);
|
|
|
|
// Validate that the user can pass additional modules to load at boot.
|
|
ValidateOutput(L"grep -iE '^(usb_storage|dm_crypt)' /proc/modules | wc -l", L"0\n", L"", 0);
|
|
|
|
configChange.Update(LxssGenerateTestConfig({.loadKernelModules = L"usb_storage,dm_crypt"}));
|
|
ValidateOutput(L"grep -iE '^(usb_storage|dm_crypt)' /proc/modules | wc -l", L"2\n", L"", 0);
|
|
|
|
// Validate that failing to load a module shows a warning in dmesg.
|
|
configChange.Update(LxssGenerateTestConfig({.loadKernelModules = L"not-found"}));
|
|
ValidateOutput(L"dmesg | grep -iF \"failed to load module 'not-found'\" | wc -l", L"1\n", L"", 0);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(CrashCollection)
|
|
{
|
|
const auto folder = std::filesystem::absolute(L"test-crash-dumps");
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
std::error_code error;
|
|
std::filesystem::remove_all(folder, error);
|
|
});
|
|
|
|
auto countCrashes = [&]() {
|
|
std::error_code error;
|
|
return std::distance(std::filesystem::directory_iterator{folder, error}, std::filesystem::directory_iterator{});
|
|
};
|
|
|
|
auto waitForCrashes = [&](int expected) {
|
|
wsl::shared::retry::RetryWithTimeout<void>(
|
|
[&]() { THROW_HR_IF(E_UNEXPECTED, countCrashes() < expected); }, std::chrono::seconds(1), std::chrono::minutes(2));
|
|
|
|
VERIFY_ARE_EQUAL(countCrashes(), expected);
|
|
};
|
|
|
|
auto crash = []() { LxsstuLaunchWsl(L"kill -SEGV $$"); };
|
|
|
|
WslConfigChange change(LxssGenerateTestConfig({.crashDumpCount = 2, .CrashDumpFolder = folder.wstring()}));
|
|
|
|
VERIFY_ARE_EQUAL(countCrashes(), 0);
|
|
|
|
crash();
|
|
waitForCrashes(1);
|
|
|
|
crash();
|
|
waitForCrashes(2);
|
|
|
|
crash();
|
|
waitForCrashes(2);
|
|
|
|
// Create a dummy file and validate that the file limit logic doesn't remove it.
|
|
std::filesystem::remove_all(folder);
|
|
std::filesystem::create_directory(folder);
|
|
std::ofstream(folder / "dummy").close();
|
|
|
|
crash();
|
|
waitForCrashes(2);
|
|
|
|
crash();
|
|
waitForCrashes(3);
|
|
|
|
crash();
|
|
waitForCrashes(3);
|
|
|
|
VERIFY_IS_TRUE(std::filesystem::exists(folder / "dummy"));
|
|
}
|
|
|
|
// UnitTests Private Methods
|
|
|
|
static VOID VerifyCaseSensitiveDirectory(_In_ PCWSTR RelativePath)
|
|
{
|
|
|
|
const std::wstring Path = LxsstuGetLxssDirectory() + L"\\" + RelativePath;
|
|
const wil::unique_hfile Directory{CreateFileW(
|
|
Path.c_str(),
|
|
FILE_READ_ATTRIBUTES,
|
|
(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE),
|
|
nullptr,
|
|
OPEN_EXISTING,
|
|
(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT),
|
|
nullptr)};
|
|
|
|
THROW_LAST_ERROR_IF(!Directory);
|
|
IO_STATUS_BLOCK IoStatus;
|
|
FILE_CASE_SENSITIVE_INFORMATION CaseInfo;
|
|
THROW_IF_NTSTATUS_FAILED(NtQueryInformationFile(Directory.get(), &IoStatus, &CaseInfo, sizeof(CaseInfo), FileCaseSensitiveInformation));
|
|
|
|
VERIFY_ARE_EQUAL(CaseInfo.Flags, (ULONG)FILE_CS_FLAG_CASE_SENSITIVE_DIR);
|
|
}
|
|
|
|
TEST_METHOD(Move)
|
|
{
|
|
constexpr auto name = L"move-test-distro";
|
|
constexpr auto testFolder = L"move-test-test-folder";
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", name, g_testDistroPath)), 0L);
|
|
|
|
auto cleanupName = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [name]() {
|
|
LxsstuLaunchWsl(std::format(L"--unregister {}", name));
|
|
std::filesystem::remove_all(testFolder);
|
|
});
|
|
|
|
auto validateDistro = []() {
|
|
auto [cmdOutput, _] = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
|
|
VERIFY_ARE_EQUAL(cmdOutput, L"ok\n");
|
|
};
|
|
|
|
// Move the distro to a different folder (relative path)
|
|
{
|
|
WslShutdown();
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--manage {} --move {}", name, testFolder)), 0L);
|
|
|
|
// Validate that the distribution still starts
|
|
validateDistro();
|
|
VERIFY_IS_TRUE(std::filesystem::exists(std::format(L"{}\\ext4.vhdx", testFolder)));
|
|
}
|
|
|
|
auto absolutePath = std::filesystem::weakly_canonical(".").wstring();
|
|
|
|
// Move the distro to a different folder (absolute path)
|
|
{
|
|
WslShutdown();
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--manage {} --move {}", name, absolutePath)), 0L);
|
|
|
|
// Validate that the distribution still starts
|
|
validateDistro();
|
|
VERIFY_IS_TRUE(std::filesystem::exists(std::format(L"{}\\ext4.vhdx", absolutePath)));
|
|
}
|
|
|
|
// Try to move the distribution to a folder that's already in use
|
|
{
|
|
WslShutdown();
|
|
|
|
wil::unique_cotaskmem_string path;
|
|
THROW_IF_FAILED(::SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &path));
|
|
auto targetPath = std::format(L"{}\\lxss", path.get());
|
|
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --move {}", name, targetPath), -1);
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
out,
|
|
L"The supplied install location is already in use.\r\nError code: "
|
|
L"Wsl/Service/MoveDistro/ERROR_FILE_EXISTS\r\n");
|
|
// Validate that the distribution still starts and that the vhd hasn't moved.
|
|
validateDistro();
|
|
VERIFY_IS_TRUE(std::filesystem::exists(std::format(L"{}\\ext4.vhdx", absolutePath)));
|
|
}
|
|
|
|
// Try to move the distribution to an invalid path
|
|
{
|
|
WslShutdown();
|
|
|
|
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --move :", name), -1);
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
out,
|
|
L"The filename, directory name, or volume label syntax is incorrect. \r\nError code: "
|
|
L"Wsl/Service/MoveDistro/ERROR_INVALID_NAME\r\n");
|
|
// Validate that the distribution still starts and that the vhd hasn't moved.
|
|
validateDistro();
|
|
VERIFY_IS_TRUE(std::filesystem::exists(std::format(L"{}\\ext4.vhdx", absolutePath)));
|
|
}
|
|
}
|
|
|
|
WSL2_TEST_METHOD(MoveVhdOwnership)
|
|
{
|
|
constexpr auto name = L"move-owner-test-distro";
|
|
constexpr auto moveElevatedFolder = L"move-owner-elevated";
|
|
constexpr auto moveNonElevatedFolder = L"move-owner-non-elevated";
|
|
|
|
// Import a WSL2 distro.
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", name, g_testDistroPath)), 0L);
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [name]() {
|
|
LxsstuLaunchWsl(std::format(L"--unregister {}", name));
|
|
std::filesystem::remove_all(moveElevatedFolder);
|
|
std::filesystem::remove_all(moveNonElevatedFolder);
|
|
});
|
|
|
|
auto verifyVhdOwner = [](const std::wstring& path) {
|
|
PSID ownerSid = nullptr;
|
|
wil::unique_hlocal descriptor;
|
|
THROW_IF_WIN32_ERROR(GetNamedSecurityInfoW(
|
|
path.c_str(), SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, &ownerSid, nullptr, nullptr, nullptr, &descriptor));
|
|
|
|
auto userToken = wil::open_current_access_token(TOKEN_QUERY);
|
|
auto tokenUser = wil::get_token_information<TOKEN_USER>(userToken.get());
|
|
|
|
VERIFY_IS_TRUE(EqualSid(ownerSid, tokenUser->User.Sid));
|
|
};
|
|
|
|
const auto nonElevatedToken = GetNonElevatedToken();
|
|
|
|
// Move as elevated, launch as non-elevated.
|
|
// This is the primary bug scenario: MoveFileEx sets owner to BUILTIN\Administrators,
|
|
// then HcsGrantVmAccess fails with E_ACCESSDENIED when impersonating the non-elevated user.
|
|
{
|
|
WslShutdown();
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--manage {} --move {}", name, moveElevatedFolder)), 0L);
|
|
|
|
auto vhdPath = std::format(L"{}\\ext4.vhdx", moveElevatedFolder);
|
|
VERIFY_IS_TRUE(std::filesystem::exists(vhdPath));
|
|
verifyVhdOwner(vhdPath);
|
|
|
|
WslShutdown();
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo ok", name), 0, nullptr, nonElevatedToken.get());
|
|
VERIFY_ARE_EQUAL(out, L"ok\n");
|
|
}
|
|
|
|
// Move as non-elevated, launch as elevated.
|
|
{
|
|
WslShutdown();
|
|
VERIFY_ARE_EQUAL(
|
|
LxsstuLaunchWsl(
|
|
std::format(L"--manage {} --move {}", name, moveNonElevatedFolder),
|
|
nullptr,
|
|
nullptr,
|
|
nullptr,
|
|
nonElevatedToken.get()),
|
|
0L);
|
|
|
|
auto vhdPath = std::format(L"{}\\ext4.vhdx", moveNonElevatedFolder);
|
|
VERIFY_IS_TRUE(std::filesystem::exists(vhdPath));
|
|
verifyVhdOwner(vhdPath);
|
|
|
|
WslShutdown();
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo ok", name));
|
|
VERIFY_ARE_EQUAL(out, L"ok\n");
|
|
}
|
|
|
|
// Also launch as non-elevated after the non-elevated move.
|
|
{
|
|
WslShutdown();
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo ok", name), 0, nullptr, nonElevatedToken.get());
|
|
VERIFY_ARE_EQUAL(out, L"ok\n");
|
|
}
|
|
}
|
|
|
|
WSL2_TEST_METHOD(Resize)
|
|
{
|
|
constexpr auto name = L"resize-test-distro";
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", name, g_testDistroPath)), 0L);
|
|
WslShutdown();
|
|
|
|
auto cleanupName =
|
|
wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [name]() { LxsstuLaunchWsl(std::format(L"--unregister {}", name)); });
|
|
|
|
auto validateDistro = [name](LPCWSTR size, LPCWSTR expectedSize, LPCWSTR expectedError = nullptr) {
|
|
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --resize {}", name, size), expectedError ? -1 : 0);
|
|
if (expectedError)
|
|
{
|
|
VERIFY_ARE_EQUAL(expectedError, out);
|
|
return;
|
|
}
|
|
|
|
std::tie(out, _) = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} df -h / --output=size | sed 1d", name));
|
|
VERIFY_ARE_EQUAL(std::format(L" {}\n", expectedSize), out);
|
|
WslShutdown();
|
|
};
|
|
|
|
validateDistro(L"1500G", L"1.5T");
|
|
validateDistro(L"500G", L"492G");
|
|
validateDistro(L"1M", nullptr, L"Failed to resize disk.\r\nError code: Wsl/Service/E_FAIL\r\n");
|
|
|
|
{
|
|
WslKeepAlive keepAlive;
|
|
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"--manage test_distro --resize 1500GB", -1);
|
|
VERIFY_ARE_EQUAL(
|
|
L"The operation could not be completed because the VHD is currently in use. To force WSL to stop use: wsl.exe "
|
|
L"--shutdown\r\nError code: Wsl/Service/WSL_E_DISTRO_NOT_STOPPED\r\n",
|
|
out);
|
|
}
|
|
}
|
|
|
|
WSL2_TEST_METHOD(FileOffsets)
|
|
{
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { DeleteFile(L"output.txt"); });
|
|
|
|
std::ofstream file("output.txt");
|
|
VERIFY_IS_TRUE(file.good() && file << "previous content\n");
|
|
file.close();
|
|
|
|
std::wstring cmd(L"C:\\windows\\system32\\cmd.exe /c \"wsl.exe echo ok >> output.txt && type output.txt\"");
|
|
auto [output, _] = LxsstuLaunchCommandAndCaptureOutput(cmd.data());
|
|
|
|
VERIFY_ARE_EQUAL(output, L"previous content\r\nok\n");
|
|
}
|
|
|
|
TEST_METHOD(GlobalFlagsOverride)
|
|
{
|
|
auto isDriveMountingEnabled = []() { return LxsstuLaunchWsl(L"test -d /mnt/c/Windows") == 0; };
|
|
|
|
VERIFY_IS_TRUE(isDriveMountingEnabled());
|
|
|
|
{
|
|
RegistryKeyChange<DWORD> key(HKEY_LOCAL_MACHINE, LXSS_SERVICE_REGISTRY_PATH, L"DistributionFlags", ~LXSS_DISTRO_FLAGS_ENABLE_DRIVE_MOUNTING);
|
|
|
|
TerminateDistribution();
|
|
VERIFY_IS_FALSE(isDriveMountingEnabled());
|
|
}
|
|
|
|
TerminateDistribution();
|
|
VERIFY_IS_TRUE(isDriveMountingEnabled());
|
|
}
|
|
|
|
WSL2_TEST_METHOD(WriteWslConfig)
|
|
{
|
|
WSL_SETTINGS_TEST();
|
|
|
|
auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath();
|
|
VERIFY_IS_TRUE(installPath.has_value());
|
|
|
|
std::filesystem::path wslInstallPath(installPath.value());
|
|
std::filesystem::path libWslDllPath = wslInstallPath / "libwsl.dll";
|
|
VERIFY_IS_TRUE(std::filesystem::exists(libWslDllPath));
|
|
|
|
LxssDynamicFunction<decltype(GetWslConfigFilePath)> getWslConfigFilePath(libWslDllPath.c_str(), "GetWslConfigFilePath");
|
|
LxssDynamicFunction<decltype(CreateWslConfig)> createWslConfig(libWslDllPath.c_str(), "CreateWslConfig");
|
|
LxssDynamicFunction<decltype(FreeWslConfig)> freeWslConfig(libWslDllPath.c_str(), "FreeWslConfig");
|
|
LxssDynamicFunction<decltype(GetWslConfigSetting)> getWslConfigSetting(libWslDllPath.c_str(), "GetWslConfigSetting");
|
|
LxssDynamicFunction<decltype(SetWslConfigSetting)> setWslConfigSetting(libWslDllPath.c_str(), "SetWslConfigSetting");
|
|
|
|
// Reset the test config file. The original has already been saved as part of module setup.
|
|
auto wslConfigFilePath = getenv("userprofile") + std::string("\\.wslconfig");
|
|
WslConfigChange config{L""};
|
|
|
|
auto apiWslConfigFilePath = getWslConfigFilePath();
|
|
VERIFY_IS_TRUE(std::filesystem::path(wslConfigFilePath) == std::filesystem::path(apiWslConfigFilePath));
|
|
|
|
auto wslConfigDefaults = createWslConfig(nullptr);
|
|
VERIFY_IS_NOT_NULL(wslConfigDefaults);
|
|
auto wslConfig = createWslConfig(apiWslConfigFilePath);
|
|
VERIFY_IS_NOT_NULL(wslConfig);
|
|
|
|
freeWslConfig(wslConfigDefaults);
|
|
freeWslConfig(wslConfig);
|
|
|
|
WslConfigSetting wslConfigSettingWriteOut;
|
|
WslConfigSetting wslConfigSettingReadIn;
|
|
|
|
auto testLoop = [&](auto& testPlan, auto& updateWslConfigSettingWriteOutValue, auto& verifyWslConfigSettingValueReadEqual) {
|
|
wslConfigSettingWriteOut = wslConfigSettingReadIn = WslConfigSetting{};
|
|
for (const auto testEntry : testPlan)
|
|
{
|
|
wslConfigSettingWriteOut = testEntry.first;
|
|
for (const auto& test : testEntry.second)
|
|
{
|
|
const auto& writeValue = test.first;
|
|
const auto& expectedValue = test.second;
|
|
{
|
|
// This scenario tests writing a value to the config file and reading it back. If the write succeeded,
|
|
// the written value will be cached in the WslConfig object. The read will then return the cached value.
|
|
wslConfig = createWslConfig(apiWslConfigFilePath);
|
|
VERIFY_IS_NOT_NULL(wslConfig);
|
|
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
|
|
|
|
updateWslConfigSettingWriteOutValue(wslConfigSettingWriteOut, writeValue);
|
|
|
|
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigSettingWriteOut), ERROR_SUCCESS);
|
|
wslConfigSettingReadIn = getWslConfigSetting(wslConfig, wslConfigSettingWriteOut.ConfigEntry);
|
|
VERIFY_ARE_EQUAL(wslConfigSettingReadIn.ConfigEntry, wslConfigSettingWriteOut.ConfigEntry);
|
|
verifyWslConfigSettingValueReadEqual(wslConfigSettingReadIn, expectedValue);
|
|
}
|
|
{
|
|
// This scenario tests reading a value from the config file. Specifically, it will parse in the
|
|
// written value to the wsl config file from the previous scenario. This validates parsing the value
|
|
// from the file (e.g. that it was written correctly and then parsed as expected).
|
|
wslConfig = createWslConfig(apiWslConfigFilePath);
|
|
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
|
|
wslConfigSettingReadIn = getWslConfigSetting(wslConfig, wslConfigSettingWriteOut.ConfigEntry);
|
|
VERIFY_ARE_EQUAL(wslConfigSettingReadIn.ConfigEntry, wslConfigSettingWriteOut.ConfigEntry);
|
|
verifyWslConfigSettingValueReadEqual(wslConfigSettingReadIn, expectedValue);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
{
|
|
// Enable NetworkingMode::Mirrored for IgnoredPorts to be set correctly upon parsing.
|
|
WslConfigChange config(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored}));
|
|
|
|
// std::pair[0] = Written value, std::pair[1] = Actual/Expected value
|
|
static const std::vector<std::pair<PCWSTR, PCWSTR>> filePathsToTest{
|
|
{L"C:\\DoesNotExit\\ext4.vhdx", L"C:\\DoesNotExit\\ext4.vhdx"},
|
|
{L"\\DoesNotExit\\ext4.vhdx", L"\\DoesNotExit\\ext4.vhdx"},
|
|
{L"", L""},
|
|
};
|
|
|
|
// tuple: WslConfigSetting, expectedValue, actualValue
|
|
std::vector<std::pair<WslConfigSetting, std::vector<std::pair<PCWSTR, PCWSTR>>>> wslConfigSettingStringTestPlan{
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::SwapFilePath},
|
|
filePathsToTest,
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::IgnoredPorts},
|
|
{
|
|
{L"1,2,300,4455,65535", L"1,2,300,4455,65535"},
|
|
{L"10,20,-100,p", L"10,20"},
|
|
{L"100,200,notaport", L"100,200"},
|
|
{L"1000,2000;3.4", L"1000,2000"},
|
|
{L"10000, 20000, 30000,40000 ,50000", L"10000,20000,30000,40000,50000"},
|
|
{L"", L""},
|
|
{L"notaport", L""},
|
|
{L"-5555", L""},
|
|
{L"C:\\DoesNotExit\\ext4.vhdx", L""},
|
|
},
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::KernelPath},
|
|
filePathsToTest,
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::SystemDistroPath},
|
|
filePathsToTest,
|
|
},
|
|
};
|
|
|
|
auto updateWslConfigSettingWriteOutStringValue = [](auto& wslConfigSettingWriteOut, auto& writeValue) {
|
|
wslConfigSettingWriteOut.StringValue = writeValue;
|
|
};
|
|
|
|
auto verifyWslConfigSettingReadStringValueEqual = [](auto& wslConfigSettingReadIn, auto& expectedValue) {
|
|
VERIFY_ARE_EQUAL(std::wstring_view(wslConfigSettingReadIn.StringValue), std::wstring_view(expectedValue));
|
|
};
|
|
|
|
testLoop(wslConfigSettingStringTestPlan, updateWslConfigSettingWriteOutStringValue, verifyWslConfigSettingReadStringValueEqual);
|
|
}
|
|
|
|
{
|
|
wslConfigSettingWriteOut = wslConfigSettingReadIn = WslConfigSetting{};
|
|
wslConfigSettingWriteOut.ConfigEntry = WslConfigEntry::NoEntry;
|
|
|
|
wslConfig = createWslConfig(apiWslConfigFilePath);
|
|
VERIFY_IS_NOT_NULL(wslConfig);
|
|
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
|
|
|
|
wslConfigSettingReadIn = getWslConfigSetting(wslConfig, wslConfigSettingWriteOut.ConfigEntry);
|
|
VERIFY_ARE_EQUAL(wslConfigSettingReadIn.ConfigEntry, wslConfigSettingWriteOut.ConfigEntry);
|
|
}
|
|
|
|
SYSTEM_INFO systemInfo{};
|
|
GetSystemInfo(&systemInfo);
|
|
{
|
|
// std::pair[0] = Written value, std::pair[1] = Actual/Expected value
|
|
static const std::vector<std::pair<int, int>> timeoutValuesToTest{
|
|
{-132445, -132445},
|
|
{0, 0},
|
|
{1, 1},
|
|
{13456, 13456},
|
|
{100000000, 100000000},
|
|
};
|
|
|
|
// tuple: WslConfigSetting, expectedValue, actualValue
|
|
std::vector<std::pair<WslConfigSetting, std::vector<std::pair<int, int>>>> wslConfigSettingInt32TestPlan{
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::ProcessorCount},
|
|
{
|
|
{-123443, systemInfo.dwNumberOfProcessors},
|
|
{-1, systemInfo.dwNumberOfProcessors},
|
|
{1, 1},
|
|
{2, std::min(2, static_cast<int>(systemInfo.dwNumberOfProcessors))},
|
|
{systemInfo.dwNumberOfProcessors, systemInfo.dwNumberOfProcessors},
|
|
{1234, systemInfo.dwNumberOfProcessors},
|
|
},
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::InitialAutoProxyTimeout},
|
|
timeoutValuesToTest,
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::VMIdleTimeout},
|
|
timeoutValuesToTest,
|
|
},
|
|
};
|
|
|
|
auto updateWslConfigSettingWriteOutInt32Value = [](auto& wslConfigSettingWriteOut, auto& writeValue) {
|
|
wslConfigSettingWriteOut.Int32Value = writeValue;
|
|
};
|
|
|
|
auto verifyWslConfigSettingReadInt32ValueEqual = [](auto& wslConfigSettingReadIn, auto& expectedValue) {
|
|
VERIFY_ARE_EQUAL(wslConfigSettingReadIn.Int32Value, expectedValue);
|
|
};
|
|
|
|
testLoop(wslConfigSettingInt32TestPlan, updateWslConfigSettingWriteOutInt32Value, verifyWslConfigSettingReadInt32ValueEqual);
|
|
}
|
|
|
|
{
|
|
MEMORYSTATUSEX memInfo{sizeof(MEMORYSTATUSEX)};
|
|
THROW_IF_WIN32_BOOL_FALSE(GlobalMemoryStatusEx(&memInfo));
|
|
const auto minimumMemorySizeBytes = 256 * _1MB;
|
|
const auto maximumMemorySizeBytes = memInfo.ullTotalPhys;
|
|
|
|
// std::pair[0] = Written value, std::pair[1] = Actual/Expected value
|
|
static const std::vector<std::pair<unsigned long long, unsigned long long>> fileSizesBytesToTest{
|
|
{0, 0}, {1, 1}, {13456, 13456}, {100000000, 100000000}, {9223372036854775807, 9223372036854775807}};
|
|
|
|
// tuple: WslConfigSetting, expectedValue, actualValue
|
|
std::vector<std::pair<WslConfigSetting, std::vector<std::pair<unsigned long long, unsigned long long>>>> wslConfigSettingUInt64TestPlan{
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::MemorySizeBytes},
|
|
{
|
|
{0, maximumMemorySizeBytes / 2},
|
|
{minimumMemorySizeBytes / 2, minimumMemorySizeBytes},
|
|
{minimumMemorySizeBytes, minimumMemorySizeBytes},
|
|
{maximumMemorySizeBytes / 2, maximumMemorySizeBytes / 2},
|
|
{maximumMemorySizeBytes, maximumMemorySizeBytes},
|
|
{maximumMemorySizeBytes * 2, maximumMemorySizeBytes},
|
|
},
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::SwapSizeBytes},
|
|
fileSizesBytesToTest,
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::VhdSizeBytes},
|
|
fileSizesBytesToTest,
|
|
},
|
|
};
|
|
|
|
auto updateWslConfigSettingWriteOutUInt64Value = [](auto& wslConfigSettingWriteOut, auto& writeValue) {
|
|
wslConfigSettingWriteOut.UInt64Value = writeValue;
|
|
};
|
|
|
|
auto verifyWslConfigSettingReadUInt64ValueEqual = [](auto& wslConfigSettingReadIn, auto& expectedValue) {
|
|
VERIFY_ARE_EQUAL(wslConfigSettingReadIn.UInt64Value, expectedValue);
|
|
};
|
|
|
|
testLoop(wslConfigSettingUInt64TestPlan, updateWslConfigSettingWriteOutUInt64Value, verifyWslConfigSettingReadUInt64ValueEqual);
|
|
}
|
|
|
|
{
|
|
// Enable NetworkingMode::Mirrored for IgnoredPorts to be set correctly upon parsing.
|
|
WslConfigChange config(LxssGenerateTestConfig());
|
|
|
|
// std::pair[0] = Written value, std::pair[1] = Actual/Expected value
|
|
static const std::vector<std::pair<bool, bool>> booleansToTest{{false, false}, {true, true}};
|
|
|
|
// tuple: WslConfigSetting, expectedValue, actualValue
|
|
std::vector<std::pair<WslConfigSetting, std::vector<std::pair<bool, bool>>>> wslConfigSettingBooleanTestPlan{
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::FirewallEnabled},
|
|
booleansToTest,
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::LocalhostForwardingEnabled},
|
|
booleansToTest,
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::HostAddressLoopbackEnabled},
|
|
// This setting is only enabled when NetworkingMode != Mirrored.
|
|
{{false, false}, {true, false}},
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::AutoProxyEnabled},
|
|
booleansToTest,
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::DNSProxyEnabled},
|
|
booleansToTest,
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::DNSTunnelingEnabled},
|
|
// This setting is only enabled when NetworkingMode != Nat && NetworkingMode != Mirrored
|
|
booleansToTest,
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::BestEffortDNSParsingEnabled},
|
|
// This setting is only enabled when DNSTunnelingEnabled = true
|
|
booleansToTest,
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::GUIApplicationsEnabled},
|
|
booleansToTest,
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::NestedVirtualizationEnabled},
|
|
booleansToTest,
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::SafeModeEnabled},
|
|
booleansToTest,
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::SparseVHDEnabled},
|
|
booleansToTest,
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::DebugConsoleEnabled},
|
|
booleansToTest,
|
|
},
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::HardwarePerformanceCountersEnabled},
|
|
// This setting is disabled when SafeModeEnabled = true.
|
|
// Since testing SafeModeEnabled is tested earlier and left as
|
|
// true (.wslconfig is re-used), this setting should be false.
|
|
{{false, false}, {true, false}},
|
|
},
|
|
};
|
|
|
|
auto updateWslConfigSettingWriteOutBooleanValue = [](auto& wslConfigSettingWriteOut, auto& writeValue) {
|
|
wslConfigSettingWriteOut.BoolValue = writeValue;
|
|
};
|
|
|
|
auto verifyWslConfigSettingReadBooleanValueEqual = [](auto& wslConfigSettingReadIn, auto& expectedValue) {
|
|
VERIFY_ARE_EQUAL(wslConfigSettingReadIn.BoolValue, expectedValue);
|
|
};
|
|
|
|
testLoop(wslConfigSettingBooleanTestPlan, updateWslConfigSettingWriteOutBooleanValue, verifyWslConfigSettingReadBooleanValueEqual);
|
|
}
|
|
|
|
{
|
|
// std::pair[0] = Written value, std::pair[1] = Actual/Expected value
|
|
static const std::vector<std::pair<NetworkingConfiguration, NetworkingConfiguration>> networkingConfigurationsToTest{
|
|
{NetworkingConfiguration::None, NetworkingConfiguration::None},
|
|
{NetworkingConfiguration::Nat, NetworkingConfiguration::Nat},
|
|
{NetworkingConfiguration::Bridged, NetworkingConfiguration::Bridged},
|
|
{NetworkingConfiguration::Mirrored, NetworkingConfiguration::Mirrored},
|
|
{NetworkingConfiguration::VirtioProxy, NetworkingConfiguration::VirtioProxy},
|
|
};
|
|
|
|
// tuple: WslConfigSetting, expectedValue, actualValue
|
|
std::vector<std::pair<WslConfigSetting, std::vector<std::pair<NetworkingConfiguration, NetworkingConfiguration>>>> wslConfigSettingNetworkingConfigurationTestPlan{
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::Networking},
|
|
networkingConfigurationsToTest,
|
|
},
|
|
};
|
|
|
|
auto updateWslConfigSettingWriteOutNetworkingConfigurationValue = [](auto& wslConfigSettingWriteOut, auto& writeValue) {
|
|
wslConfigSettingWriteOut.NetworkingConfigurationValue = writeValue;
|
|
};
|
|
|
|
auto verifyWslConfigSettingReadNetworkingConfigurationValueEqual = [](auto& wslConfigSettingReadIn, auto& expectedValue) {
|
|
VERIFY_ARE_EQUAL(expectedValue, wslConfigSettingReadIn.NetworkingConfigurationValue);
|
|
};
|
|
|
|
testLoop(
|
|
wslConfigSettingNetworkingConfigurationTestPlan,
|
|
updateWslConfigSettingWriteOutNetworkingConfigurationValue,
|
|
verifyWslConfigSettingReadNetworkingConfigurationValueEqual);
|
|
}
|
|
|
|
{
|
|
// std::pair[0] = Written value, std::pair[1] = Actual/Expected value
|
|
static const std::vector<std::pair<MemoryReclaimConfiguration, MemoryReclaimConfiguration>> memoryReclaimModesToTest{
|
|
{MemoryReclaimConfiguration::Disabled, MemoryReclaimConfiguration::Disabled},
|
|
{MemoryReclaimConfiguration::Gradual, MemoryReclaimConfiguration::Gradual},
|
|
{MemoryReclaimConfiguration::DropCache, MemoryReclaimConfiguration::DropCache},
|
|
};
|
|
|
|
// tuple: WslConfigSetting, expectedValue, actualValue
|
|
std::vector<std::pair<WslConfigSetting, std::vector<std::pair<MemoryReclaimConfiguration, MemoryReclaimConfiguration>>>> wslConfigSettingMemoryReclaimModeTestPlan{
|
|
{
|
|
{.ConfigEntry = WslConfigEntry::AutoMemoryReclaim},
|
|
memoryReclaimModesToTest,
|
|
},
|
|
};
|
|
|
|
auto updateWslConfigSettingWriteOutMemoryReclaimModeValue = [](auto& wslConfigSettingWriteOut, auto& writeValue) {
|
|
wslConfigSettingWriteOut.MemoryReclaimModeValue = writeValue;
|
|
};
|
|
|
|
auto verifyWslConfigSettingReadMemoryReclaimModeValueEqual = [](auto& wslConfigSettingReadIn, auto& expectedValue) {
|
|
VERIFY_ARE_EQUAL(wslConfigSettingReadIn.MemoryReclaimModeValue, expectedValue);
|
|
};
|
|
|
|
testLoop(wslConfigSettingMemoryReclaimModeTestPlan, updateWslConfigSettingWriteOutMemoryReclaimModeValue, verifyWslConfigSettingReadMemoryReclaimModeValueEqual);
|
|
}
|
|
|
|
{
|
|
std::wstring customWslConfigContentOut{
|
|
LR"(
|
|
[wsl2] # trailing section comment
|
|
vmIdleTimeout=200 # property trailing comment
|
|
vmIdleTimeout=20000 # property trailing comment
|
|
vmIdleTimeout=20000 # property trailing comment
|
|
mountDeviceTimeout=120\
|
|
000
|
|
kernelBootTimeout=120000
|
|
|
|
# property comment
|
|
swapfile=E:\\wsl-b\
|
|
uild\\src\\win\
|
|
dows\\wslc\
|
|
ore\\lib\\swap.vhdx # multi-line property with trailing comment
|
|
telemetry=false
|
|
safeMode=false
|
|
guiApplications=true
|
|
earlyBootLogging=false
|
|
# comment 1
|
|
# comment 2
|
|
# \t \b
|
|
virtio9p=true # property trailing comment, ensure new property is appended to the section while preserving this comment
|
|
|
|
# section comment
|
|
[experimental]
|
|
autoProxy=false
|
|
|
|
[wsl2]
|
|
|
|
# end comment
|
|
)"};
|
|
|
|
WslConfigChange config(customWslConfigContentOut);
|
|
|
|
wslConfig = createWslConfig(apiWslConfigFilePath);
|
|
VERIFY_IS_NOT_NULL(wslConfig);
|
|
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
|
|
|
|
// The config contains multiple vmIdleTimeout entries. The first one should be updated/written.
|
|
wslConfigSettingWriteOut = WslConfigSetting{};
|
|
wslConfigSettingWriteOut.ConfigEntry = WslConfigEntry::VMIdleTimeout;
|
|
wslConfigSettingWriteOut.Int32Value = 1234;
|
|
|
|
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigSettingWriteOut), ERROR_SUCCESS);
|
|
|
|
// Replace the swapfile path, which is a multi-line property with a trailing comment.
|
|
// The multi-line value should be replaced with the new value and trailing comment preserved.
|
|
wslConfigSettingWriteOut.ConfigEntry = WslConfigEntry::SwapFilePath;
|
|
wslConfigSettingWriteOut.StringValue = LR"(C:\DoesNotExist\swap.vhdx)";
|
|
|
|
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigSettingWriteOut), ERROR_SUCCESS);
|
|
|
|
// Write out a new setting that doesn't exist in the original config but its section
|
|
// does. The new setting should be appended to that section. There are two cases here::
|
|
wslConfigSettingWriteOut.ConfigEntry = WslConfigEntry::HardwarePerformanceCountersEnabled;
|
|
wslConfigSettingWriteOut.BoolValue = true;
|
|
|
|
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigSettingWriteOut), ERROR_SUCCESS);
|
|
|
|
wslConfigSettingWriteOut.ConfigEntry = WslConfigEntry::AutoMemoryReclaim;
|
|
wslConfigSettingWriteOut.MemoryReclaimModeValue = MemoryReclaimConfiguration::Gradual;
|
|
|
|
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigSettingWriteOut), ERROR_SUCCESS);
|
|
|
|
std::wstring customWslConfigContentExpected{
|
|
LR"(
|
|
[wsl2] # trailing section comment
|
|
vmIdleTimeout=1234 # property trailing comment
|
|
vmIdleTimeout=20000 # property trailing comment
|
|
vmIdleTimeout=20000 # property trailing comment
|
|
mountDeviceTimeout=120\
|
|
000
|
|
kernelBootTimeout=120000
|
|
|
|
# property comment
|
|
swapfile=C:\\DoesNotExist\\swap.vhdx # multi-line property with trailing comment
|
|
telemetry=false
|
|
safeMode=false
|
|
guiApplications=true
|
|
earlyBootLogging=false
|
|
# comment 1
|
|
# comment 2
|
|
# \t \b
|
|
virtio9p=true # property trailing comment, ensure new property is appended to the section while preserving this comment
|
|
|
|
# section comment
|
|
[experimental]
|
|
autoProxy=false
|
|
autoMemoryReclaim=Gradual
|
|
|
|
[wsl2]
|
|
|
|
# end comment
|
|
)"};
|
|
|
|
std::wifstream configRead(apiWslConfigFilePath);
|
|
auto customWslConfigContentActual = std::wstring{std::istreambuf_iterator<wchar_t>(configRead), {}};
|
|
configRead.close();
|
|
VERIFY_ARE_EQUAL(customWslConfigContentExpected, customWslConfigContentActual);
|
|
}
|
|
|
|
{
|
|
// This test contains an invalid line ('babyshark') in the wsl2 section.
|
|
// The line should be preserved and no additional spacing/lines should be added.
|
|
std::wstring customWslConfigContentOut{
|
|
LR"(
|
|
[wsl2]
|
|
memory=32G
|
|
processors=12
|
|
hostAddressLoopback=false
|
|
dnsTunneling=true
|
|
defaultVhdSize=1099511627776
|
|
babyshark
|
|
localhostForwarding=true
|
|
autoProxy=false
|
|
)"};
|
|
|
|
WslConfigChange config(customWslConfigContentOut);
|
|
|
|
wslConfig = createWslConfig(apiWslConfigFilePath);
|
|
VERIFY_IS_NOT_NULL(wslConfig);
|
|
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
|
|
|
|
auto wslConfigSetting = getWslConfigSetting(wslConfig, WslConfigEntry::AutoProxyEnabled);
|
|
const auto autoProxyEnabled = false;
|
|
VERIFY_ARE_EQUAL(wslConfigSetting.BoolValue, autoProxyEnabled);
|
|
|
|
wslConfigSetting.BoolValue = !autoProxyEnabled;
|
|
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigSetting), ERROR_SUCCESS);
|
|
|
|
std::wstring customWslConfigContentExpected{
|
|
LR"(
|
|
[wsl2]
|
|
memory=32G
|
|
processors=12
|
|
hostAddressLoopback=false
|
|
dnsTunneling=true
|
|
defaultVhdSize=1099511627776
|
|
babyshark
|
|
localhostForwarding=true
|
|
)"};
|
|
|
|
std::wifstream configRead(apiWslConfigFilePath);
|
|
auto customWslConfigContentActual = std::wstring{std::istreambuf_iterator<wchar_t>(configRead), {}};
|
|
configRead.close();
|
|
VERIFY_ARE_EQUAL(customWslConfigContentActual, customWslConfigContentExpected);
|
|
}
|
|
|
|
{
|
|
// This test verifies removal of a setting from the .wslconfig when a default value for the particular setting is
|
|
// set. This gives wsl control over the default value.
|
|
std::wstring customWslConfigContentOut{
|
|
LR"(
|
|
[wsl2]
|
|
memory=32G
|
|
processors=12 # property trailing comment
|
|
hostAddressLoopback=false
|
|
dnsTunneling=true
|
|
defaultVhdSize=1099511627776
|
|
localhostForwarding=true
|
|
autoProxy=false
|
|
)"};
|
|
|
|
WslConfigChange config(customWslConfigContentOut);
|
|
|
|
wslConfig = createWslConfig(apiWslConfigFilePath);
|
|
VERIFY_IS_NOT_NULL(wslConfig);
|
|
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
|
|
|
|
wslConfigDefaults = createWslConfig(nullptr);
|
|
VERIFY_IS_NOT_NULL(wslConfigDefaults);
|
|
auto cleanupWslConfigDefaults = wil::scope_exit([&] { freeWslConfig(wslConfigDefaults); });
|
|
|
|
// This setting should be removed from the .wslconfig file.
|
|
auto wslConfigDefaultSettingMemorySize = getWslConfigSetting(wslConfigDefaults, WslConfigEntry::MemorySizeBytes);
|
|
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigDefaultSettingMemorySize), ERROR_SUCCESS);
|
|
|
|
// This setting should be removed from the .wslconfig file but trailing comment preserved.
|
|
auto wslConfigDefaultSettingProcessorCount = getWslConfigSetting(wslConfigDefaults, WslConfigEntry::ProcessorCount);
|
|
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigDefaultSettingProcessorCount), ERROR_SUCCESS);
|
|
|
|
// This setting should be preserved with an updated value in the .wslconfig file.
|
|
auto wslConfigDefaultSettingVhdSize = getWslConfigSetting(wslConfigDefaults, WslConfigEntry::VhdSizeBytes);
|
|
wslConfigDefaultSettingVhdSize.UInt64Value -= 1;
|
|
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigDefaultSettingVhdSize), ERROR_SUCCESS);
|
|
|
|
// This setting should be removed from the .wslconfig file.
|
|
auto wslConfigDefaultSettingAutoProxy = getWslConfigSetting(wslConfigDefaults, WslConfigEntry::AutoProxyEnabled);
|
|
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigDefaultSettingAutoProxy), ERROR_SUCCESS);
|
|
|
|
// This setting should not be written to the .wslconfig file.
|
|
auto wslConfigDefaultSettingGuiApplications = getWslConfigSetting(wslConfigDefaults, WslConfigEntry::GUIApplicationsEnabled);
|
|
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, wslConfigDefaultSettingGuiApplications), ERROR_SUCCESS);
|
|
|
|
std::wstring customWslConfigContentExpected{
|
|
LR"(
|
|
[wsl2]
|
|
# property trailing comment
|
|
hostAddressLoopback=false
|
|
dnsTunneling=true
|
|
defaultVhdSize=1099511627775
|
|
localhostForwarding=true
|
|
)"};
|
|
|
|
std::wifstream configRead(apiWslConfigFilePath);
|
|
auto customWslConfigContentActual = std::wstring{std::istreambuf_iterator<wchar_t>(configRead), {}};
|
|
configRead.close();
|
|
VERIFY_ARE_EQUAL(customWslConfigContentActual, customWslConfigContentExpected);
|
|
}
|
|
|
|
// Regression test for GitHub issue #12671:
|
|
// Ensure that section headers always appear BEFORE their key-value pairs.
|
|
// Bug: WSL Settings GUI was writing keys before the section header, causing "Unknown key" errors.
|
|
{
|
|
std::wstring bugScenarioConfig =
|
|
LR"([wsl2]
|
|
[experimental]
|
|
[wsl2]
|
|
)";
|
|
WslConfigChange config{bugScenarioConfig.c_str()};
|
|
|
|
wslConfig = createWslConfig(apiWslConfigFilePath);
|
|
VERIFY_IS_NOT_NULL(wslConfig);
|
|
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
|
|
|
|
// Write memory setting - this should NOT appear before the first [wsl2]
|
|
WslConfigSetting memorySetting{};
|
|
memorySetting.ConfigEntry = WslConfigEntry::MemorySizeBytes;
|
|
memorySetting.UInt64Value = 17825792000ULL; // Value from bug report
|
|
|
|
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, memorySetting), ERROR_SUCCESS);
|
|
|
|
// Read and verify
|
|
std::wifstream configRead(apiWslConfigFilePath);
|
|
std::wstring fileContent{std::istreambuf_iterator<wchar_t>(configRead), {}};
|
|
configRead.close();
|
|
|
|
// Find FIRST occurrence of [wsl2] and memory=
|
|
auto firstWsl2Pos = fileContent.find(L"[wsl2]");
|
|
auto memoryPos = fileContent.find(L"memory=");
|
|
|
|
VERIFY_ARE_NOT_EQUAL(firstWsl2Pos, std::wstring::npos);
|
|
VERIFY_ARE_NOT_EQUAL(memoryPos, std::wstring::npos);
|
|
|
|
// The critical assertion: memory= must NOT appear before [wsl2]
|
|
VERIFY_IS_TRUE(firstWsl2Pos < memoryPos);
|
|
|
|
// Additional check: memory should appear after the first [wsl2], not after line 1
|
|
auto firstLineEnd = fileContent.find(L'\n');
|
|
VERIFY_IS_TRUE(memoryPos > firstLineEnd);
|
|
}
|
|
|
|
// Test: Empty file - should create proper [wsl2] section structure
|
|
{
|
|
std::wofstream emptyConfig(apiWslConfigFilePath, std::ios::trunc);
|
|
emptyConfig.close();
|
|
|
|
wslConfig = createWslConfig(apiWslConfigFilePath);
|
|
VERIFY_IS_NOT_NULL(wslConfig);
|
|
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
|
|
|
|
WslConfigSetting memorySetting{};
|
|
memorySetting.ConfigEntry = WslConfigEntry::MemorySizeBytes;
|
|
memorySetting.UInt64Value = 4294967296ULL; // 4GB
|
|
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, memorySetting), ERROR_SUCCESS);
|
|
|
|
std::wifstream configRead(apiWslConfigFilePath);
|
|
std::wstring fileContent{std::istreambuf_iterator<wchar_t>(configRead), {}};
|
|
configRead.close();
|
|
|
|
// Should create [wsl2] section and add memory key
|
|
VERIFY_IS_TRUE(fileContent.find(L"[wsl2]") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(fileContent.find(L"memory=") != std::wstring::npos);
|
|
// Verify [wsl2] comes before memory=
|
|
VERIFY_IS_TRUE(fileContent.find(L"[wsl2]") < fileContent.find(L"memory="));
|
|
}
|
|
|
|
// Test: Multiple same-section instances - should update first occurrence
|
|
{
|
|
std::wofstream configFile(apiWslConfigFilePath, std::ios::trunc);
|
|
configFile << L"[wsl2]\n";
|
|
configFile << L"processors=4\n";
|
|
configFile << L"\n";
|
|
configFile << L"[experimental]\n";
|
|
configFile << L"autoProxy=true\n";
|
|
configFile << L"\n";
|
|
configFile << L"[wsl2]\n"; // Second [wsl2] section
|
|
configFile << L"swap=0\n";
|
|
configFile.close();
|
|
|
|
wslConfig = createWslConfig(apiWslConfigFilePath);
|
|
VERIFY_IS_NOT_NULL(wslConfig);
|
|
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
|
|
|
|
WslConfigSetting memorySetting{};
|
|
memorySetting.ConfigEntry = WslConfigEntry::MemorySizeBytes;
|
|
memorySetting.UInt64Value = 8589934592ULL; // 8GB
|
|
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, memorySetting), ERROR_SUCCESS);
|
|
|
|
std::wifstream configRead(apiWslConfigFilePath);
|
|
std::wstring fileContent{std::istreambuf_iterator<wchar_t>(configRead), {}};
|
|
configRead.close();
|
|
|
|
// Find first and second [wsl2]
|
|
auto firstWsl2 = fileContent.find(L"[wsl2]");
|
|
auto secondWsl2 = fileContent.find(L"[wsl2]", firstWsl2 + 1);
|
|
auto memoryPos = fileContent.find(L"memory=");
|
|
|
|
VERIFY_ARE_NOT_EQUAL(firstWsl2, std::wstring::npos);
|
|
VERIFY_ARE_NOT_EQUAL(secondWsl2, std::wstring::npos);
|
|
VERIFY_ARE_NOT_EQUAL(memoryPos, std::wstring::npos);
|
|
|
|
// Memory should be added to FIRST [wsl2] section, not second
|
|
VERIFY_IS_TRUE(memoryPos > firstWsl2);
|
|
VERIFY_IS_TRUE(memoryPos < secondWsl2);
|
|
}
|
|
|
|
// Test: EOF without trailing newline
|
|
{
|
|
std::wofstream configFile(apiWslConfigFilePath, std::ios::trunc);
|
|
configFile << L"[wsl2]\n";
|
|
configFile << L"processors=2"; // No trailing newline
|
|
configFile.close();
|
|
|
|
wslConfig = createWslConfig(apiWslConfigFilePath);
|
|
VERIFY_IS_NOT_NULL(wslConfig);
|
|
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
|
|
|
|
WslConfigSetting memorySetting{};
|
|
memorySetting.ConfigEntry = WslConfigEntry::MemorySizeBytes;
|
|
memorySetting.UInt64Value = 3221225472ULL; // 3GB
|
|
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, memorySetting), ERROR_SUCCESS);
|
|
|
|
std::wifstream configRead(apiWslConfigFilePath);
|
|
std::wstring fileContent{std::istreambuf_iterator<wchar_t>(configRead), {}};
|
|
configRead.close();
|
|
|
|
// Should properly append memory key even without trailing newline on last line
|
|
VERIFY_IS_TRUE(fileContent.find(L"processors=2") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(fileContent.find(L"memory=") != std::wstring::npos);
|
|
|
|
// Verify both keys are in the same section
|
|
auto wsl2Pos = fileContent.find(L"[wsl2]");
|
|
auto processorsPos = fileContent.find(L"processors=2");
|
|
auto memoryPos = fileContent.find(L"memory=");
|
|
VERIFY_IS_TRUE(wsl2Pos < processorsPos);
|
|
VERIFY_IS_TRUE(wsl2Pos < memoryPos);
|
|
|
|
// Memory should come after processors in the same section
|
|
VERIFY_IS_TRUE(processorsPos < memoryPos);
|
|
}
|
|
|
|
// Test: Empty section followed by another section
|
|
{
|
|
std::wofstream configFile(apiWslConfigFilePath, std::ios::trunc);
|
|
configFile << L"[wsl2]\n";
|
|
configFile << L"[experimental]\n";
|
|
configFile << L"autoProxy=true\n";
|
|
configFile.close();
|
|
|
|
wslConfig = createWslConfig(apiWslConfigFilePath);
|
|
VERIFY_IS_NOT_NULL(wslConfig);
|
|
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
|
|
|
|
WslConfigSetting memorySetting{};
|
|
memorySetting.ConfigEntry = WslConfigEntry::MemorySizeBytes;
|
|
memorySetting.UInt64Value = 5368709120ULL; // 5GB
|
|
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, memorySetting), ERROR_SUCCESS);
|
|
|
|
std::wifstream configRead(apiWslConfigFilePath);
|
|
std::wstring fileContent{std::istreambuf_iterator<wchar_t>(configRead), {}};
|
|
configRead.close();
|
|
|
|
// Should insert memory into empty [wsl2] section before [experimental]
|
|
auto wsl2Pos = fileContent.find(L"[wsl2]");
|
|
auto memoryPos = fileContent.find(L"memory=");
|
|
auto experimentalPos = fileContent.find(L"[experimental]");
|
|
|
|
VERIFY_ARE_NOT_EQUAL(wsl2Pos, std::wstring::npos);
|
|
VERIFY_ARE_NOT_EQUAL(memoryPos, std::wstring::npos);
|
|
VERIFY_ARE_NOT_EQUAL(experimentalPos, std::wstring::npos);
|
|
|
|
// Order should be: [wsl2], memory=, [experimental]
|
|
VERIFY_IS_TRUE(wsl2Pos < memoryPos);
|
|
VERIFY_IS_TRUE(memoryPos < experimentalPos);
|
|
}
|
|
|
|
// Test: Section header at EOF with no content
|
|
{
|
|
std::wofstream configFile(apiWslConfigFilePath, std::ios::trunc);
|
|
configFile << L"[wsl2]"; // Section at EOF, no newline, no content
|
|
configFile.close();
|
|
|
|
wslConfig = createWslConfig(apiWslConfigFilePath);
|
|
VERIFY_IS_NOT_NULL(wslConfig);
|
|
auto cleanupWslConfig = wil::scope_exit([&] { freeWslConfig(wslConfig); });
|
|
|
|
WslConfigSetting memorySetting{};
|
|
memorySetting.ConfigEntry = WslConfigEntry::MemorySizeBytes;
|
|
memorySetting.UInt64Value = 6442450944ULL; // 6GB
|
|
VERIFY_ARE_EQUAL(setWslConfigSetting(wslConfig, memorySetting), ERROR_SUCCESS);
|
|
|
|
std::wifstream configRead(apiWslConfigFilePath);
|
|
std::wstring fileContent{std::istreambuf_iterator<wchar_t>(configRead), {}};
|
|
configRead.close();
|
|
|
|
// Should properly add key to section at EOF
|
|
VERIFY_IS_TRUE(fileContent.find(L"[wsl2]") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(fileContent.find(L"memory=") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(fileContent.find(L"[wsl2]") < fileContent.find(L"memory="));
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(LaunchWslSettingsFromProtocol)
|
|
{
|
|
WSL_SETTINGS_TEST();
|
|
|
|
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
|
|
|
|
SHELLEXECUTEINFOW execInfo{};
|
|
execInfo.cbSize = sizeof(execInfo);
|
|
execInfo.fMask = SEE_MASK_CLASSNAME | SEE_MASK_FLAG_NO_UI | SEE_MASK_NOCLOSEPROCESS;
|
|
execInfo.lpClass = L"wsl-settings";
|
|
execInfo.lpFile = L"wsl-settings://";
|
|
execInfo.nShow = SW_HIDE;
|
|
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(ShellExecuteExW(&execInfo));
|
|
const wil::unique_process_handle process{execInfo.hProcess};
|
|
VERIFY_IS_NOT_NULL(process.get());
|
|
|
|
auto killProcess = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&process]() {
|
|
if (process)
|
|
{
|
|
LOG_IF_WIN32_BOOL_FALSE(TerminateProcess(process.get(), 0));
|
|
}
|
|
});
|
|
|
|
const auto moduleFileName = wil::GetModuleFileNameExW<std::wstring>(process.get(), nullptr);
|
|
const auto findExeName = moduleFileName.find(L"wslsettings.exe");
|
|
VERIFY_ARE_NOT_EQUAL(findExeName, std::wstring::npos);
|
|
}
|
|
|
|
TEST_METHOD(ManageDefaultUid)
|
|
{
|
|
const auto distroKey = OpenDistributionKey(LXSS_DISTRO_NAME_TEST_L);
|
|
|
|
auto assertDefaultUid = [&](ULONG ExpectedUid) {
|
|
const auto uid = wsl::windows::common::registry::ReadDword(distroKey.get(), nullptr, L"DefaultUid", 0);
|
|
|
|
VERIFY_ARE_EQUAL(ExpectedUid, uid);
|
|
|
|
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"id -u");
|
|
while (!out.empty() && (out.back() == '\n' || out.back() == '\r'))
|
|
{
|
|
out.pop_back();
|
|
}
|
|
|
|
VERIFY_ARE_EQUAL(out, std::to_wstring(ExpectedUid));
|
|
};
|
|
|
|
assertDefaultUid(0);
|
|
|
|
auto validateUidChange =
|
|
[&](const std::wstring& User, ULONG expectedDefaultUid, LPCWSTR ExpectedOutput, const std::wstring& ExpectedError, int ExpectedExitCode) {
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(
|
|
std::format(L"--manage {} --set-default-user {}", LXSS_DISTRO_NAME_TEST_L, User), ExpectedExitCode);
|
|
|
|
VERIFY_ARE_EQUAL(out, ExpectedOutput);
|
|
VERIFY_ARE_EQUAL(err, ExpectedError);
|
|
|
|
assertDefaultUid(expectedDefaultUid);
|
|
};
|
|
|
|
validateUidChange(L"root", 0, L"The operation completed successfully. \r\n", L"", 0);
|
|
|
|
constexpr auto TestUser = L"testuser";
|
|
|
|
auto cleanup = wil::scope_exit_log(
|
|
WI_DIAGNOSTICS_INFO, [TestUser]() { LxsstuLaunchWsl(std::format(L"-u root userdel -f {}", TestUser)); });
|
|
|
|
ULONG Uid{};
|
|
ULONG Gid{};
|
|
CreateUser(TestUser, &Uid, &Gid);
|
|
VERIFY_ARE_NOT_EQUAL(Uid, 0);
|
|
|
|
validateUidChange(L"testuser", Uid, L"The operation completed successfully. \r\n", L"", 0);
|
|
validateUidChange(L"root", 0, L"The operation completed successfully. \r\n", L"", 0);
|
|
|
|
const std::wstring invalidUser = L"Nonexistent";
|
|
validateUidChange(invalidUser, 0, L"", L"id: \u2018" + invalidUser + L"\u2019: no such user\n", 1);
|
|
|
|
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"--manage nonexistent --set-default-user root", -1);
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
out, L"There is no distribution with the supplied name.\r\nError code: Wsl/Service/WSL_E_DISTRO_NOT_FOUND\r\n");
|
|
}
|
|
|
|
TEST_METHOD(PostDistroRegistrationSettingsOOBE)
|
|
{
|
|
WSL_SETTINGS_TEST();
|
|
|
|
wsl::windows::common::SvcComm service;
|
|
const auto distros = service.EnumerateDistributions();
|
|
if (distros.size() != 1)
|
|
{
|
|
LogSkipped("Test distro as the only distro is required to run this test.");
|
|
return;
|
|
}
|
|
|
|
const auto lxssKey = wsl::windows::common::registry::OpenLxssUserKey();
|
|
// Test setup should set OOBEComplete
|
|
VERIFY_ARE_EQUAL(bool(wsl::windows::common::registry::ReadDword(lxssKey.get(), nullptr, LXSS_OOBE_COMPLETE_NAME, false)), true);
|
|
|
|
// Delete the OOBEComplete reg value to simulate OOBE not being complete
|
|
wsl::windows::common::registry::DeleteValue(lxssKey.get(), LXSS_OOBE_COMPLETE_NAME);
|
|
|
|
// Restore the OOBEComplete reg value in case of failure
|
|
auto restoreOOBEComplete = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
wsl::windows::common::registry::WriteDword(lxssKey.get(), nullptr, LXSS_OOBE_COMPLETE_NAME, true);
|
|
});
|
|
|
|
constexpr auto wslSettingsWindowName = L"Welcome to Windows Subsystem for Linux";
|
|
VERIFY_ARE_EQUAL(FindWindowEx(nullptr, nullptr, nullptr, wslSettingsWindowName), nullptr);
|
|
|
|
auto testDistro = distros.front();
|
|
VERIFY_IS_TRUE(wsl::shared::string::IsEqual(testDistro.DistroName, LXSS_DISTRO_NAME_TEST_L, false));
|
|
// Get the original BasePath in order to restore the test distro as before.
|
|
auto guidStringWithBraces = wsl::shared::string::GuidToString<wchar_t>(testDistro.DistroGuid);
|
|
auto testDistroBasePath =
|
|
wsl::windows::common::registry::ReadString(lxssKey.get(), guidStringWithBraces.c_str(), L"BasePath", L"");
|
|
VERIFY_ARE_NOT_EQUAL(testDistroBasePath, L"");
|
|
|
|
if (LxsstuVmMode())
|
|
{
|
|
const auto testDistroVhdPath = std::filesystem::path(testDistroBasePath) / LXSS_VM_MODE_VHD_NAME;
|
|
VERIFY_IS_TRUE(std::filesystem::exists(testDistroVhdPath));
|
|
const auto testDistroVhdPathExported = std::filesystem::path(testDistroBasePath) / L"exported.vhdx";
|
|
|
|
WslShutdown();
|
|
VERIFY_ARE_EQUAL(
|
|
LxsstuLaunchWsl(std::format(L"--export {} \"{}\" --vhd", testDistro.DistroName, testDistroVhdPathExported.c_str())), 0u);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--unregister {}", testDistro.DistroName)), 0u);
|
|
VERIFY_IS_FALSE(std::filesystem::exists(testDistroVhdPath));
|
|
VERIFY_IS_TRUE(service.EnumerateDistributions().empty());
|
|
|
|
std::error_code ec{};
|
|
std::filesystem::rename(testDistroVhdPathExported, testDistroVhdPath, ec);
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
LxsstuLaunchWsl(std::format(L"--import-in-place {} \"{}\"", testDistro.DistroName, testDistroVhdPath.c_str())), 0L);
|
|
}
|
|
else
|
|
{
|
|
const auto testDistroRootfsPath = std::filesystem::path(testDistroBasePath) / LXSS_ROOTFS_DIRECTORY;
|
|
VERIFY_IS_TRUE(std::filesystem::exists(testDistroRootfsPath));
|
|
const auto testDistroExported = std::filesystem::path(testDistroBasePath) / L"exported.tar";
|
|
auto deleteTar = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { DeleteFile(testDistroExported.c_str()); });
|
|
|
|
WslShutdown();
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--export {} \"{}\"", testDistro.DistroName, testDistroExported.c_str())), 0u);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--unregister {}", testDistro.DistroName)), 0u);
|
|
VERIFY_IS_FALSE(std::filesystem::exists(testDistroRootfsPath));
|
|
VERIFY_IS_TRUE(service.EnumerateDistributions().empty());
|
|
VERIFY_ARE_EQUAL(
|
|
LxsstuLaunchWsl(std::format(
|
|
L"--import {} \"{}\" \"{}\" --version 1", testDistro.DistroName, testDistroBasePath, testDistroExported.c_str())),
|
|
0L);
|
|
}
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--set-default {}", testDistro.DistroName)), 0);
|
|
|
|
VERIFY_ARE_EQUAL(service.EnumerateDistributions().size(), 1);
|
|
HWND wslSettingsWindow{};
|
|
const auto findWslSettingsWindowAttempts = 60;
|
|
for (auto attempt = 0; attempt < findWslSettingsWindowAttempts; ++attempt)
|
|
{
|
|
wslSettingsWindow = FindWindowEx(nullptr, nullptr, nullptr, wslSettingsWindowName);
|
|
if (wslSettingsWindow)
|
|
{
|
|
break;
|
|
}
|
|
|
|
Sleep(500);
|
|
}
|
|
|
|
VERIFY_ARE_NOT_EQUAL(wslSettingsWindow, nullptr);
|
|
SendMessage(wslSettingsWindow, WM_CLOSE, 0, 0);
|
|
VERIFY_ARE_EQUAL(bool(wsl::windows::common::registry::ReadDword(lxssKey.get(), nullptr, LXSS_OOBE_COMPLETE_NAME, false)), true);
|
|
}
|
|
|
|
TEST_METHOD(VersionFlavorParsing)
|
|
{
|
|
DWORD currentVersion = LxsstuVmMode() ? 2 : 1;
|
|
DWORD convertVersion = LxsstuVmMode() ? 1 : 2;
|
|
|
|
const auto lxssKey = wsl::windows::common::registry::OpenLxssUserKey();
|
|
|
|
auto validateFlavorVersion = [&](LPCWSTR Distro, LPCWSTR ExpectedFlavor, LPCWSTR ExpectedVersion) {
|
|
const auto testDistroId = GetDistributionId(Distro);
|
|
VERIFY_IS_TRUE(testDistroId.has_value());
|
|
|
|
const auto distroId = wsl::shared::string::GuidToString<wchar_t>(testDistroId.value());
|
|
|
|
TerminateDistribution(Distro);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"-d {} cat /etc/os-release || true", Distro).c_str()), 0L);
|
|
|
|
const auto flavor = wsl::windows::common::registry::ReadString(lxssKey.get(), distroId.c_str(), L"Flavor", L"");
|
|
const auto version = wsl::windows::common::registry::ReadString(lxssKey.get(), distroId.c_str(), L"OsVersion", L"");
|
|
|
|
VERIFY_ARE_EQUAL(ExpectedFlavor, flavor);
|
|
VERIFY_ARE_EQUAL(ExpectedVersion, version);
|
|
};
|
|
|
|
validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"debian", L"13");
|
|
|
|
constexpr auto testTar = L"exported-distro.tar";
|
|
constexpr auto tmpDistroName = L"tmpdistro";
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [tmpDistroName]() {
|
|
DeleteFile(testTar);
|
|
LxsstuLaunchWsl(std::format(L"--unregister {}", tmpDistroName));
|
|
});
|
|
|
|
DistroFileChange osRelease(L"/etc/os-release");
|
|
|
|
{
|
|
osRelease.SetContent(
|
|
LR"(
|
|
ID=Distro
|
|
VERSION_ID=Version
|
|
)");
|
|
|
|
validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"Distro", L"Version");
|
|
}
|
|
|
|
{
|
|
osRelease.SetContent(
|
|
LR"(
|
|
DISTRO_I=Wrong
|
|
ID="DistroWithQuotes"
|
|
VERSION_ID="VersionWithQuotes"
|
|
Something else
|
|
)");
|
|
|
|
validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"DistroWithQuotes", L"VersionWithQuotes");
|
|
}
|
|
|
|
{
|
|
osRelease.SetContent(
|
|
LR"(
|
|
ID="InvalidFormat!"
|
|
VERSION_ID="ValidFormat"
|
|
)");
|
|
|
|
validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"DistroWithQuotes", L"ValidFormat");
|
|
}
|
|
|
|
{
|
|
osRelease.SetContent(
|
|
LR"(
|
|
ID="Distro-_.,"
|
|
VERSION_ID="ValidFormat"
|
|
)");
|
|
|
|
validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"Distro-_.,", L"ValidFormat");
|
|
}
|
|
|
|
{
|
|
osRelease.SetContent(
|
|
LR"(
|
|
ID="Invalid|Format"
|
|
VERSION_ID="Invalid|Format"
|
|
)");
|
|
|
|
validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"Distro-_.,", L"ValidFormat");
|
|
}
|
|
|
|
{
|
|
osRelease.Delete(); // Nothing should happen if the file is deleted, but the distro should still work.
|
|
validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"Distro-_.,", L"ValidFormat");
|
|
}
|
|
|
|
// Validate that importing a distro without os-release works.
|
|
{
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--export {} {}", LXSS_DISTRO_NAME_TEST_L, testTar).c_str()), 0L);
|
|
VERIFY_ARE_EQUAL(
|
|
LxsstuLaunchWsl(std::format(L"--import {} . {} --version {}", tmpDistroName, testTar, currentVersion).c_str()), 0L);
|
|
|
|
validateFlavorVersion(tmpDistroName, L"", L"");
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"-d {} echo -e 'VERSION_ID=v' > /etc/os-release", tmpDistroName).c_str()), 0L);
|
|
validateFlavorVersion(tmpDistroName, L"", L"v");
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--unregister {}", tmpDistroName).c_str()), 0L);
|
|
}
|
|
|
|
// Validate that importing and then converting also behaves correctly when there's no os-release
|
|
{
|
|
VERIFY_ARE_EQUAL(
|
|
LxsstuLaunchWsl(std::format(L"--import {} . {} --version {}", tmpDistroName, testTar, convertVersion).c_str()), 0L);
|
|
validateFlavorVersion(tmpDistroName, L"", L"");
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--set-version {} {}", tmpDistroName, currentVersion).c_str()), 0L);
|
|
|
|
validateFlavorVersion(tmpDistroName, L"", L"");
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"-d {} echo -e 'VERSION_ID=v2' > /etc/os-release", tmpDistroName).c_str()), 0L);
|
|
validateFlavorVersion(tmpDistroName, L"", L"v2");
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--unregister {}", tmpDistroName).c_str()), 0L);
|
|
}
|
|
|
|
// Verify that importing a distribution with an os-release as then converting works as well
|
|
VERIFY_ARE_EQUAL(
|
|
LxsstuLaunchWsl(std::format(L"--import {} . {} --version {}", tmpDistroName, g_testDistroPath, convertVersion).c_str()), 0L);
|
|
validateFlavorVersion(tmpDistroName, L"debian", L"13");
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--set-version {} {}", tmpDistroName, currentVersion).c_str()), 0L);
|
|
validateFlavorVersion(tmpDistroName, L"debian", L"13");
|
|
}
|
|
|
|
TEST_METHOD(DistributionId)
|
|
{
|
|
using namespace wsl::windows::common::string;
|
|
const auto testDistroId = GetDistributionId(LXSS_DISTRO_NAME_TEST_L);
|
|
VERIFY_IS_TRUE(testDistroId.has_value());
|
|
|
|
auto validateOutput = [](const std::wstring& Cmd, LPCWSTR ExpectedOutput, int ExitCode = 0) {
|
|
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(Cmd, ExitCode);
|
|
|
|
VERIFY_ARE_EQUAL(out, ExpectedOutput);
|
|
};
|
|
|
|
validateOutput(
|
|
std::format(
|
|
L"--distribution-id {} echo -n OK",
|
|
wsl::shared::string::GuidToString<wchar_t>(testDistroId.value(), wsl::shared::string::GuidToStringFlags::None)),
|
|
L"OK");
|
|
|
|
validateOutput(
|
|
std::format(
|
|
L"--distribution-id {} echo -n OK",
|
|
wsl::shared::string::GuidToString<wchar_t>(testDistroId.value(), wsl::shared::string::GuidToStringFlags::AddBraces)),
|
|
L"OK");
|
|
|
|
validateOutput(
|
|
std::format(
|
|
L"--distribution-id {} echo -n OK",
|
|
wsl::shared::string::GuidToString<wchar_t>(testDistroId.value(), wsl::shared::string::GuidToStringFlags::Uppercase)),
|
|
L"OK");
|
|
|
|
validateOutput(L"--distribution-id InvalidGuid", L"The parameter is incorrect. \r\nError code: Wsl/E_INVALIDARG\r\n", -1);
|
|
validateOutput(
|
|
L"--distribution-id {C13B2B63-F9D5-4840-8105-F6ABECCF46CA}",
|
|
L"There is no distribution with the supplied name.\r\nError code: "
|
|
L"Wsl/Service/CreateInstance/ReadDistroConfig/WSL_E_DISTRO_NOT_FOUND\r\n",
|
|
-1);
|
|
}
|
|
|
|
TEST_METHOD(ModernOOBE)
|
|
{
|
|
const auto lxssKey = wsl::windows::common::registry::OpenLxssUserKey();
|
|
const auto testDistroId = GetDistributionId(LXSS_DISTRO_NAME_TEST_L);
|
|
VERIFY_IS_TRUE(testDistroId.has_value());
|
|
const auto testDistroIdString = wsl::shared::string::GuidToString<wchar_t>(testDistroId.value());
|
|
|
|
DistroFileChange distributionconf(L"/etc/wsl-distribution.conf", false);
|
|
distributionconf.SetContent(L"[oobe]\ncommand = /bin/bash -c 'echo OOBE'\n");
|
|
|
|
RegistryKeyChange<DWORD> runOOBE(lxssKey.get(), testDistroIdString.c_str(), L"RunOOBE", 1);
|
|
const RegistryKeyChange<DWORD> defaultUid(lxssKey.get(), testDistroIdString.c_str(), L"DefaultUid", 0);
|
|
|
|
auto validateOutput = [](LPCWSTR Cmd, LPCWSTR ExpectedOutput, LPCWSTR ExpectedWarnings = L"", DWORD ExpectedExitCode = 0) {
|
|
auto [read, write] = CreateSubprocessPipe(true, false);
|
|
write.reset();
|
|
|
|
wsl::windows::common::SubProcess process(nullptr, LxssGenerateWslCommandLine(Cmd).c_str());
|
|
process.SetStdHandles(read.get(), nullptr, nullptr);
|
|
|
|
const auto output = process.RunAndCaptureOutput();
|
|
|
|
VERIFY_ARE_EQUAL(ExpectedExitCode, output.ExitCode);
|
|
|
|
VERIFY_ARE_EQUAL(ExpectedOutput, output.Stdout);
|
|
VERIFY_ARE_EQUAL(ExpectedWarnings, output.Stderr);
|
|
};
|
|
|
|
{
|
|
TerminateDistribution();
|
|
|
|
// Non-interactive commands shouldn't trigger OOBE
|
|
validateOutput(L"echo no oobe", L"no oobe\n");
|
|
VERIFY_ARE_EQUAL(runOOBE.Get(), 1);
|
|
|
|
// Interactive shell should trigger OOBE
|
|
validateOutput(nullptr, L"OOBE\n");
|
|
VERIFY_ARE_EQUAL(runOOBE.Get(), 0);
|
|
|
|
// OOBE should only trigger once
|
|
validateOutput(L"", L"");
|
|
}
|
|
|
|
{
|
|
runOOBE.Set(1);
|
|
distributionconf.SetContent(L"[oobe]\ncommand = /bin/bash -c 'echo failed OOBE && exit 1'\n");
|
|
|
|
TerminateDistribution();
|
|
|
|
constexpr auto expectedStdErr = L"OOBE command \"/bin/bash -c 'echo failed OOBE && exit 1'\" failed, exiting\n";
|
|
|
|
validateOutput(nullptr, L"failed OOBE\n", expectedStdErr, 1);
|
|
VERIFY_ARE_EQUAL(runOOBE.Get(), 1);
|
|
|
|
// Failed OOBE command should be retried
|
|
TerminateDistribution();
|
|
validateOutput(nullptr, L"failed OOBE\n", expectedStdErr, 1);
|
|
VERIFY_ARE_EQUAL(runOOBE.Get(), 1);
|
|
}
|
|
|
|
{
|
|
runOOBE.Set(1);
|
|
distributionconf.SetContent(
|
|
L"[oobe]\ncommand = /bin/bash -c 'echo OOBE && useradd -u 1010 -m -s /bin/bash user'\n defaultUid = 1010\n");
|
|
|
|
TerminateDistribution();
|
|
|
|
validateOutput(nullptr, L"OOBE\n");
|
|
VERIFY_ARE_EQUAL(runOOBE.Get(), 0);
|
|
|
|
// Validate that DefaultUid was set
|
|
validateOutput(L"id -u", L"1010\n");
|
|
VERIFY_ARE_EQUAL(defaultUid.Get(), 1010);
|
|
}
|
|
|
|
// Verify that the default UID isn't changed if it's not present in wsl-distribution.conf.
|
|
{
|
|
runOOBE.Set(1);
|
|
|
|
distributionconf.SetContent(L"[oobe]\ncommand = /bin/bash -c 'echo OOBE'");
|
|
TerminateDistribution();
|
|
|
|
validateOutput(nullptr, L"OOBE\n");
|
|
VERIFY_ARE_EQUAL(defaultUid.Get(), 1010);
|
|
}
|
|
|
|
// Verify that OOBE doesn't run if a distribution is installed via wsl --import
|
|
{
|
|
constexpr auto testDir = L"test-oobe-import";
|
|
constexpr auto testDistroName = L"test-oobe-import";
|
|
|
|
std::filesystem::create_directory(testDir);
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, testDistroName]() {
|
|
LxsstuLaunchWsl(std::format(L"--unregister {}", testDistroName));
|
|
std::error_code error;
|
|
std::filesystem::remove_all(testDir, error);
|
|
});
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--export {} {}/exported.tar", LXSS_DISTRO_NAME_TEST_L, testDir)), 0L);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} {} {}/exported.tar", testDistroName, testDir, testDistroName)), 0L);
|
|
|
|
const auto distroKey = OpenDistributionKey(testDistroName);
|
|
|
|
VERIFY_ARE_EQUAL(wsl::windows::common::registry::ReadDword(distroKey.get(), nullptr, L"RunOOBE", 1), 0);
|
|
validateOutput(nullptr, L"");
|
|
}
|
|
|
|
// Make sure the defaultUid is reset for next test case.
|
|
TerminateDistribution();
|
|
}
|
|
|
|
static void ValidateDistributionStarts(LPCWSTR Name)
|
|
{
|
|
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo -n OK", Name));
|
|
VERIFY_ARE_EQUAL(out, L"OK");
|
|
}
|
|
|
|
TEST_METHOD(InstallWithBrokenDefault)
|
|
{
|
|
// This test case validates that a broken 'DefaultDistribution' value doesn't prevent installing new distributions.
|
|
|
|
// Create a broken default
|
|
RegistryKeyChange defaultDistro(
|
|
HKEY_CURRENT_USER,
|
|
L"Software\\Microsoft\\Windows\\CurrentVersion\\Lxss",
|
|
L"DefaultDistribution",
|
|
std::wstring{L"{1DB260CB-912D-432A-B898-518DFD0F374E}"});
|
|
|
|
// Validate that installing a new distribution succeeds.
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { LxsstuLaunchWsl(L"--unregister test_new_default"); });
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
LxsstuLaunchWsl(std::format(L"--install --from-file \"{}\" --no-launch --name test_new_default", g_testDistroPath)), 0L);
|
|
|
|
auto [out, error] = LxsstuLaunchWslAndCaptureOutput(L"-d test_new_default echo OK");
|
|
VERIFY_ARE_EQUAL(out, L"OK\n");
|
|
VERIFY_ARE_EQUAL(error, L"");
|
|
|
|
// Verify that the default distribution is updated
|
|
const auto key = wsl::windows::common::registry::OpenLxssUserKey();
|
|
|
|
const auto defaultValue = wsl::windows::common::registry::ReadString(key.get(), nullptr, L"DefaultDistribution");
|
|
|
|
VERIFY_ARE_EQUAL(GetDistributionId(L"test_new_default").value_or(GUID_NULL), wsl::shared::string::ToGuid(defaultValue));
|
|
}
|
|
|
|
TEST_METHOD(ModernInstall)
|
|
{
|
|
using namespace wsl::windows::common::wslutil;
|
|
using namespace wsl::windows::common::string;
|
|
constexpr auto IconPath = L"test-icon.ico";
|
|
|
|
auto CreateTarFromManifest = [](LPCWSTR Manifest, LPCWSTR TarName) {
|
|
DistroFileChange distributionconf(L"/etc/wsl-distribution.conf", false);
|
|
distributionconf.SetContent(Manifest);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--export test_distro {}", TarName)), 0L);
|
|
};
|
|
|
|
auto InstallFromTar =
|
|
[](LPCWSTR TarName, LPCWSTR ExtraArgs = L"", int ExpectedExitCode = 0, LPCWSTR ExpectedOutput = nullptr, LPCWSTR ExpectedWarnings = nullptr) {
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(
|
|
std::format(L"--install --no-launch --from-file {} {}", TarName, ExtraArgs), ExpectedExitCode);
|
|
|
|
if (ExpectedOutput != nullptr)
|
|
{
|
|
VERIFY_ARE_EQUAL(ExpectedOutput, out);
|
|
}
|
|
|
|
if (ExpectedWarnings != nullptr)
|
|
{
|
|
VERIFY_ARE_EQUAL(ExpectedWarnings, err);
|
|
}
|
|
};
|
|
|
|
auto installLocation = wsl::windows::common::wslutil::GetMsiPackagePath();
|
|
VERIFY_IS_TRUE(installLocation.has_value());
|
|
|
|
auto wslExePath = installLocation.value() + L"wsl.exe";
|
|
|
|
wil::unique_hmodule wslExe{LoadLibrary(wslExePath.c_str())};
|
|
VERIFY_IS_TRUE(!!wslExe);
|
|
|
|
auto resource = FindResource(wslExe.get(), MAKEINTRESOURCE(1), RT_ICON);
|
|
VERIFY_IS_TRUE(resource != nullptr);
|
|
|
|
auto loadedResource = LoadResource(wslExe.get(), resource);
|
|
const void* iconAddress = LockResource(loadedResource);
|
|
|
|
wil::unique_handle icon{CreateFile(IconPath, GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_FLAG_DELETE_ON_CLOSE, nullptr)};
|
|
VERIFY_IS_TRUE(!!icon);
|
|
|
|
DWORD bytes{};
|
|
VERIFY_IS_TRUE(WriteFile(icon.get(), iconAddress, SizeofResource(wslExe.get(), resource), &bytes, nullptr));
|
|
LogInfo("Created icon %ls (%lu bytes)", IconPath, bytes);
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"cp '{}' /icon.ico", IconPath)), 0L);
|
|
|
|
// Distribution with default name and icon
|
|
{
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
|
|
LxsstuLaunchWsl(L"--unregister test-default-name");
|
|
DeleteFile(L"distro-default-name-icon.tar");
|
|
});
|
|
|
|
CreateTarFromManifest(
|
|
L"[shortcut]\nicon = /icon.ico\n[oobe]\ndefaultName = test-default-name", L"distro-default-name-icon.tar");
|
|
|
|
//
|
|
// Validate that the distribution icon path is also correct when installing via wsl --import.
|
|
//
|
|
|
|
{
|
|
constexpr auto distroName = L"TestCustomLocation";
|
|
|
|
auto currentDirectory = std::filesystem::absolute(std::filesystem::current_path()).wstring();
|
|
for (const auto& location : {currentDirectory, std::wstring(L".")})
|
|
{
|
|
auto cleanup = wil::scope_exit_log(
|
|
WI_DIAGNOSTICS_INFO, [&]() { LxsstuLaunchWsl(std::format(L"--unregister {}", distroName)); });
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
LxsstuLaunchWsl(
|
|
std::format(L"--import {} \"{}\" {}", distroName, location, "distro-default-name-icon.tar")),
|
|
0L);
|
|
|
|
auto [json, profile_path] = ValidateDistributionTerminalProfile(distroName, false);
|
|
VERIFY_ARE_EQUAL(
|
|
json["profiles"][1]["icon"].get<std::string>(), (std::filesystem::absolute(".") / "shortcut.ico").string());
|
|
}
|
|
}
|
|
|
|
InstallFromTar(L"distro-default-name-icon.tar");
|
|
ValidateDistributionStarts(L"test-default-name");
|
|
|
|
// Validate that the distribution was installed under the right name
|
|
auto distroKey = OpenDistributionKey(L"test-default-name");
|
|
VERIFY_IS_TRUE(!!distroKey);
|
|
|
|
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
|
|
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
|
|
|
|
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
|
|
|
|
ValidateDistributionShortcut(L"test-default-name", icon.get());
|
|
auto [json, profile_path] = ValidateDistributionTerminalProfile(L"test-default-name", false);
|
|
|
|
VERIFY_IS_TRUE(std::filesystem::exists(profile_path));
|
|
cleanup.reset();
|
|
|
|
// Terminal profile should be removed when the distribution is unregistered.
|
|
VERIFY_IS_FALSE(std::filesystem::exists(profile_path));
|
|
|
|
// Validate that the base path is removed and that the shortcut is gone*
|
|
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_FALSE(std::filesystem::exists(basePath));
|
|
}
|
|
|
|
// Distribution with default name and no icon
|
|
{
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
|
|
LxsstuLaunchWsl(L"--unregister test-default-name");
|
|
DeleteFile(L"distro-default-name-no-icon.tar");
|
|
});
|
|
|
|
CreateTarFromManifest(L"\n[oobe]\ndefaultName = test-default-name", L"distro-default-name-no-icon.tar");
|
|
InstallFromTar(L"distro-default-name-no-icon.tar");
|
|
ValidateDistributionStarts(L"test-default-name");
|
|
|
|
// Validate that the distribution was installed under the right name and icon
|
|
auto distroKey = OpenDistributionKey(L"test-default-name");
|
|
VERIFY_IS_TRUE(!!distroKey);
|
|
|
|
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
|
|
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
|
|
|
|
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
|
|
ValidateDistributionShortcut(L"test-default-name", nullptr);
|
|
|
|
cleanup.reset();
|
|
|
|
// Validate that the base path is removed and that the shortcut is gone*
|
|
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_FALSE(std::filesystem::exists(basePath));
|
|
}
|
|
|
|
// Distribution with no default name
|
|
{
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
|
|
LxsstuLaunchWsl(L"--unregister test-distro-no-default-name");
|
|
DeleteFile(L"distro-no-default-name.tar");
|
|
});
|
|
|
|
CreateTarFromManifest(L"", L"distro-no-default-name.tar");
|
|
|
|
// Import should fail without --name
|
|
constexpr auto expectedOutput =
|
|
L"Installing: distro-no-default-name.tar\r\n\
|
|
This distribution doesn't contain a default name. Use --name to chose the distribution name.\r\n\
|
|
Error code: Wsl/Service/RegisterDistro/WSL_E_DISTRIBUTION_NAME_NEEDED\r\n";
|
|
|
|
InstallFromTar(L"distro-no-default-name.tar", L"", -1, expectedOutput);
|
|
|
|
// And succeed with --name
|
|
InstallFromTar(L"distro-no-default-name.tar", L"--name test-distro-no-default-name");
|
|
ValidateDistributionStarts(L"test-distro-no-default-name");
|
|
|
|
auto distroKey = OpenDistributionKey(L"test-distro-no-default-name");
|
|
VERIFY_IS_TRUE(!!distroKey);
|
|
|
|
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
|
|
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
|
|
|
|
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
|
|
ValidateDistributionShortcut(L"test-distro-no-default-name", nullptr);
|
|
|
|
cleanup.reset();
|
|
|
|
// Validate that the base path is removed and that the shortcut is gone*
|
|
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_FALSE(std::filesystem::exists(basePath));
|
|
}
|
|
|
|
// Distribution specifying a VHD size.
|
|
auto InstallWithVhdSize = [&](bool FixedVhd) {
|
|
constexpr auto distroName = L"distro-vhd-size";
|
|
constexpr auto tarFileName = L"distro-vhd-size.tar";
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
LxsstuLaunchWsl(std::format(L"--unregister {}", distroName));
|
|
DeleteFile(tarFileName);
|
|
});
|
|
|
|
CreateTarFromManifest(std::format(L"[shortcut]\nicon = /icon.ico\n[oobe]\ndefaultName = {}", distroName).c_str(), tarFileName);
|
|
|
|
InstallFromTar(tarFileName, std::format(L"--vhd-size 1GB {}", FixedVhd ? L"--fixed-vhd" : L"").c_str());
|
|
ValidateDistributionStarts(distroName);
|
|
|
|
// Terminate the VM to make sure the VHD is not in use.
|
|
WslShutdown();
|
|
|
|
// Validate that the distribution was installed under the right name
|
|
auto distroKey = OpenDistributionKey(distroName);
|
|
VERIFY_IS_TRUE(!!distroKey);
|
|
|
|
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
|
|
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
|
|
|
|
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
|
|
|
|
ValidateDistributionShortcut(distroName, icon.get());
|
|
auto [json, profile_path] = ValidateDistributionTerminalProfile(distroName, false);
|
|
|
|
VERIFY_IS_TRUE(std::filesystem::exists(profile_path));
|
|
|
|
// Verify that the is the correct type.
|
|
{
|
|
std::filesystem::path vhdFilePath = std::filesystem::path(basePath) / LXSS_VM_MODE_VHD_NAME;
|
|
VIRTUAL_STORAGE_TYPE storageType{};
|
|
storageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_UNKNOWN;
|
|
storageType.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN;
|
|
wil::unique_handle disk;
|
|
THROW_IF_WIN32_ERROR(OpenVirtualDisk(
|
|
&storageType, vhdFilePath.c_str(), VIRTUAL_DISK_ACCESS_GET_INFO, OPEN_VIRTUAL_DISK_FLAG_NONE, nullptr, &disk));
|
|
|
|
GET_VIRTUAL_DISK_INFO diskInfo{};
|
|
diskInfo.Version = GET_VIRTUAL_DISK_INFO_VIRTUAL_STORAGE_TYPE;
|
|
ULONG diskInfoSize = sizeof(diskInfo);
|
|
THROW_IF_WIN32_ERROR(GetVirtualDiskInformation(disk.get(), &diskInfoSize, &diskInfo, nullptr));
|
|
|
|
VERIFY_IS_TRUE(diskInfo.VirtualStorageType.DeviceId == VIRTUAL_STORAGE_TYPE_DEVICE_VHDX);
|
|
|
|
diskInfo.Version = GET_VIRTUAL_DISK_INFO_PROVIDER_SUBTYPE;
|
|
diskInfoSize = sizeof(diskInfo);
|
|
THROW_IF_WIN32_ERROR(GetVirtualDiskInformation(disk.get(), &diskInfoSize, &diskInfo, nullptr));
|
|
|
|
VERIFY_ARE_EQUAL(FixedVhd, diskInfo.ProviderSubtype == 2);
|
|
}
|
|
|
|
// Unregister the distribution.
|
|
cleanup.reset();
|
|
|
|
// Terminal profile should be removed when the distribution is unregistered.
|
|
VERIFY_IS_FALSE(std::filesystem::exists(profile_path));
|
|
|
|
// Validate that the base path is removed and that the shortcut is gone*
|
|
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_FALSE(std::filesystem::exists(basePath));
|
|
};
|
|
|
|
InstallWithVhdSize(false);
|
|
InstallWithVhdSize(true);
|
|
|
|
// Distribution imported in place
|
|
if (LxsstuVmMode())
|
|
{
|
|
auto CreateVhdFromManifest = [](LPCWSTR Manifest, LPCWSTR VhdName) {
|
|
DistroFileChange distributionconf(L"/etc/wsl-distribution.conf", false);
|
|
distributionconf.SetContent(Manifest);
|
|
WslShutdown();
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--export test_distro {} --format vhd", VhdName)), 0L);
|
|
};
|
|
|
|
auto InstallFromVhd =
|
|
[](LPCWSTR DistroName, LPCWSTR VhdName, int ExpectedExitCode = 0, LPCWSTR ExpectedOutput = nullptr, LPCWSTR ExpectedWarnings = nullptr) {
|
|
auto [out, err] =
|
|
LxsstuLaunchWslAndCaptureOutput(std::format(L"--import-in-place {} {}", DistroName, VhdName), ExpectedExitCode);
|
|
|
|
if (ExpectedOutput != nullptr)
|
|
{
|
|
VERIFY_ARE_EQUAL(ExpectedOutput, out);
|
|
}
|
|
|
|
if (ExpectedWarnings != nullptr)
|
|
{
|
|
VERIFY_ARE_EQUAL(ExpectedWarnings, err);
|
|
}
|
|
};
|
|
|
|
const auto distroName = L"distro-import-in-place";
|
|
const auto vhdName = L"distro-import-in-place.vhdx";
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
LxsstuLaunchWsl(std::format(L"--unregister {}", distroName).c_str());
|
|
DeleteFileW(vhdName);
|
|
});
|
|
|
|
CreateVhdFromManifest(L"", vhdName);
|
|
|
|
InstallFromVhd(distroName, vhdName);
|
|
ValidateDistributionStarts(distroName);
|
|
|
|
// Validate that the distribution was installed under the right name
|
|
auto distroKey = OpenDistributionKey(distroName);
|
|
VERIFY_IS_TRUE(!!distroKey);
|
|
|
|
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
|
|
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
|
|
|
|
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
|
|
ValidateDistributionShortcut(distroName, nullptr);
|
|
auto [json, profile_path] = ValidateDistributionTerminalProfile(distroName, true);
|
|
|
|
VERIFY_IS_TRUE(std::filesystem::exists(profile_path));
|
|
cleanup.reset();
|
|
|
|
// Terminal profile should be removed when the distribution is unregistered.
|
|
VERIFY_IS_FALSE(std::filesystem::exists(profile_path));
|
|
|
|
// Validate that the shortcut is gone
|
|
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
|
|
}
|
|
|
|
// Distribution with overridden default location
|
|
{
|
|
auto cleanup = wil::scope_exit_log(
|
|
WI_DIAGNOSTICS_INFO, []() { LxsstuLaunchWsl(L"--unregister test-overridden-default-location"); });
|
|
|
|
auto currentPath = std::filesystem::current_path();
|
|
WslConfigChange wslconfig(std::format(L"[general]\ndistributionInstallPath = {}", EscapePath(currentPath.wstring())));
|
|
|
|
InstallFromTar(g_testDistroPath.c_str(), L"--name test-overridden-default-location");
|
|
ValidateDistributionStarts(L"test-overridden-default-location");
|
|
|
|
auto distroKey = OpenDistributionKey(L"test-overridden-default-location");
|
|
VERIFY_IS_TRUE(!!distroKey);
|
|
|
|
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
|
|
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
|
|
|
|
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
|
|
|
|
// Validate that the distribution was created in the correct path
|
|
VERIFY_ARE_EQUAL(std::filesystem::path(basePath).parent_path().string(), currentPath.string());
|
|
|
|
ValidateDistributionShortcut(L"test-overridden-default-location", nullptr);
|
|
|
|
cleanup.reset();
|
|
|
|
// Validate that the base path is removed and that the shortcut is gone*
|
|
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_FALSE(std::filesystem::exists(basePath));
|
|
}
|
|
|
|
// Distribution installed in a custom location
|
|
|
|
{
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { LxsstuLaunchWsl(L"--unregister test-custom-location"); });
|
|
|
|
InstallFromTar(g_testDistroPath.c_str(), L"--name test-custom-location --location test-distro-folder");
|
|
ValidateDistributionStarts(L"test-custom-location");
|
|
|
|
// Validate that the distribution was installed under the right name
|
|
auto distroKey = OpenDistributionKey(L"test-custom-location");
|
|
VERIFY_IS_TRUE(!!distroKey);
|
|
|
|
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
|
|
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
|
|
|
|
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
|
|
VERIFY_ARE_EQUAL(std::filesystem::absolute("test-distro-folder").wstring(), basePath);
|
|
|
|
ValidateDistributionShortcut(L"test-custom-location", nullptr);
|
|
|
|
cleanup.reset();
|
|
|
|
// Validate that the base path is removed and that the shortcut is gone*
|
|
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_FALSE(std::filesystem::exists(basePath));
|
|
}
|
|
|
|
// Distribution installed from stdin
|
|
{
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { LxsstuLaunchWsl(L"--unregister test-install-stdin"); });
|
|
|
|
wil::unique_handle importTar{
|
|
CreateFile(g_testDistroPath.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, HANDLE_FLAG_INHERIT, nullptr)};
|
|
|
|
VERIFY_IS_TRUE(!!importTar);
|
|
|
|
VERIFY_IS_TRUE(SetHandleInformation(importTar.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--install --no-launch --from-file - --name test-install-stdin", importTar.get()), 0L);
|
|
|
|
ValidateDistributionStarts(L"test-install-stdin");
|
|
|
|
// Validate that the distribution was installed under the right name
|
|
auto distroKey = OpenDistributionKey(L"test-install-stdin");
|
|
VERIFY_IS_TRUE(!!distroKey);
|
|
|
|
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
|
|
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
|
|
|
|
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
|
|
|
|
ValidateDistributionShortcut(L"test-install-stdin", nullptr);
|
|
|
|
cleanup.reset();
|
|
|
|
// Validate that the base path is removed and that the shortcut is gone*
|
|
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_FALSE(std::filesystem::exists(basePath));
|
|
}
|
|
|
|
// Distribution default name conflicts with already installed distribution
|
|
{
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { DeleteFile(L"conflict.tar"); });
|
|
|
|
CreateTarFromManifest(L"[oobe]\ndefaultName = test_distro", L"conflict.tar");
|
|
|
|
constexpr auto expectedOutput =
|
|
L"Installing: conflict.tar\r\n\
|
|
A distribution with the supplied name already exists. Use --name to chose a different name.\r\n\
|
|
Error code: Wsl/Service/RegisterDistro/ERROR_ALREADY_EXISTS\r\n";
|
|
|
|
InstallFromTar(L"conflict.tar", L"", -1, expectedOutput);
|
|
}
|
|
|
|
// Distribution default name is invalid
|
|
{
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { DeleteFile(L"invalid.tar"); });
|
|
|
|
CreateTarFromManifest(L"[oobe]\ndefaultName = invalid!", L"invalid.tar");
|
|
|
|
constexpr auto expectedOutput =
|
|
L"Installing: invalid.tar\r\n\
|
|
Invalid distribution name: \"invalid!\".\r\n\
|
|
Error code: Wsl/Service/RegisterDistro/E_INVALIDARG\r\n";
|
|
|
|
InstallFromTar(L"invalid.tar", L"", -1, expectedOutput);
|
|
}
|
|
|
|
// Distribution icon file is too big
|
|
{
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
|
|
DeleteFile(L"big-icon.tar");
|
|
LxsstuLaunchWsl(L"--unregister big-icon");
|
|
});
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"fallocate /icon.ico -l 20MB"), 0L);
|
|
|
|
CreateTarFromManifest(L"[shortcut]\nicon = /icon.ico", L"big-icon.tar");
|
|
|
|
WslKeepAlive keepAlive;
|
|
InstallFromTar(L"big-icon.tar", L"--name big-icon");
|
|
ValidateDistributionStarts(L"big-icon");
|
|
|
|
if (LxsstuVmMode())
|
|
{
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"dmesg | grep -iz 'File.*is too big' > /dev/null"), 0L);
|
|
}
|
|
|
|
// Validate that the distribution was installed under the right name
|
|
auto distroKey = OpenDistributionKey(L"big-icon");
|
|
VERIFY_IS_TRUE(!!distroKey);
|
|
|
|
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
|
|
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
|
|
|
|
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
|
|
|
|
ValidateDistributionShortcut(L"big-icon", nullptr);
|
|
|
|
cleanup.reset();
|
|
|
|
// Validate that the base path is removed and that the shortcut is gone*
|
|
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_FALSE(std::filesystem::exists(basePath));
|
|
}
|
|
|
|
// Distribution icon file doesn't exist
|
|
{
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
|
|
DeleteFile(L"icon-not-found.tar");
|
|
LxsstuLaunchWsl(L"--unregister icon-not-found");
|
|
});
|
|
|
|
CreateTarFromManifest(L"[shortcut]\nicon = /does-not-exist.ico", L"icon-not-found.tar");
|
|
|
|
InstallFromTar(L"icon-not-found.tar", L"--name icon-not-found");
|
|
ValidateDistributionStarts(L"icon-not-found");
|
|
|
|
// Validate that the distribution was installed under the right name
|
|
auto distroKey = OpenDistributionKey(L"icon-not-found");
|
|
VERIFY_IS_TRUE(!!distroKey);
|
|
|
|
auto shortcutPath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L"");
|
|
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
|
|
|
|
VERIFY_IS_TRUE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_TRUE(std::filesystem::exists(basePath));
|
|
|
|
ValidateDistributionShortcut(L"icon-not-found", nullptr);
|
|
|
|
cleanup.reset();
|
|
|
|
// Validate that the base path is removed and that the shortcut is gone*
|
|
VERIFY_IS_FALSE(std::filesystem::exists(shortcutPath));
|
|
VERIFY_IS_FALSE(std::filesystem::exists(basePath));
|
|
}
|
|
|
|
// Distribution with a custom terminal profile
|
|
{
|
|
constexpr auto distroName = L"custom-terminal-profile";
|
|
constexpr auto tarName = L"custom-terminal-profile.tar";
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [distroName]() {
|
|
DeleteFile(tarName);
|
|
LxsstuLaunchWsl(std::format(L"--unregister {}", distroName));
|
|
});
|
|
|
|
DistroFileChange profileTemplate(L"/terminal.json", false);
|
|
|
|
constexpr auto templateContent =
|
|
LR"(
|
|
{
|
|
"profiles": [{"custom-field": "custom-value"}],
|
|
"schemes": [{"name": "my-scheme"}]
|
|
})";
|
|
|
|
profileTemplate.SetContent(templateContent);
|
|
|
|
CreateTarFromManifest(L"[windowsterminal]\nprofileTemplate = /terminal.json", tarName);
|
|
|
|
InstallFromTar(tarName, std::format(L"--name {}", distroName).c_str());
|
|
ValidateDistributionStarts(distroName);
|
|
|
|
auto distroKey = OpenDistributionKey(distroName);
|
|
VERIFY_IS_TRUE(!!distroKey);
|
|
|
|
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
|
|
auto [json, profile_path] = ValidateDistributionTerminalProfile(distroName, true);
|
|
|
|
VERIFY_ARE_EQUAL(json["profiles"][1]["custom-field"].get<std::string>(), "custom-value");
|
|
VERIFY_ARE_EQUAL(json["schemes"][0]["name"].get<std::string>(), "my-scheme");
|
|
|
|
VERIFY_IS_TRUE(std::filesystem::exists(profile_path));
|
|
cleanup.reset();
|
|
|
|
// Terminal profile should be removed when the distribution is unregistered.
|
|
VERIFY_IS_FALSE(std::filesystem::exists(profile_path));
|
|
}
|
|
|
|
// Distribution with an invalid terminal profile json
|
|
{
|
|
constexpr auto distroName = L"custom-terminal-profile-bad-json";
|
|
constexpr auto tarName = L"custom-terminal-profile-bad-json.tar";
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [distroName]() {
|
|
DeleteFile(tarName);
|
|
LxsstuLaunchWsl(std::format(L"--unregister {}", distroName));
|
|
});
|
|
|
|
DistroFileChange profileTemplate(L"/terminal.json", false);
|
|
profileTemplate.SetContent(L"bad-json");
|
|
|
|
CreateTarFromManifest(L"[windowsterminal]\nprofileTemplate = /terminal.json", tarName);
|
|
|
|
// Validate the invalid json blob generates a warning.
|
|
InstallFromTar(
|
|
tarName,
|
|
std::format(L"--name {}", distroName).c_str(),
|
|
0,
|
|
nullptr,
|
|
L"wsl: Failed to parse terminal profile while registering distribution: [json.exception.parse_error.101] "
|
|
L"parse "
|
|
L"error at line 1, column 1: syntax error while parsing value - invalid literal; last read: 'b'\r\n");
|
|
|
|
ValidateDistributionStarts(distroName);
|
|
}
|
|
|
|
// Distribution with a preexisting hide profile.
|
|
{
|
|
constexpr auto distroName = L"custom-terminal-profile-hide";
|
|
constexpr auto tarName = L"custom-terminal-profile-hide.tar";
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [distroName]() {
|
|
DeleteFile(tarName);
|
|
LxsstuLaunchWsl(std::format(L"--unregister {}", distroName));
|
|
});
|
|
|
|
auto profileGuid = wsl::shared::string::GuidToString<wchar_t>(
|
|
CreateV5Uuid(GeneratedProfilesTerminalNamespace, std::as_bytes(std::span{std::wstring_view{distroName}})));
|
|
|
|
auto content = std::format(
|
|
LR"({{"profiles": [{{ "updates": "{}", "hidden": true, "custom": true}}, {{"name": "my-profile"}}]}})", profileGuid);
|
|
|
|
DistroFileChange profileTemplate(L"/terminal.json", false);
|
|
profileTemplate.SetContent(content.c_str());
|
|
|
|
CreateTarFromManifest(L"[windowsterminal]\nprofileTemplate = /terminal.json", tarName);
|
|
InstallFromTar(tarName, std::format(L"--name {}", distroName).c_str());
|
|
|
|
ValidateDistributionStarts(distroName);
|
|
|
|
auto distroKey = OpenDistributionKey(distroName);
|
|
VERIFY_IS_TRUE(!!distroKey);
|
|
|
|
// Validate that the default terminal profile is still generated.
|
|
auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
|
|
auto [json, profile_path] = ValidateDistributionTerminalProfile(distroName, true);
|
|
VERIFY_ARE_EQUAL(json["profiles"][0]["custom"].get<bool>(), true);
|
|
VERIFY_ARE_EQUAL(json["profiles"].size(), 2);
|
|
|
|
VERIFY_IS_TRUE(std::filesystem::exists(profile_path));
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
profile_path, wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"TerminalProfilePath", L""));
|
|
|
|
cleanup.reset();
|
|
|
|
// Terminal profile should be removed when the distribution is unregistered.
|
|
VERIFY_IS_FALSE(std::filesystem::exists(profile_path));
|
|
}
|
|
|
|
// Distribution opting-out of terminal profile generation
|
|
{
|
|
constexpr auto distroName = L"no-terminal-profile";
|
|
constexpr auto tarName = L"no-terminal-profile.tar";
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [distroName]() {
|
|
DeleteFile(tarName);
|
|
LxsstuLaunchWsl(std::format(L"--unregister {}", distroName));
|
|
});
|
|
|
|
CreateTarFromManifest(L"[windowsterminal]\nenabled = false", tarName);
|
|
|
|
InstallFromTar(tarName, std::format(L"--name {}", distroName).c_str());
|
|
|
|
auto distroKey = OpenDistributionKey(distroName);
|
|
VERIFY_IS_TRUE(!!distroKey);
|
|
|
|
// Validate that no terminal profile is generated.
|
|
VERIFY_ARE_EQUAL(
|
|
L"", wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"TerminalProfilePath", L""));
|
|
}
|
|
|
|
// Distribution opting-out of shortcut generation
|
|
{
|
|
constexpr auto distroName = L"no-shortcut";
|
|
constexpr auto tarName = L"no-shortcut.tar";
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [distroName]() {
|
|
DeleteFile(tarName);
|
|
LxsstuLaunchWsl(std::format(L"--unregister {}", distroName));
|
|
});
|
|
|
|
CreateTarFromManifest(L"[shortcut]\nenabled = false", tarName);
|
|
|
|
InstallFromTar(tarName, std::format(L"--name {}", distroName).c_str());
|
|
|
|
auto distroKey = OpenDistributionKey(distroName);
|
|
VERIFY_IS_TRUE(!!distroKey);
|
|
|
|
// Validate that no terminal profile is generated.
|
|
VERIFY_ARE_EQUAL(L"", wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"ShortcutPath", L""));
|
|
}
|
|
}
|
|
|
|
static auto SetManifest(const std::string& Content, bool Append = false)
|
|
{
|
|
auto file = wsl::windows::common::filesystem::TempFile(GENERIC_WRITE, FILE_SHARE_READ, OPEN_EXISTING);
|
|
VERIFY_IS_TRUE(WriteFile(file.Handle.get(), Content.c_str(), static_cast<DWORD>(Content.size()), nullptr, nullptr));
|
|
|
|
RegistryKeyChange<std::wstring> manifestOverride{
|
|
HKEY_LOCAL_MACHINE,
|
|
LXSS_REGISTRY_PATH,
|
|
Append ? wsl::windows::common::distribution::c_distroUrlAppendRegistryValue : wsl::windows::common::distribution::c_distroUrlRegistryValue,
|
|
L"file://" + file.Path.wstring()};
|
|
|
|
return std::make_pair(std::move(file), std::move(manifestOverride));
|
|
}
|
|
|
|
static void ValidateInstall(const std::wstring& cmd, LPCWSTR ExpectedOutput = nullptr)
|
|
{
|
|
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--install --no-launch {}", cmd));
|
|
|
|
if (ExpectedOutput != nullptr)
|
|
{
|
|
VERIFY_ARE_EQUAL(ExpectedOutput, out);
|
|
}
|
|
}
|
|
|
|
static void ValidateInstallError(
|
|
const std::wstring& cmd, const std::wstring& expectedOutput, const std::wstring& expectedWarnings = L"")
|
|
{
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(cmd, -1);
|
|
|
|
VERIFY_ARE_EQUAL(expectedOutput, out);
|
|
VERIFY_ARE_EQUAL(expectedWarnings, err);
|
|
}
|
|
|
|
static void UnregisterDistribution(LPCWSTR Name)
|
|
{
|
|
LxsstuLaunchWsl(std::format(L"--unregister {}", Name));
|
|
}
|
|
|
|
TEST_METHOD(FileUrl)
|
|
{
|
|
auto check = [](LPCWSTR Input, const std::optional<std::filesystem::path>& ExpectedOutput) {
|
|
const auto output = wsl::windows::common::filesystem::TryGetPathFromFileUrl(Input);
|
|
|
|
VERIFY_ARE_EQUAL(output.has_value(), ExpectedOutput.has_value());
|
|
|
|
if (output.has_value())
|
|
{
|
|
VERIFY_ARE_EQUAL(output.value(), ExpectedOutput.value());
|
|
}
|
|
};
|
|
|
|
check(L"file://C:/File", L"C:\\File");
|
|
check(L"file://C:\\File", L"C:\\File");
|
|
check(L"file:///C:\\File", L"C:\\File");
|
|
check(L"file:///RelativeFile", L"RelativeFile");
|
|
check(L"file:///RelativeFile\\SubPath/SubPath", L"RelativeFile\\SubPath\\SubPath");
|
|
check(L"notfile:///C:\\File", {});
|
|
}
|
|
|
|
TEST_METHOD(MacAddressParsing)
|
|
{
|
|
using namespace wsl::shared::string;
|
|
auto testParse = [](const std::wstring& Input, const std::optional<MacAddress>& ExpectedOutput, char separator = '\0') {
|
|
const auto result = wsl::shared::string::ParseMacAddressNoThrow<wchar_t>(Input, separator);
|
|
|
|
VERIFY_ARE_EQUAL(result.has_value(), ExpectedOutput.has_value());
|
|
if (result.has_value())
|
|
{
|
|
VERIFY_ARE_EQUAL(ExpectedOutput.value(), result.value());
|
|
}
|
|
};
|
|
|
|
testParse(L"", {});
|
|
testParse(L"-", {});
|
|
testParse(L"00:00:00:00:00:0", {});
|
|
testParse(L"00::00:00:00:00:00", {});
|
|
testParse(L"000:00:00:00:00:00", {});
|
|
testParse(L"000:00:00:00:00:0g", {});
|
|
testParse(L"00:00:00:00:00:00", {{0, 0, 0, 0, 0, 0}});
|
|
testParse(L"01:23:45:67:89:AB", {{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}});
|
|
testParse(L"01-23-45-67-89-AB", {{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}});
|
|
testParse(L"01-23-45-67-89-AB", {{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}}, '-');
|
|
testParse(L"01-23-45-67-89-AB", {}, ':');
|
|
testParse(L"01-23-45-67-89:AB", {});
|
|
testParse(L"01,23,45,67,89,AB", {});
|
|
|
|
VERIFY_ARE_EQUAL(wsl::shared::string::FormatMacAddress({0x01, 0x23, 0x45, 0x67, 0x89, 0xab}, '-'), "01-23-45-67-89-AB");
|
|
VERIFY_ARE_EQUAL(wsl::shared::string::FormatMacAddress({0x01, 0x23, 0x45, 0x67, 0x89, 0xab}, ':'), "01:23:45:67:89:AB");
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
wsl::shared::string::FormatMacAddress({0x01, 0x23, 0x45, 0x67, 0x89, 0xab}, L'-'),
|
|
wsl::shared::string::MultiByteToWide("01-23-45-67-89-AB"));
|
|
VERIFY_ARE_EQUAL(
|
|
wsl::shared::string::FormatMacAddress({0x01, 0x23, 0x45, 0x67, 0x89, 0xab}, L':'),
|
|
wsl::shared::string::MultiByteToWide("01:23:45:67:89:AB"));
|
|
}
|
|
|
|
TEST_METHOD(ModernDistroInstall)
|
|
{
|
|
auto tarPath = "file://" + wsl::shared::string::WideToMultiByte(EscapePath(g_testDistroPath));
|
|
|
|
wil::unique_handle tarHandle{CreateFile(g_testDistroPath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr)};
|
|
VERIFY_IS_TRUE(!!tarHandle);
|
|
|
|
auto tarHash = wsl::shared::string::WideToMultiByte(
|
|
wsl::windows::common::string::BytesToHex(wsl::windows::common::wslutil::HashFile(tarHandle.get(), CALG_SHA_256)));
|
|
|
|
// Install a modern distribution
|
|
{
|
|
auto manifest = std::format(
|
|
R"({{
|
|
"ModernDistributions": {{
|
|
"debian": [
|
|
{{
|
|
"Name": "debian-12",
|
|
"FriendlyName": "DebianFriendlyName",
|
|
"Default": true,
|
|
"Amd64Url": {{
|
|
"Url": "{}",
|
|
"Sha256": "{}"
|
|
}}
|
|
}}
|
|
]
|
|
}}}})",
|
|
tarPath,
|
|
tarHash);
|
|
|
|
auto restore = SetManifest(manifest);
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { UnregisterDistribution(L"debian-12"); });
|
|
|
|
ValidateInstall(L"debian --no-launch --name debian-12");
|
|
|
|
ValidateDistributionStarts(L"debian-12");
|
|
|
|
UnregisterDistribution(L"debian-12");
|
|
|
|
ValidateInstall(L"debian-12 --no-launch --name debian-12");
|
|
ValidateDistributionStarts(L"debian-12");
|
|
|
|
ValidateInstallError(
|
|
L"--install DoesNotExists",
|
|
L"Invalid distribution name: 'DoesNotExists'.\r\n\
|
|
To get a list of valid distributions, use 'wsl.exe --list --online'.\r\n\
|
|
Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND\r\n");
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister debian-12"), 0L);
|
|
|
|
// Verify that name matching is not case-sensitive on the version.
|
|
ValidateInstall(L"Debian-12 --no-launch --name debian-12");
|
|
ValidateDistributionStarts(L"debian-12");
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister debian-12"), 0L);
|
|
|
|
// Verify that name matching is not case-sensitive on the flavor.
|
|
ValidateInstall(L"Debian --no-launch --name debian-12");
|
|
ValidateDistributionStarts(L"debian-12");
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister debian-12"), 0L);
|
|
|
|
// Validate an install with a vhd size.
|
|
ValidateInstall(L"Debian --no-launch --name debian-12 --vhd-size 1GB");
|
|
ValidateDistributionStarts(L"debian-12");
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister debian-12"), 0L);
|
|
|
|
// Validate an install with a vhd size and fixed vhd.
|
|
ValidateInstall(L"Debian --no-launch --name debian-12 --vhd-size 1GB --fixed-vhd");
|
|
ValidateDistributionStarts(L"debian-12");
|
|
}
|
|
|
|
// Validate that default works correctly
|
|
{
|
|
auto manifest = std::format(
|
|
R"({{
|
|
"Default": "debian",
|
|
"ModernDistributions": {{
|
|
"debian": [
|
|
{{
|
|
"Name": "debian-nondefault",
|
|
"FriendlyName": "",
|
|
"Amd64Url": {{
|
|
"Url": "",
|
|
"Sha256": ""
|
|
}}
|
|
}},
|
|
{{
|
|
"Name": "debian-default",
|
|
"FriendlyName": "DebianFriendlyName",
|
|
"Default": true,
|
|
"Amd64Url": {{
|
|
"Url": "{}",
|
|
"Sha256": "{}"
|
|
}}
|
|
}}
|
|
],
|
|
|
|
"ubuntu": [
|
|
{{
|
|
"Name": "ubuntu-nondefault",
|
|
"FriendlyName": "",
|
|
"Amd64Url": {{
|
|
"Url": "",
|
|
"Sha256": ""
|
|
}}
|
|
}},
|
|
{{
|
|
"Name": "ubuntu-default",
|
|
"FriendlyName": "UbuntuFriendlyName",
|
|
"Default": true,
|
|
"Amd64Url": {{
|
|
"Url": "{}",
|
|
"Sha256": "{}"
|
|
}}
|
|
}}
|
|
]
|
|
}}}})",
|
|
tarPath,
|
|
tarHash,
|
|
tarPath,
|
|
tarHash);
|
|
|
|
auto restore = SetManifest(manifest);
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
|
|
UnregisterDistribution(L"debian-default");
|
|
UnregisterDistribution(L"ubuntu-default");
|
|
});
|
|
|
|
ValidateInstall(
|
|
L"--no-launch --name debian-default",
|
|
L"Installing: DebianFriendlyName\r\n\
|
|
Distribution successfully installed. It can be launched via 'wsl.exe -d debian-default'\r\n");
|
|
|
|
ValidateDistributionStarts(L"debian-default");
|
|
|
|
ValidateInstall(
|
|
L"ubuntu --no-launch --name ubuntu-default",
|
|
L"Installing: UbuntuFriendlyName\r\n\
|
|
Distribution successfully installed. It can be launched via 'wsl.exe -d ubuntu-default'\r\n");
|
|
|
|
ValidateDistributionStarts(L"ubuntu-default");
|
|
|
|
// Validate that default can be override via the 'Append' manifest
|
|
auto overrideRestore = SetManifest(R"({"Default": "ubuntu"})", true);
|
|
|
|
UnregisterDistribution(L"ubuntu-default");
|
|
|
|
ValidateInstall(
|
|
L"--no-launch --name ubuntu-default",
|
|
L"Installing: UbuntuFriendlyName\r\n\
|
|
Distribution successfully installed. It can be launched via 'wsl.exe -d ubuntu-default'\r\n");
|
|
|
|
ValidateDistributionStarts(L"ubuntu-default");
|
|
}
|
|
|
|
// Install a legacy distribution
|
|
{
|
|
auto manifest = std::format(
|
|
R"({{
|
|
"ModernDistributions": {{
|
|
"debian": [
|
|
{{
|
|
"Name": "debian-12",
|
|
"FriendlyName": "DebianFriendlyName",
|
|
"Amd64Url": {{
|
|
"Url": "",
|
|
"Sha256": ""
|
|
}}
|
|
}}
|
|
]
|
|
}},
|
|
"Distributions": [
|
|
{{"Name": "legacy",
|
|
"FriendlyName": "legacy",
|
|
"StoreAppId": "Dummy",
|
|
"PackageFamilyName": "Dummy",
|
|
"Amd64": true,
|
|
"Arm64": true,
|
|
"Amd64PackageUrl": "http://127.0.0.1:12/dummyUrl" }}]
|
|
}})",
|
|
tarPath);
|
|
|
|
auto restore = SetManifest(manifest);
|
|
|
|
// There's no easy way to automate the appx package installation, but verify that we take the legacy path
|
|
ValidateInstallError(
|
|
L"--install legacy --no-launch --web-download",
|
|
L"Downloading: legacy\r\n\
|
|
A connection with the server could not be established \r\n\
|
|
Error code: Wsl/InstallDistro/WININET_E_CANNOT_CONNECT\r\n",
|
|
L"wsl: Using legacy distribution registration. Consider using a tar based distribution instead.\r\n");
|
|
|
|
ValidateInstallError(
|
|
L"--install legacy --no-launch --web-download --legacy",
|
|
L"Downloading: legacy\r\n\
|
|
A connection with the server could not be established \r\n\
|
|
Error code: Wsl/InstallDistro/WININET_E_CANNOT_CONNECT\r\n",
|
|
L"wsl: Using legacy distribution registration. Consider using a tar based distribution instead.\r\n");
|
|
}
|
|
|
|
// Validate that modern distros takes precedences, but can be overridden.
|
|
{
|
|
auto manifest = std::format(
|
|
R"({{
|
|
"ModernDistributions": {{
|
|
"debian": [
|
|
{{
|
|
"Name": "debian-12",
|
|
"FriendlyName": "DebianFriendlyName",
|
|
"Amd64Url": {{
|
|
"Url": "{}",
|
|
"Sha256": "{}"
|
|
}}
|
|
}}
|
|
]
|
|
}},
|
|
"Distributions": [
|
|
{{"Name": "debian-12",
|
|
"FriendlyName": "debian-12",
|
|
"StoreAppId": "Dummy",
|
|
"PackageFamilyName": "Dummy",
|
|
"Amd64": true,
|
|
"Arm64": true,
|
|
"Amd64PackageUrl": "http://127.0.0.1:12/dummyUrl" }}]
|
|
}})",
|
|
tarPath,
|
|
tarHash);
|
|
|
|
auto restore = SetManifest(manifest);
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { UnregisterDistribution(L"debian-12"); });
|
|
|
|
ValidateInstall(L"debian-12 --no-launch --name debian-12");
|
|
ValidateDistributionStarts(L"debian-12");
|
|
|
|
// Validate that --legacy takes the appx path.
|
|
ValidateInstallError(
|
|
L"--install debian-12 --no-launch --web-download --legacy",
|
|
L"Downloading: debian-12\r\n\
|
|
A connection with the server could not be established \r\n\
|
|
Error code: Wsl/InstallDistro/WININET_E_CANNOT_CONNECT\r\n",
|
|
L"wsl: Using legacy distribution registration. Consider using a tar based distribution instead.\r\n");
|
|
}
|
|
|
|
// Validate that distribution can be overridden
|
|
{
|
|
auto manifest = std::format(
|
|
R"({{
|
|
"ModernDistributions": {{
|
|
"debian": [
|
|
{{
|
|
"Name": "debian-12",
|
|
"FriendlyName": "DebianFriendlyName",
|
|
"Amd64Url": {{
|
|
"Url": "{}",
|
|
"Sha256": "{}"
|
|
}}
|
|
}},
|
|
{{
|
|
"Name": "debian-base",
|
|
"FriendlyName": "DebianFriendlyName",
|
|
"Default": true,
|
|
"Amd64Url": {{
|
|
"Url": "{}",
|
|
"Sha256": ""
|
|
}}
|
|
}}
|
|
]
|
|
}},
|
|
"Distributions": [{{"Name": "Dummy", "FriendlyName": "Dummy", "StoreAppId": "Dummy", "Amd64": true, "Arm64": true }}]
|
|
}})",
|
|
"DoesNotExist",
|
|
tarPath,
|
|
tarHash);
|
|
|
|
auto overrideManifest = std::format(
|
|
R"({{
|
|
"ModernDistributions": {{
|
|
"debian": [
|
|
{{
|
|
"Name": "debian-12",
|
|
"FriendlyName": "DebianFriendlyNameOverridden",
|
|
"Amd64Url": {{
|
|
"Url": "{}",
|
|
"Sha256": "{}"
|
|
}}
|
|
}}
|
|
]
|
|
}}
|
|
}})",
|
|
tarPath,
|
|
tarHash);
|
|
|
|
auto restore = SetManifest(manifest);
|
|
auto override = SetManifest(overrideManifest, true);
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
|
|
UnregisterDistribution(L"debian-12");
|
|
UnregisterDistribution(L"debian-base");
|
|
});
|
|
|
|
ValidateInstall(L"debian-12 --no-launch --name debian-12");
|
|
|
|
// Validate that distros coming from the 'main' manifest can still be installed.
|
|
ValidateInstall(L"debian-12 --no-launch --name debian-base");
|
|
}
|
|
|
|
// Validate that the distribution default name comes from the manifest, event if oobe.defaultName isn't set
|
|
{
|
|
auto manifest = std::format(
|
|
R"({{
|
|
"ModernDistributions": {{
|
|
"debian": [
|
|
{{
|
|
"Name": "test-default-manifest-name",
|
|
"FriendlyName": "DebianFriendlyName",
|
|
"Amd64Url": {{
|
|
"Url": "{}",
|
|
"Sha256": "{}"
|
|
}}
|
|
}}
|
|
]
|
|
}}
|
|
}})",
|
|
tarPath,
|
|
tarHash);
|
|
|
|
auto restore = SetManifest(manifest);
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { UnregisterDistribution(L"test-default-manifest-name"); });
|
|
|
|
ValidateInstall(L"test-default-manifest-name");
|
|
ValidateDistributionStarts(L"test-default-manifest-name");
|
|
}
|
|
|
|
// Validate that install fails if hash doesn't match
|
|
{
|
|
auto manifest = std::format(
|
|
R"({{
|
|
"ModernDistributions": {{
|
|
"debian": [
|
|
{{
|
|
"Name": "debian-12",
|
|
"FriendlyName": "DebianFriendlyName",
|
|
"Amd64Url": {{
|
|
"Url": "{}",
|
|
"Sha256": "0x12"
|
|
}}
|
|
}}
|
|
]
|
|
}}
|
|
}})",
|
|
tarPath);
|
|
|
|
auto restore = SetManifest(manifest);
|
|
|
|
ValidateInstallError(
|
|
L"--install debian-12",
|
|
std::format(
|
|
L"Installing: DebianFriendlyName\r\n\
|
|
The distribution hash doesn't match. Expected: 0x12, actual hash: {}\r\n\
|
|
Error code: Wsl/InstallDistro/VerifyChecksum/TRUST_E_BAD_DIGEST\r\n",
|
|
wsl::shared::string::MultiByteToWide(tarHash)),
|
|
L"");
|
|
}
|
|
|
|
// Validate that we fail if the hash format is incorrect
|
|
{
|
|
auto manifest = std::format(
|
|
R"({{
|
|
"ModernDistributions": {{
|
|
"debian": [
|
|
{{
|
|
"Name": "debian-12",
|
|
"FriendlyName": "DebianFriendlyName",
|
|
"Amd64Url": {{
|
|
"Url": "{}",
|
|
"Sha256": "wrongformat"
|
|
}}
|
|
}}
|
|
]
|
|
}}
|
|
}})",
|
|
tarPath);
|
|
|
|
auto restore = SetManifest(manifest);
|
|
|
|
ValidateInstallError(
|
|
L"--install debian-12",
|
|
L"Installing: DebianFriendlyName\r\n\
|
|
Invalid hex string: wrongformat\r\n\
|
|
Error code: Wsl/InstallDistro/VerifyChecksum/E_INVALIDARG\r\n",
|
|
L"");
|
|
}
|
|
|
|
// Validate various command line error paths
|
|
{
|
|
auto manifest =
|
|
R"({
|
|
"Distributions": [
|
|
{"Name": "debian-12",
|
|
"FriendlyName": "debian-12",
|
|
"StoreAppId": "Dummy",
|
|
"PackageFamilyName": "Dummy",
|
|
"Amd64": true,
|
|
"Arm64": true,
|
|
"Amd64PackageUrl": "" }]
|
|
})";
|
|
|
|
auto restore = SetManifest(manifest);
|
|
|
|
ValidateInstallError(
|
|
L"--install debian-12 --location foo",
|
|
L"'--location' is not supported when installing legacy distributions.\r\n",
|
|
L"");
|
|
|
|
ValidateInstallError(
|
|
L"--install debian-12 --name foo", L"'--name' is not supported when installing legacy distributions.\r\n", L"");
|
|
|
|
ValidateInstallError(
|
|
L"--install debian-12 --vhd-size 1GB",
|
|
L"'--vhd-size' is not supported when installing legacy distributions.\r\n",
|
|
L"");
|
|
|
|
ValidateInstallError(
|
|
L"--install invalid",
|
|
L"Invalid distribution name: 'invalid'.\r\n\
|
|
To get a list of valid distributions, use 'wsl.exe --list --online'.\r\n\
|
|
Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND\r\n",
|
|
L"");
|
|
}
|
|
|
|
// Validate that a distribution isn't downloaded if its name is already in use.
|
|
{
|
|
auto manifest = std::format(
|
|
R"({{
|
|
"ModernDistributions": {{
|
|
"debian": [
|
|
{{
|
|
"Name": "{}",
|
|
"FriendlyName": "DebianFriendlyName",
|
|
"Amd64Url": {{
|
|
"Url": "file://nonexistent",
|
|
"Sha256": ""
|
|
}}
|
|
}},
|
|
{{
|
|
"Name": "dummy",
|
|
"FriendlyName": "dummy",
|
|
"Amd64Url": {{
|
|
"Url": "file://nonexistent",
|
|
"Sha256": ""
|
|
}}
|
|
}}
|
|
]
|
|
}}
|
|
}})",
|
|
LXSS_DISTRO_NAME_TEST);
|
|
|
|
auto restore = SetManifest(manifest);
|
|
|
|
{
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--install {}", LXSS_DISTRO_NAME_TEST_L), -1);
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
out,
|
|
L"Cannot create a file when that file already exists. \r\n"
|
|
L"Error code: Wsl/InstallDistro/ERROR_ALREADY_EXISTS\r\n");
|
|
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
|
|
{
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--install dummy --name {}", LXSS_DISTRO_NAME_TEST_L), -1);
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
out,
|
|
L"Cannot create a file when that file already exists. \r\n"
|
|
L"Error code: Wsl/InstallDistro/ERROR_ALREADY_EXISTS\r\n");
|
|
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
}
|
|
|
|
// Validate handling of case where no default install distro is configured.
|
|
{
|
|
auto manifest =
|
|
R"({
|
|
"ModernDistributions": {
|
|
"debian": [
|
|
{
|
|
"Name": "debian-12",
|
|
"FriendlyName": "DebianFriendlyName",
|
|
"Amd64Url": {
|
|
"Url": "",
|
|
"Sha256": ""
|
|
}
|
|
}
|
|
]
|
|
}
|
|
})";
|
|
|
|
auto restore = SetManifest(manifest);
|
|
ValidateInstallError(
|
|
L"--install",
|
|
L"No default distribution has been configured. Please provide a distribution to install.\r\n\
|
|
Error code: Wsl/InstallDistro/E_UNEXPECTED\r\n",
|
|
L"");
|
|
}
|
|
|
|
// Validate that invalid json errors are correctly handled.
|
|
{
|
|
auto restore = SetManifest("Bad json");
|
|
|
|
ValidateInstallError(
|
|
L"--install debian",
|
|
L"Invalid JSON document. Parse error: [json.exception.parse_error.101] parse error at line 1, column 1: syntax error while parsing value - invalid literal; last read: 'B'\r\n\
|
|
Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n",
|
|
L"");
|
|
}
|
|
|
|
// Validate that url parameters are correctly handled.
|
|
{
|
|
constexpr auto tarEndpoint = L"http://127.0.0.1:6667/";
|
|
|
|
UniqueWebServer fileServer(tarEndpoint, std::filesystem::path(g_testDistroPath));
|
|
|
|
wil::unique_handle tarHandle{CreateFile(g_testDistroPath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr)};
|
|
VERIFY_IS_TRUE(!!tarHandle);
|
|
|
|
auto manifest = std::format(
|
|
R"({{
|
|
"ModernDistributions": {{
|
|
"test": [
|
|
{{
|
|
"Name": "test-url-download",
|
|
"FriendlyName": "FriendlyName",
|
|
"Default": true,
|
|
"Amd64Url": {{
|
|
"Url": "{}/distro.tar?foo=bar&key=value",
|
|
"Sha256": "{}"
|
|
}}
|
|
}}
|
|
]
|
|
}}}})",
|
|
tarEndpoint,
|
|
tarHash);
|
|
|
|
auto restore = SetManifest(manifest);
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { UnregisterDistribution(L"test-url-download"); });
|
|
|
|
auto [output, error] = LxsstuLaunchWslAndCaptureOutput(L"--install --no-launch test-url-download");
|
|
VERIFY_ARE_EQUAL(
|
|
output,
|
|
L"Downloading: FriendlyName\r\nInstalling: FriendlyName\r\nDistribution successfully installed. It can be "
|
|
L"launched via 'wsl.exe -d test-url-download'\r\n");
|
|
|
|
VERIFY_ARE_EQUAL(error, L"");
|
|
}
|
|
|
|
// Validate that manifest distribution ordering is preserved.
|
|
{
|
|
auto validateOrder = [](const std::vector<LPCWSTR>& expected) {
|
|
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"--list --online");
|
|
|
|
auto lines = wsl::shared::string::Split<wchar_t>(out, '\n');
|
|
|
|
for (size_t i = 0; i < expected.size(); i++)
|
|
{
|
|
auto end = lines[i + 4].find_first_of(L" \t");
|
|
VERIFY_ARE_NOT_EQUAL(end, std::wstring::npos);
|
|
|
|
auto distro = lines[i + 4].substr(0, end);
|
|
|
|
VERIFY_ARE_EQUAL(expected[i], distro);
|
|
}
|
|
};
|
|
|
|
{
|
|
auto manifest =
|
|
R"({
|
|
"ModernDistributions": {
|
|
"distro1": [
|
|
{
|
|
"Name": "distro1",
|
|
"FriendlyName": "distro1Name",
|
|
"Amd64Url": {"Url": "","Sha256": ""}
|
|
}
|
|
],
|
|
"distro2": [
|
|
{
|
|
"Name": "distro2",
|
|
"FriendlyName": "distro2Name",
|
|
"Amd64Url": {"Url": "","Sha256": ""}
|
|
}
|
|
]
|
|
}
|
|
})";
|
|
|
|
auto restore = SetManifest(manifest);
|
|
validateOrder({L"distro1", L"distro2"});
|
|
}
|
|
|
|
{
|
|
auto manifest =
|
|
R"({
|
|
"ModernDistributions": {
|
|
"distro2": [
|
|
{
|
|
"Name": "distro2",
|
|
"FriendlyName": "distro2Name",
|
|
"Amd64Url": {"Url": "","Sha256": ""}
|
|
}
|
|
],
|
|
"distro1": [
|
|
{
|
|
"Name": "distro1",
|
|
"FriendlyName": "distro1Name",
|
|
"Amd64Url": {"Url": "","Sha256": ""}
|
|
}
|
|
]
|
|
}
|
|
})";
|
|
|
|
auto restore = SetManifest(manifest);
|
|
|
|
validateOrder({L"distro2", L"distro1"});
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ModernInstallEndToEnd)
|
|
{
|
|
constexpr auto tarName = L"end2end.tar";
|
|
|
|
DistroFileChange distributionconf(L"/etc/wsl-distribution.conf", false);
|
|
distributionconf.SetContent(
|
|
L"[oobe]\ncommand = /bin/bash -c 'echo OOBE && useradd -u 1011 -m -s /bin/bash myuser'\n defaultUid = 1011\n");
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--export test_distro {}", tarName)), 0L);
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() {
|
|
DeleteFile(tarName);
|
|
|
|
LxsstuLaunchWsl(L"--unregister end2end");
|
|
});
|
|
|
|
wil::unique_handle tarHandle{CreateFile(tarName, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr)};
|
|
VERIFY_IS_TRUE(!!tarHandle);
|
|
|
|
auto tarHash = wsl::windows::common::string::BytesToHex(wsl::windows::common::wslutil::HashFile(tarHandle.get(), CALG_SHA_256));
|
|
|
|
constexpr auto manifestEndpoint = L"http://127.0.0.1:6666/";
|
|
constexpr auto tarEndpoint = L"http://127.0.0.1:6667/";
|
|
|
|
auto manifest = std::format(
|
|
LR"({{
|
|
\"ModernDistributions\": {{
|
|
\"end2end\": [
|
|
{{
|
|
\"Name\": \"end2end\",
|
|
\"FriendlyName\": \"FriendlyName\",
|
|
\"Default\": true,
|
|
\"Amd64Url\": {{
|
|
\"Url\": \"{}/distro.tar\",
|
|
\"Sha256\": \"{}\"
|
|
}}
|
|
}}
|
|
]
|
|
}}}})",
|
|
tarEndpoint,
|
|
tarHash);
|
|
|
|
UniqueWebServer apiServer(manifestEndpoint, manifest.c_str());
|
|
UniqueWebServer fileServer(tarEndpoint, std::filesystem::path(tarName));
|
|
|
|
RegistryKeyChange<std::wstring> manifestOverride{
|
|
HKEY_LOCAL_MACHINE, LXSS_REGISTRY_PATH, wsl::windows::common::distribution::c_distroUrlRegistryValue, manifestEndpoint};
|
|
|
|
{
|
|
auto [output, error] = LxsstuLaunchWslAndCaptureOutput(L"--install --no-launch end2end");
|
|
VERIFY_ARE_EQUAL(
|
|
output,
|
|
L"Downloading: FriendlyName\r\nInstalling: FriendlyName\r\nDistribution successfully installed. It can be "
|
|
L"launched via 'wsl.exe -d end2end'\r\n");
|
|
|
|
VERIFY_ARE_EQUAL(error, L"");
|
|
}
|
|
|
|
// Check that OOBE runs
|
|
{
|
|
auto [read, write] = CreateSubprocessPipe(true, false);
|
|
write.reset();
|
|
|
|
wsl::windows::common::SubProcess process(nullptr, LxssGenerateWslCommandLine(L"-d end2end").c_str());
|
|
process.SetStdHandles(read.get(), nullptr, nullptr);
|
|
|
|
auto oobeResult = process.RunAndCaptureOutput();
|
|
VERIFY_ARE_EQUAL(oobeResult.Stdout, L"OOBE\n");
|
|
VERIFY_ARE_EQUAL(oobeResult.Stderr, L"");
|
|
VERIFY_ARE_EQUAL(oobeResult.ExitCode, 0);
|
|
}
|
|
|
|
// Run the command again to check that oobe doesn't run twice
|
|
{
|
|
auto [read, write] = CreateSubprocessPipe(true, false);
|
|
write.reset();
|
|
|
|
wsl::windows::common::SubProcess process(nullptr, LxssGenerateWslCommandLine(L"-d end2end").c_str());
|
|
process.SetStdHandles(read.get(), nullptr, nullptr);
|
|
|
|
auto oobeResult = process.RunAndCaptureOutput();
|
|
VERIFY_ARE_EQUAL(oobeResult.Stdout, L"");
|
|
VERIFY_ARE_EQUAL(oobeResult.Stderr, L"");
|
|
VERIFY_ARE_EQUAL(oobeResult.ExitCode, 0);
|
|
}
|
|
|
|
// Validate UID
|
|
auto [output, error] = LxsstuLaunchWslAndCaptureOutput(L"-d end2end id -u");
|
|
VERIFY_ARE_EQUAL(output, L"1011\n");
|
|
VERIFY_ARE_EQUAL(error, L"");
|
|
}
|
|
|
|
TEST_METHOD(DistroTarFormats)
|
|
{
|
|
auto version = LxsstuVmMode() ? L"2" : L"1";
|
|
|
|
auto convert = [](LPCWSTR Command, LPCWSTR FileName) {
|
|
const wil::unique_handle output{CreateFile(FileName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, 0, nullptr)};
|
|
VERIFY_IS_TRUE(!!output);
|
|
|
|
wsl::windows::common::helpers::SetHandleInheritable(output.get());
|
|
|
|
LxsstuLaunchWsl(std::format(L"xz -d -c $(wslpath '{}') | {}", g_testDistroPath, Command), nullptr, output.get());
|
|
|
|
return wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [FileName]() { std::filesystem::remove(FileName); });
|
|
};
|
|
|
|
auto importAndTest = [&version](LPCWSTR FileName) {
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [FileName]() { LxsstuLaunchWsl(L"--unregister test-format"); });
|
|
LxsstuLaunchWsl(std::format(L"--install --no-launch --from-file {} --name test-format --version {}", FileName, version));
|
|
|
|
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"-d test-format echo OK");
|
|
VERIFY_ARE_EQUAL(out, L"OK\n");
|
|
};
|
|
|
|
// Tar bz2
|
|
{
|
|
auto cleanup = convert(L"bzip2", L"test-distro.tar.bz2");
|
|
importAndTest(L"test-distro.tar.bz2");
|
|
}
|
|
|
|
// Tar gz
|
|
{
|
|
auto cleanup = convert(L"gzip", L"test-distro.tar.gz");
|
|
importAndTest(L"test-distro.tar.gz");
|
|
}
|
|
|
|
// N.B. tar xz is already covered since it's the format of the test distro.
|
|
VERIFY_IS_TRUE(wsl::shared::string::EndsWith(g_testDistroPath, std::wstring_view{L".xz"}));
|
|
}
|
|
|
|
TEST_METHOD(InnerCommandLineParsing)
|
|
{
|
|
using namespace wsl::windows::common;
|
|
using namespace wsl::shared;
|
|
|
|
constexpr auto entryPoint = L"dummy";
|
|
|
|
auto parse = [&](ArgumentParser& Parser, LPCWSTR ExpectedError = nullptr) {
|
|
const ExecutionContext context(Context::Wsl);
|
|
std::optional<std::wstring> error;
|
|
|
|
try
|
|
{
|
|
Parser.Parse();
|
|
}
|
|
catch (...)
|
|
{
|
|
if (context.ReportedError().has_value())
|
|
{
|
|
error = wslutil::ErrorToString(context.ReportedError().value()).Message;
|
|
}
|
|
else
|
|
{
|
|
error = wslutil::ErrorCodeToString(wil::ResultFromCaughtException());
|
|
THROW_HR(wil::ResultFromCaughtException());
|
|
}
|
|
}
|
|
|
|
if (error.has_value())
|
|
{
|
|
VERIFY_ARE_EQUAL(ExpectedError, error.value());
|
|
}
|
|
else
|
|
{
|
|
VERIFY_IS_NULL(ExpectedError);
|
|
}
|
|
};
|
|
|
|
{
|
|
ArgumentParser parser(L"--a b --c d pos-value", entryPoint, 0);
|
|
std::wstring a;
|
|
std::wstring c;
|
|
std::wstring e;
|
|
std::wstring pos;
|
|
parser.AddArgument(a, L"--a");
|
|
parser.AddArgument(c, L"--c");
|
|
parser.AddArgument(e, L"--e");
|
|
parser.AddPositionalArgument(pos, 0);
|
|
|
|
parse(parser);
|
|
|
|
VERIFY_ARE_EQUAL(a, L"b");
|
|
VERIFY_ARE_EQUAL(c, L"d");
|
|
VERIFY_ARE_EQUAL(pos, L"pos-value");
|
|
VERIFY_ARE_EQUAL(e, L"");
|
|
}
|
|
|
|
{
|
|
ArgumentParser parser(L"--a b -- --c", entryPoint, 0);
|
|
std::wstring a;
|
|
std::wstring c;
|
|
std::wstring e;
|
|
std::wstring pos;
|
|
parser.AddArgument(a, L"--a");
|
|
parser.AddArgument(e, L"--e");
|
|
parser.AddPositionalArgument(pos, 0);
|
|
|
|
parse(parser);
|
|
|
|
VERIFY_ARE_EQUAL(a, L"b");
|
|
VERIFY_ARE_EQUAL(pos, L"--c");
|
|
VERIFY_ARE_EQUAL(e, L"");
|
|
}
|
|
|
|
{
|
|
GUID expectedGuid = {0x12345678, 0x1234, 0x1234, {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}};
|
|
auto commandLine = std::format(
|
|
L"--flag b --arg value pos-arg2 pos-arg3 --flag3 --flag4 value4 --guid {}",
|
|
wsl::shared::string::GuidToString<wchar_t>(expectedGuid));
|
|
|
|
ArgumentParser parser(commandLine.c_str(), entryPoint, 0);
|
|
bool flag{};
|
|
std::wstring arg;
|
|
std::wstring pos1;
|
|
std::wstring pos2;
|
|
std::wstring pos3;
|
|
bool flag3{};
|
|
std::wstring value4;
|
|
bool dummy{};
|
|
GUID parsedGuid;
|
|
|
|
parser.AddArgument(flag, L"--flag");
|
|
parser.AddArgument(arg, L"--arg");
|
|
parser.AddPositionalArgument(pos1, 0);
|
|
parser.AddPositionalArgument(pos2, 1);
|
|
parser.AddPositionalArgument(pos3, 2);
|
|
parser.AddArgument(flag3, L"--flag3");
|
|
parser.AddArgument(value4, L"--flag4");
|
|
parser.AddArgument(dummy, L"--dummy");
|
|
parser.AddArgument(parsedGuid, L"--guid");
|
|
|
|
parse(parser);
|
|
|
|
VERIFY_IS_TRUE(flag);
|
|
VERIFY_ARE_EQUAL(arg, L"value");
|
|
VERIFY_ARE_EQUAL(pos1, L"b");
|
|
VERIFY_ARE_EQUAL(pos2, L"pos-arg2");
|
|
VERIFY_ARE_EQUAL(pos3, L"pos-arg3");
|
|
VERIFY_IS_TRUE(flag3);
|
|
VERIFY_ARE_EQUAL(L"value4", value4);
|
|
VERIFY_IS_FALSE(dummy);
|
|
VERIFY_ARE_EQUAL(expectedGuid, parsedGuid);
|
|
}
|
|
|
|
{
|
|
ArgumentParser parser(L"--a", entryPoint, 0);
|
|
std::wstring a;
|
|
parser.AddArgument(a, L"--a");
|
|
|
|
parse(
|
|
parser,
|
|
std::format(
|
|
L"Command line argument --a requires a value.\n"
|
|
"Please use '{} --help' to get a list of supported arguments.",
|
|
entryPoint)
|
|
.c_str());
|
|
}
|
|
|
|
{
|
|
ArgumentParser parser(L"--does-not-exist --a b -- --c", entryPoint, 0);
|
|
parser.AddArgument(NoOp{}, L"--a");
|
|
parser.AddArgument(NoOp{}, L"--e");
|
|
parser.AddPositionalArgument(NoOp{}, 0);
|
|
|
|
parse(
|
|
parser,
|
|
std::format(
|
|
L"Invalid command line argument: --does-not-exist\n"
|
|
"Please use '{} --help' to get a list of supported arguments.",
|
|
entryPoint)
|
|
.c_str());
|
|
}
|
|
|
|
{
|
|
ArgumentParser parser(L"--guid foo", entryPoint, 0);
|
|
GUID guid;
|
|
parser.AddArgument(guid, L"--guid");
|
|
|
|
parse(parser, L"Invalid GUID format: 'foo'");
|
|
}
|
|
|
|
{
|
|
ArgumentParser parser(L"-abc pos-value", entryPoint, 0);
|
|
bool aLong{};
|
|
bool a{};
|
|
bool b{};
|
|
bool c{};
|
|
bool d{};
|
|
std::wstring pos;
|
|
|
|
parser.AddArgument(aLong, L"--a");
|
|
parser.AddArgument(a, nullptr, 'a');
|
|
parser.AddArgument(b, nullptr, 'b');
|
|
parser.AddArgument(c, nullptr, 'c');
|
|
parser.AddArgument(d, nullptr, 'd');
|
|
parser.AddPositionalArgument(pos, 0);
|
|
|
|
parse(parser);
|
|
|
|
VERIFY_IS_TRUE(a);
|
|
VERIFY_IS_TRUE(b);
|
|
VERIFY_IS_TRUE(c);
|
|
VERIFY_IS_FALSE(d);
|
|
VERIFY_IS_FALSE(aLong);
|
|
VERIFY_ARE_EQUAL(pos, L"pos-value");
|
|
}
|
|
|
|
{
|
|
ArgumentParser parser(L"-abc", entryPoint, 0);
|
|
|
|
parser.AddArgument(NoOp{}, nullptr, 'a');
|
|
parser.AddArgument(NoOp{}, nullptr, 'c');
|
|
|
|
parse(
|
|
parser,
|
|
std::format(
|
|
L"Invalid command line argument: -abc\n"
|
|
"Please use '{} --help' to get a list of supported arguments.",
|
|
entryPoint)
|
|
.c_str());
|
|
}
|
|
|
|
{
|
|
ArgumentParser parser(L"- --", entryPoint, 0);
|
|
|
|
parse(
|
|
parser,
|
|
std::format(
|
|
L"Invalid command line argument: -\n"
|
|
"Please use '{} --help' to get a list of supported arguments.",
|
|
entryPoint)
|
|
.c_str());
|
|
}
|
|
|
|
{
|
|
ArgumentParser parser(L"--foo -", entryPoint, 0);
|
|
bool a{};
|
|
std::wstring pos;
|
|
|
|
parser.AddArgument(a, L"--foo");
|
|
parser.AddPositionalArgument(pos, 0);
|
|
|
|
parse(parser);
|
|
VERIFY_IS_TRUE(a);
|
|
VERIFY_ARE_EQUAL(pos, L"-");
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(CaseSensitivity)
|
|
{
|
|
auto setCaseSensitivity = [](const std::wstring& Path, bool enable) {
|
|
auto cmd = std::format(L"fsutil.exe file setCaseSensitiveInfo \"{}\" {}", Path.c_str(), enable ? L"enable" : L"disable");
|
|
LxsstuLaunchCommandAndCaptureOutput(cmd.data());
|
|
};
|
|
|
|
auto getCaseSensitivity = [](const std::wstring& Path) {
|
|
auto cmd = std::format(L"fsutil.exe file queryCaseSensitiveInfo \"{}\"", Path);
|
|
auto [out, _] = LxsstuLaunchCommandAndCaptureOutput(cmd.data());
|
|
if (out.find(L"is disabled") != std::string::npos)
|
|
{
|
|
return false;
|
|
}
|
|
else if (out.find(L"is enabled") != std::string::npos)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
LogError("Failed to parse fsutil output: %ls", out.c_str());
|
|
VERIFY_FAIL();
|
|
|
|
return true;
|
|
};
|
|
|
|
constexpr auto testDir = L"case-test";
|
|
constexpr auto flags = wsl::windows::common::filesystem::c_case_sensitive_folders_only | LXSS_CREATE_INSTANCE_FLAGS_ALLOW_FS_UPGRADE;
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { std::filesystem::remove_all(testDir); });
|
|
|
|
std::filesystem::create_directories(testDir);
|
|
setCaseSensitivity(testDir, false);
|
|
VERIFY_IS_FALSE(getCaseSensitivity(testDir));
|
|
|
|
wsl::windows::common::filesystem::EnsureCaseSensitiveDirectory(testDir, flags);
|
|
VERIFY_IS_TRUE(getCaseSensitivity(testDir));
|
|
setCaseSensitivity(testDir, false);
|
|
|
|
std::filesystem::create_directories(std::format(L"{}/l1/l2/l3", testDir));
|
|
setCaseSensitivity(std::format(L"{}/l1/l2/l3", testDir), false);
|
|
setCaseSensitivity(std::format(L"{}/l1/l2", testDir), false);
|
|
|
|
std::filesystem::create_directories(std::format(L"{}/l1/l2/l3-other", testDir));
|
|
setCaseSensitivity(std::format(L"{}/l1/l2/l3-other", testDir), false);
|
|
|
|
VERIFY_IS_FALSE(getCaseSensitivity(std::format(L"{}/l1/l2", testDir)));
|
|
VERIFY_IS_FALSE(getCaseSensitivity(std::format(L"{}/l1/l2/l3", testDir)));
|
|
VERIFY_IS_FALSE(getCaseSensitivity(std::format(L"{}/l1/l2/l3-other", testDir)));
|
|
|
|
wsl::windows::common::filesystem::EnsureCaseSensitiveDirectory(testDir, flags);
|
|
|
|
VERIFY_IS_TRUE(getCaseSensitivity(std::format(L"{}/l1/l2/l3", testDir)));
|
|
VERIFY_IS_TRUE(getCaseSensitivity(std::format(L"{}/l1/l2/l3-other", testDir)));
|
|
VERIFY_IS_TRUE(getCaseSensitivity(std::format(L"{}/l1/l2", testDir)));
|
|
VERIFY_IS_TRUE(getCaseSensitivity(std::format(L"{}/l1", testDir)));
|
|
VERIFY_IS_TRUE(getCaseSensitivity(testDir));
|
|
}
|
|
|
|
TEST_METHOD(AutomountRespectedWithElevation)
|
|
{
|
|
DistroFileChange distributionconf(L"/etc/wsl.conf", false);
|
|
distributionconf.SetContent(L"[automount]\nenabled=false\n");
|
|
|
|
DistroFileChange distributionFstab(L"/etc/fstab", false);
|
|
distributionFstab.SetContent(L"");
|
|
TerminateDistribution();
|
|
|
|
const auto nonElevatedToken = GetNonElevatedToken();
|
|
VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(L"echo dummy", nullptr, nullptr, nullptr, nonElevatedToken.get()));
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"mountpoint /mnt/c", 32u);
|
|
VERIFY_ARE_EQUAL(out, L"/mnt/c is not a mountpoint\n");
|
|
}
|
|
|
|
TEST_METHOD(FstabRespectedWithElevationAndAutomountDisabled)
|
|
{
|
|
DistroFileChange distributionconf(L"/etc/wsl.conf", false);
|
|
distributionconf.SetContent(L"[automount]\nenabled=false\n");
|
|
|
|
DistroFileChange distributionFstab(L"/etc/fstab", false);
|
|
distributionFstab.SetContent(L"C:\\\\ /mnt/c drvfs metadata 0 0");
|
|
|
|
TerminateDistribution();
|
|
|
|
const auto nonElevatedToken = GetNonElevatedToken();
|
|
|
|
VERIFY_ARE_EQUAL(0u, LxsstuLaunchWsl(L"echo dummy", nullptr, nullptr, nullptr, nonElevatedToken.get()));
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"mountpoint /mnt/c", 0u);
|
|
VERIFY_ARE_EQUAL(out, L"/mnt/c is a mountpoint\n");
|
|
}
|
|
|
|
// This test case validates that the pipeline doesn't get stuck when both stdout & stdin are a pipe.
|
|
// See: https://github.com/microsoft/WSL/issues/12523
|
|
TEST_METHOD(DualPipeRelay)
|
|
{
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { DeleteFile(L"compressed.gz"); });
|
|
|
|
wsl::windows::common::SubProcess process{
|
|
nullptr, L"cmd /c type \"C:\\Program Files\\WSL\\wsl.exe\" | wsl gzip > compressed.gz"};
|
|
|
|
VERIFY_ARE_EQUAL(process.Run(), 0L);
|
|
|
|
wil::unique_handle file{CreateFile(L"compressed.gz", GENERIC_READ, 0, nullptr, OPEN_EXISTING, 0, nullptr)};
|
|
VERIFY_IS_TRUE(!!file);
|
|
|
|
wsl::windows::common::helpers::SetHandleInheritable(file.get());
|
|
|
|
// Validate that the relay didn't get stuck, and that its output is correct.
|
|
auto [expandedHash, _] = LxsstuLaunchWslAndCaptureOutput(L"gzip -d -| md5sum -", 0, file.get());
|
|
auto [expectedHash, __] =
|
|
LxsstuLaunchWslAndCaptureOutput(L"cat \"$(wslpath 'C:\\Program Files\\WSL\\wsl.exe')\" | md5sum - ");
|
|
|
|
VERIFY_ARE_EQUAL(expandedHash, expectedHash);
|
|
}
|
|
|
|
TEST_METHOD(EtcHosts)
|
|
{
|
|
{
|
|
// Verify that setting network.generateHosts=false doesn't create /etc/hosts
|
|
|
|
DistroFileChange wslConf{L"/etc/wsl.conf", false};
|
|
wslConf.SetContent(L"[network]\ngenerateHosts=false");
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"rm /etc/hosts"), 0L);
|
|
|
|
TerminateDistribution();
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"! test -f /etc/hosts"), 0L);
|
|
}
|
|
|
|
{
|
|
// Verify that /etc/hosts generation is correct.
|
|
TerminateDistribution();
|
|
|
|
auto [content, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /etc/hosts");
|
|
auto [hostname, domain] = wsl::windows::common::filesystem::GetHostAndDomainNames();
|
|
|
|
const auto lines = wsl::shared::string::Split(content, L'\n');
|
|
VERIFY_IS_TRUE(lines.size() > 4);
|
|
VERIFY_ARE_EQUAL(lines[0] + L"\n", WIDEN(LX_INIT_AUTO_GENERATED_FILE_HEADER));
|
|
VERIFY_ARE_EQUAL(lines[1], L"# [network]");
|
|
VERIFY_ARE_EQUAL(lines[2], L"# generateHosts = false");
|
|
VERIFY_ARE_EQUAL(lines[3], L"127.0.0.1\tlocalhost");
|
|
VERIFY_ARE_EQUAL(lines[4], std::format(L"127.0.1.1\t{}.{}\t{}", hostname, domain, hostname));
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ExecEmptyArg)
|
|
{
|
|
// See: https://github.com/microsoft/WSL/issues/12649
|
|
|
|
{
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"--exec echo \"\"");
|
|
VERIFY_ARE_EQUAL(out, L"\n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
|
|
{
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"--exec echo foo \"\" bar");
|
|
VERIFY_ARE_EQUAL(out, L"foo bar\n"); // Two spaces because echo adds one between each argument.
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(DistroTimeout)
|
|
{
|
|
WslConfigChange config(LxssGenerateTestConfig() + L"[general]\ninstanceIdleTimeout=-1");
|
|
auto distroId = GetDistributionId(LXSS_DISTRO_NAME_TEST_L);
|
|
|
|
auto getDistroState = [&]() {
|
|
wsl::windows::common::SvcComm service;
|
|
|
|
for (const auto& e : service.EnumerateDistributions())
|
|
{
|
|
if (wsl::shared::string::IsEqual(e.DistroName, LXSS_DISTRO_NAME_TEST_L))
|
|
{
|
|
return e.State;
|
|
}
|
|
}
|
|
|
|
return LxssDistributionStateInvalid;
|
|
};
|
|
|
|
// Validate that distributions don't time out when timeout is -1
|
|
{
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"echo OK"), 0L);
|
|
|
|
std::this_thread::sleep_for(std::chrono::seconds(20));
|
|
VERIFY_ARE_EQUAL(getDistroState(), LxssDistributionStateRunning);
|
|
}
|
|
|
|
// Validate that distributions time out when timeout value is > 0
|
|
{
|
|
config.Update(LxssGenerateTestConfig() + L"[general]\ninstanceIdleTimeout=2000");
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"echo OK"), 0L);
|
|
|
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::minutes(1);
|
|
|
|
unsigned long iterations = 0;
|
|
while (std::chrono::steady_clock::now() < deadline)
|
|
{
|
|
if (getDistroState() == LxssDistributionStateInstalled)
|
|
{
|
|
LogInfo("Distribution stopped after %lu iterations", iterations);
|
|
return;
|
|
}
|
|
|
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
|
iterations++;
|
|
}
|
|
|
|
LogError("Distribution failed to time out after %lu iterations. State: %i", iterations, getDistroState());
|
|
VERIFY_FAIL();
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(WslUpdate)
|
|
{
|
|
// Test the regular wsl --update logic
|
|
{
|
|
auto json =
|
|
LR"(
|
|
{
|
|
"name": "2.4.12",
|
|
"assets": [
|
|
{
|
|
"url": "http://arm-url",
|
|
"id": 1,
|
|
"name": "wsl.2.4.12.0.arm64.msi"
|
|
},
|
|
{
|
|
"url": "http://x64-url",
|
|
"id": 2,
|
|
"name": "wsl.2.4.12.0.x64.msi"
|
|
}]})";
|
|
|
|
auto [version, asset] = wsl::windows::common::wslutil::GetLatestGitHubRelease(false, json);
|
|
|
|
VERIFY_ARE_EQUAL(version, L"2.4.12");
|
|
VERIFY_ARE_EQUAL(asset.id, 2);
|
|
VERIFY_ARE_EQUAL(asset.url, L"http://x64-url");
|
|
VERIFY_ARE_EQUAL(asset.name, L"wsl.2.4.12.0.x64.msi");
|
|
}
|
|
|
|
// Test wsl --update --pre-release
|
|
{
|
|
auto json =
|
|
LR"([
|
|
{
|
|
"name": "2.4.12"
|
|
},
|
|
{
|
|
"name": "2.5.1",
|
|
"assets": [
|
|
{
|
|
"url": "http://arm-url",
|
|
"id": 1,
|
|
"name": "wsl.2.5.1.0.arm64.msi"
|
|
},
|
|
{
|
|
"url": "http://x64-url",
|
|
"id": 2,
|
|
"name": "wsl.2.5.1.0.x64.msi"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "2.4.13"
|
|
}])";
|
|
|
|
auto [version, asset] = wsl::windows::common::wslutil::GetLatestGitHubRelease(true, json);
|
|
|
|
VERIFY_ARE_EQUAL(version, L"2.5.1");
|
|
VERIFY_ARE_EQUAL(asset.id, 2);
|
|
VERIFY_ARE_EQUAL(asset.url, L"http://x64-url");
|
|
VERIFY_ARE_EQUAL(asset.name, L"wsl.2.5.1.0.x64.msi");
|
|
}
|
|
}
|
|
|
|
WSL2_TEST_METHOD(CustomModulesVhd)
|
|
{
|
|
#ifdef WSL_DEV_INSTALL_PATH
|
|
|
|
auto modulesPath = std::format(L"{}\\modules.vhd", WSL_DEV_INSTALL_PATH);
|
|
auto kernelPath = std::format(L"{}\\kernel", WSL_DEV_INSTALL_PATH);
|
|
|
|
#else
|
|
auto modulesPath = std::format(L"{}\\tools\\modules.vhd", wsl::windows::common::wslutil::GetMsiPackagePath().value());
|
|
auto kernelPath = std::format(L"{}\\tools\\kernel", wsl::windows::common::wslutil::GetMsiPackagePath().value());
|
|
|
|
#endif
|
|
|
|
// Create a copy of the modules vhd
|
|
auto testModules = std::filesystem::current_path() / "test-modules.vhd";
|
|
|
|
VERIFY_IS_TRUE(CopyFile(modulesPath.c_str(), testModules.c_str(), false));
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove(testModules); });
|
|
|
|
auto cmd = std::format(
|
|
LR"($acl = Get-Acl '{}' ; $acl.RemoveAccessRuleAll((New-Object System.Security.AccessControl.FileSystemAccessRule(\"Everyone\", \"Read\", \"None\", \"None\", \"Allow\"))); Set-Acl -Path '{}' -AclObject $acl)",
|
|
testModules,
|
|
testModules);
|
|
|
|
LxsstuLaunchPowershellAndCaptureOutput(cmd);
|
|
|
|
// Update .wslconfig to point to the copied kernel
|
|
WslConfigChange config{LxssGenerateTestConfig({.kernel = kernelPath, .kernelModules = testModules.wstring()})};
|
|
|
|
// Validate that WSL starts correctly
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"echo OK");
|
|
VERIFY_ARE_EQUAL(out, L"OK\n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
|
|
TEST_METHOD(BrokenDistroImport)
|
|
{ // Validate that importing an empty tar fails.
|
|
{
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"--import broken-test-distro . NUL", -1);
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
out,
|
|
L"The imported file is not a valid Linux distribution.\r\nError code: "
|
|
L"Wsl/Service/RegisterDistro/WSL_E_NOT_A_LINUX_DISTRO\r\n");
|
|
|
|
// TODO: Uncomment once SetVersionDebug is removed from the tests .wslconfig.
|
|
// VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
|
|
// Validate that importing an empty tar via wsl --install fails.
|
|
{
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"--install --from-file NUL --name broken-test-distro", -1);
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
out,
|
|
L"Installing: NUL\r\nThe imported file is not a valid Linux distribution.\r\nError code: "
|
|
L"Wsl/Service/RegisterDistro/WSL_E_NOT_A_LINUX_DISTRO\r\n");
|
|
// TODO: Uncomment once SetVersionDebug is removed from the tests .wslconfig.
|
|
// VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
|
|
// Validate that importing an empty VHDX fails.
|
|
if (LxsstuVmMode())
|
|
{
|
|
constexpr auto testVhd = L"EmptyVhd.vhdx";
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { DeleteFile(testVhd); });
|
|
|
|
LxsstuLaunchPowershellAndCaptureOutput(std::format(L"New-Vhd {} -SizeBytes 20MB", testVhd));
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--mount {} --vhd --bare", testVhd)), 0L);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"mkfs.ext4 /dev/sde"), 0L);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unmount"), 0L);
|
|
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--import-in-place broken-test-distro {}", testVhd), -1);
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
out,
|
|
L"The imported file is not a valid Linux distribution.\r\nError code: "
|
|
L"Wsl/Service/RegisterDistro/WSL_E_NOT_A_LINUX_DISTRO\r\n");
|
|
// TODO: Uncomment once SetVersionDebug is removed from the tests .wslconfig.
|
|
// VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
|
|
// Validate that tars containing /etc, but not /bin/sh are accepted.
|
|
if (LxsstuVmMode())
|
|
{
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { LxsstuLaunchWsl(L"--unregister empty-distro"); });
|
|
|
|
DistroFileChange conf(L"/etc/wsl.conf", false);
|
|
conf.SetContent(L"");
|
|
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(
|
|
L"tar cf - /etc/wsl.conf | wsl.exe --install --from-file - --name empty-distro --no-launch "
|
|
L"--version 2");
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ImportExportStdout)
|
|
{
|
|
constexpr auto test_distro = L"import-test-distro";
|
|
auto cleanup = wil::scope_exit_log(
|
|
WI_DIAGNOSTICS_INFO, [test_distro]() { LxsstuLaunchWsl(std::format(L"--unregister {}", test_distro)); });
|
|
|
|
// The below logline makes it easier to find the bsdtar output when debugging this test case.
|
|
fprintf(stderr, "Starting ImportExportStdout test case\n");
|
|
|
|
auto commandLine = std::format(L"cmd.exe /c wsl --export {} - | wsl --import {} . -", LXSS_DISTRO_NAME_TEST_L, test_distro);
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuRunCommand(commandLine.data()), 0L);
|
|
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo ok", test_distro));
|
|
VERIFY_ARE_EQUAL(out, L"ok\n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
|
|
TEST_METHOD(EtcHostsParsing)
|
|
{
|
|
constexpr auto inputFileName = L"test-etc-hosts.txt";
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { DeleteFile(inputFileName); });
|
|
|
|
auto validate = [](const std::string& Input, const std::string& ExpectedOutput) {
|
|
wil::unique_handle inputFile{CreateFile(inputFileName, GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, 0, nullptr)};
|
|
|
|
VERIFY_IS_TRUE(WriteFile(inputFile.get(), Input.c_str(), static_cast<DWORD>(Input.size()), nullptr, nullptr));
|
|
|
|
auto output = wsl::windows::common::filesystem::GetWindowsHosts(inputFileName);
|
|
|
|
VERIFY_ARE_EQUAL(ExpectedOutput, output);
|
|
};
|
|
|
|
validate("127.0.0.1 microsoft.com", "127.0.0.1\tmicrosoft.com\n");
|
|
validate("\xEF\xBB\xBF 127.0.0.1 microsoft.com", "127.0.0.1\tmicrosoft.com\n"); // Validate that BOM headers are ignored.
|
|
validate("#Comment 127.0.0.1 microsoft.com windows.microsoft.com\n#AnotherComment", "");
|
|
validate(
|
|
"#Comment 127.0.0.1 microsoft.com windows.microsoft.com\n#AnotherComment\n127.0.0.1 wsl.dev", "127.0.0.1\twsl.dev\n");
|
|
}
|
|
|
|
// Validate that a distribution can be unregistered even if its BasePath doesn't exist.
|
|
// See https://github.com/microsoft/WSL/issues/13004
|
|
TEST_METHOD(BrokenDistroUnregister)
|
|
{
|
|
const auto userKey = wsl::windows::common::registry::OpenLxssUserKey();
|
|
const auto distroKey = wsl::windows::common::registry::CreateKey(userKey.get(), L"{baa405ef-1822-4bbe-84e2-30e4c6330d42}");
|
|
|
|
auto revert = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] {
|
|
wsl::windows::common::registry::DeleteKey(userKey.get(), L"{baa405ef-1822-4bbe-84e2-30e4c6330d42}");
|
|
});
|
|
|
|
wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"BasePath", L"C:\\DoesNotExit");
|
|
wsl::windows::common::registry::WriteString(distroKey.get(), nullptr, L"DistributionName", L"DummyBrokenDistro");
|
|
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"DefaultUid", 0);
|
|
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"Version", LXSS_DISTRO_VERSION_2);
|
|
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"State", LxssDistributionStateInstalled);
|
|
wsl::windows::common::registry::WriteDword(distroKey.get(), nullptr, L"Flags", LXSS_DISTRO_FLAGS_VM_MODE);
|
|
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"--unregister DummyBrokenDistro");
|
|
|
|
VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
|
|
// Validate that calling the binfmt interpreter with tty fd's but not controlling terminal doesn't display a warning.
|
|
// See https://github.com/microsoft/WSL/issues/13173.
|
|
TEST_METHOD(SetSidNoWarning)
|
|
{
|
|
auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"socat - 'EXEC:setsid --wait cmd.exe /c echo OK',pty,setsid,stderr");
|
|
|
|
VERIFY_ARE_EQUAL(out, L"OK\r\r\n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
|
|
WSL2_TEST_METHOD(WslDebug)
|
|
{
|
|
// Verify that hvsocket debug events are logged to dmesg.
|
|
WslConfigChange config(LxssGenerateTestConfig({.kernelCommandLine = L"WSL_DEBUG=hvsocket"}));
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"dmesg | grep -iF 'vmbus_send_tl_connect_request'"), 0L);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(CGroupv1)
|
|
{
|
|
auto expectedMount = [](const char* path, const wchar_t* expected) {
|
|
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(std::format(L"findmnt -ln '{}' || true", path));
|
|
|
|
VERIFY_ARE_EQUAL(out, expected);
|
|
};
|
|
|
|
// Validate that cgroupv2 is mounted by default.
|
|
expectedMount("/sys/fs/cgroup", L"/sys/fs/cgroup cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate\n");
|
|
|
|
// Validate that setting cgroup=v1 causes unified cgroups to be mounted.
|
|
DistroFileChange wslConf(L"/etc/wsl.conf", false);
|
|
wslConf.SetContent(L"[automount]\ncgroups=v1");
|
|
|
|
TerminateDistribution();
|
|
|
|
expectedMount(
|
|
"/sys/fs/cgroup/unified", L"/sys/fs/cgroup/unified cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate\n");
|
|
|
|
// Validate that the cgroupv1 mounts are present.
|
|
expectedMount("/sys/fs/cgroup/cpu", L"/sys/fs/cgroup/cpu cgroup cgroup rw,nosuid,nodev,noexec,relatime,cpu\n");
|
|
|
|
// Validate that having cgroup_no_v1=all causes the distribution to fall back to v2.
|
|
WslConfigChange wslConfig(LxssGenerateTestConfig({.kernelCommandLine = L"cgroup_no_v1=all"}));
|
|
|
|
expectedMount("/sys/fs/cgroup/unified", L"");
|
|
expectedMount("/sys/fs/cgroup", L"/sys/fs/cgroup cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate\n");
|
|
|
|
auto [dmesg, __] = LxsstuLaunchWslAndCaptureOutput(L"dmesg");
|
|
VERIFY_ARE_NOT_EQUAL(
|
|
dmesg.find(
|
|
L"Distribution has cgroupv1 enabled, but kernel command line has cgroup_no_v1=all. Falling back to cgroupv2"),
|
|
std::wstring::npos);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(InitPermissions)
|
|
{
|
|
auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"stat -c %a /init");
|
|
|
|
VERIFY_ARE_EQUAL(out, L"755\n");
|
|
}
|
|
|
|
WSL2_TEST_METHOD(ExportImportVhd)
|
|
{
|
|
WslShutdown();
|
|
|
|
constexpr auto vhdPath = L"exported-test-distro.vhd";
|
|
constexpr auto vhdxPath = L"exported-test-distro.vhdx";
|
|
constexpr auto exportedVhdPath = L"exported-vhd.vhd";
|
|
constexpr auto newDistroName = L"imported-test-distro";
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
LOG_IF_WIN32_BOOL_FALSE(DeleteFile(vhdPath));
|
|
LOG_IF_WIN32_BOOL_FALSE(DeleteFile(vhdxPath));
|
|
LOG_IF_WIN32_BOOL_FALSE(DeleteFile(exportedVhdPath));
|
|
LxsstuLaunchWsl(std::format(L"--unregister {}", newDistroName));
|
|
});
|
|
|
|
// Attempt to export the distribution to a .vhd (should fail).
|
|
auto [out, err] =
|
|
LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format vhd", LXSS_DISTRO_NAME_TEST_L, vhdPath), -1);
|
|
VERIFY_ARE_EQUAL(
|
|
out, L"The specified file must have the .vhdx file extension.\r\nError code: Wsl/Service/WSL_E_EXPORT_FAILED\r\n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
|
|
// Export the distribution to a .vhdx.
|
|
std::tie(out, err) =
|
|
LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format vhd", LXSS_DISTRO_NAME_TEST_L, vhdxPath));
|
|
VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
|
|
// Convert the .vhdx to .vhd.
|
|
LxsstuLaunchPowershellAndCaptureOutput(std::format(L"Convert-VHD -Path '{}' -DestinationPath '{}'", vhdxPath, vhdPath));
|
|
|
|
// Import a new distribution from the .vhd file.
|
|
std::tie(out, err) =
|
|
LxsstuLaunchWslAndCaptureOutput(std::format(L"--import {} {} {} --vhd", newDistroName, newDistroName, vhdPath));
|
|
VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
|
|
// Export the newly imported distribution to another .vhd file.
|
|
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format vhd", newDistroName, exportedVhdPath));
|
|
VERIFY_ARE_EQUAL(out, L"The operation completed successfully. \r\n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
|
|
// Attempt to export to a .vhdx (should fail).
|
|
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--export {} {} --format vhd", newDistroName, vhdxPath), -1);
|
|
VERIFY_ARE_EQUAL(
|
|
out, L"The specified file must have the .vhd file extension.\r\nError code: Wsl/Service/WSL_E_EXPORT_FAILED\r\n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
|
|
// Attempt to import to a non VHD file.
|
|
auto tempFile = wsl::windows::common::filesystem::TempFile(
|
|
GENERIC_ALL, 0, CREATE_ALWAYS, wsl::windows::common::filesystem::TempFileFlags::None, L"txt");
|
|
|
|
tempFile.Handle.reset();
|
|
|
|
constexpr auto negativeVariationDistro = L"negative-variation-distro";
|
|
std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(
|
|
std::format(L"--import {} {} {} --vhd", negativeVariationDistro, negativeVariationDistro, tempFile.Path), -1);
|
|
VERIFY_ARE_EQUAL(
|
|
out,
|
|
L"The specified file must have the .vhd or .vhdx file extension.\r\nError code: "
|
|
L"Wsl/Service/RegisterDistro/WSL_E_IMPORT_FAILED\r\n");
|
|
VERIFY_ARE_EQUAL(err, L"");
|
|
}
|
|
|
|
TEST_METHOD(BytesToHex)
|
|
{
|
|
using wsl::windows::common::string::BytesToHex;
|
|
|
|
VERIFY_ARE_EQUAL(BytesToHex({}), L"0x");
|
|
VERIFY_ARE_EQUAL(BytesToHex({0x0F}), L"0x0f");
|
|
VERIFY_ARE_EQUAL(BytesToHex({0xDE, 0xAD, 0xBE, 0xEF}), L"0xdeadbeef");
|
|
VERIFY_ARE_EQUAL(BytesToHex({0x00, 0x00}), L"0x0000");
|
|
VERIFY_ARE_EQUAL(BytesToHex({0xFF, 0xFF}), L"0xffff");
|
|
}
|
|
|
|
TEST_METHOD(HexToBytes)
|
|
{
|
|
using wsl::windows::common::string::BytesToHex;
|
|
using wsl::windows::common::string::HexToBytes;
|
|
using ByteVec = std::vector<BYTE>;
|
|
|
|
// Wide string with 0x prefix
|
|
VERIFY_ARE_EQUAL(HexToBytes(L"0xdeadbeef"), (ByteVec{0xDE, 0xAD, 0xBE, 0xEF}));
|
|
|
|
// Narrow string with 0x prefix
|
|
VERIFY_ARE_EQUAL(HexToBytes("0xdeadbeef"), (ByteVec{0xDE, 0xAD, 0xBE, 0xEF}));
|
|
|
|
// Wide string without prefix
|
|
VERIFY_ARE_EQUAL(HexToBytes(L"deadbeef"), (ByteVec{0xDE, 0xAD, 0xBE, 0xEF}));
|
|
|
|
// Narrow string without prefix
|
|
VERIFY_ARE_EQUAL(HexToBytes("deadbeef"), (ByteVec{0xDE, 0xAD, 0xBE, 0xEF}));
|
|
|
|
// Empty string
|
|
VERIFY_ARE_EQUAL(HexToBytes(L""), (ByteVec{}));
|
|
|
|
// Single byte
|
|
VERIFY_ARE_EQUAL(HexToBytes(L"0x0f"), (ByteVec{0x0F}));
|
|
|
|
// Uppercase hex digits
|
|
VERIFY_ARE_EQUAL(HexToBytes(L"0xDEADBEEF"), (ByteVec{0xDE, 0xAD, 0xBE, 0xEF}));
|
|
|
|
// Round-trip: BytesToHex -> HexToBytes
|
|
const ByteVec original = {0x01, 0x23, 0xAB};
|
|
VERIFY_ARE_EQUAL(HexToBytes(BytesToHex(original)), original);
|
|
|
|
// Odd-length string (after stripping "0x") throws E_INVALIDARG
|
|
bool threw = false;
|
|
try
|
|
{
|
|
HexToBytes(L"0xabc");
|
|
}
|
|
catch (const wil::ResultException& e)
|
|
{
|
|
VERIFY_ARE_EQUAL(e.GetErrorCode(), E_INVALIDARG);
|
|
threw = true;
|
|
}
|
|
VERIFY_IS_TRUE(threw);
|
|
|
|
// Invalid hex character throws E_INVALIDARG
|
|
threw = false;
|
|
try
|
|
{
|
|
HexToBytes(L"0xZZ");
|
|
}
|
|
catch (const wil::ResultException& e)
|
|
{
|
|
VERIFY_ARE_EQUAL(e.GetErrorCode(), E_INVALIDARG);
|
|
threw = true;
|
|
}
|
|
VERIFY_IS_TRUE(threw);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(InteractiveMount)
|
|
{
|
|
// Add a fake interactive mount helper.
|
|
DistroFileChange mountHelper(L"/sbin/mount.hang", false);
|
|
mountHelper.SetContent(
|
|
L"#!/bin/sh\n"
|
|
L"read pass < /dev/tty\n");
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"chmod +x /sbin/mount.hang"), (DWORD)0);
|
|
|
|
// Don't keep the original fstab as it can be missing on the pipeline.
|
|
DistroFileChange fstab(L"/etc/fstab", false);
|
|
fstab.SetContent(L"none /mnt/ttytest hang 0 0\n");
|
|
|
|
// Restart the distro with this mount.
|
|
WslShutdown();
|
|
wsl::windows::common::SubProcess process(nullptr, LxssGenerateWslCommandLine(L"echo booted").c_str());
|
|
auto result = process.RunAndCaptureOutput(60 * 1000);
|
|
VERIFY_ARE_EQUAL(result.Stdout, L"booted\n");
|
|
VERIFY_ARE_EQUAL(result.ExitCode, 0);
|
|
}
|
|
|
|
TEST_METHOD(UninstallRejectsArguments)
|
|
{
|
|
VerifyOutput(
|
|
L"--uninstall Distro",
|
|
wsl::shared::Localization::MessageUninstallNoArguments(WSL_UNINSTALL_ARG, WSL_UNREGISTER_ARG) + L"\r\n",
|
|
-1);
|
|
}
|
|
|
|
}; // namespace UnitTests
|
|
} // namespace UnitTests
|