diff --git a/.github/actions/triage/action.yml b/.github/actions/triage/action.yml index 448129e5a..00c255502 100644 --- a/.github/actions/triage/action.yml +++ b/.github/actions/triage/action.yml @@ -21,14 +21,17 @@ runs: - name: 'Run WTI' shell: pwsh env: + INPUT_COMMENT: "${{ inputs.comment }}" + INPUT_ISSUE: "${{ inputs.issue }}" + INPUT_TOKEN: "${{ inputs.token }}" previous_body: "${{ inputs.previous_body }}" run: | $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop $maybe_comment = @() - if (![string]::IsNullOrEmpty("${{ inputs.comment }}")) + if (![string]::IsNullOrEmpty($env:INPUT_COMMENT)) { - $maybe_comment = @("--comment", "${{ inputs.comment }}") + $maybe_comment = @("--comment", $env:INPUT_COMMENT) } $maybe_previous_body = @() @@ -40,4 +43,4 @@ runs: curl.exe -L https://github.com/OneBlue/wti/releases/download/v0.1.12/wti.exe -o triage/wti.exe - cd triage && .\wti.exe --issue ${{ inputs.issue }} --config config.yml --github-token "${{ inputs.token }}" --ignore-tags @maybe_comment @maybe_previous_body \ No newline at end of file + cd triage && .\wti.exe --issue $env:INPUT_ISSUE --config config.yml --github-token $env:INPUT_TOKEN --ignore-tags @maybe_comment @maybe_previous_body \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 91a47ebdb..b43c8a095 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -92,7 +92,8 @@ Test execution: - **Requires Administrator privileges** - test.bat will fail without admin rights Test debugging: -- Wait for debugger: `/waitfordebugger` +- Attach WinDbgX automatically: `/attachdebugger` +- Wait for debugger (manual attach): `/waitfordebugger` - Break on failure: `/breakonfailure` - Run in-process: `/inproc` diff --git a/.pipelines/build-stage.yml b/.pipelines/build-stage.yml index 9b3fc58bb..8aa4e92ab 100644 --- a/.pipelines/build-stage.yml +++ b/.pipelines/build-stage.yml @@ -23,7 +23,7 @@ parameters: - name: targets type: object default: - - target: "wsl;libwsl;wslg;wslservice;wslhost;wslrelay;wslinstaller;wslinstall;init;wslserviceproxystub;wslsettings;wslinstallerproxystub;testplugin" + - target: "wsl;libwsl;wslg;wslservice;wslhost;wslrelay;wslinstaller;wslinstall;initramfs;wslserviceproxystub;wslsettings;wslinstallerproxystub;testplugin" pattern: "wsl.exe,libwsl.dll,wslg.exe,wslservice.exe,wslhost.exe,wslrelay.exe,wslinstaller.exe,wslinstall.dll,wslserviceproxystub.dll,wslsettings/wslsettings.dll,wslsettings/wslsettings.exe,wslinstallerproxystub.dll,wsldevicehost.dll,WSLDVCPlugin.dll,testplugin.dll,wsldeps.dll" - target: "msixgluepackage" pattern: "gluepackage.msix" @@ -382,24 +382,26 @@ stages: - powershell: | $taefVersion = (Select-Xml -Path packages.config -XPath '/packages/package[@id=''Microsoft.Taef'']/@version').Node.Value New-Item -ItemType Directory -Path "$(ob_outputDirectory)\bundle" -Force - foreach ($arch in @("x64", "ARM64")) + # TODO: Add "ARM64" to this list once arm64 testing is supported + foreach ($arch in @("x64")) { mkdir $(ob_outputDirectory)\testbin\$arch\release Move-Item -Path "bin\$arch\release\wsltests.dll" -Destination "$(ob_outputDirectory)\testbin\$arch\release\wsltests.dll" Move-Item -Path "bin\$arch\release\testplugin.dll" -Destination "$(ob_outputDirectory)\testbin\$arch\release\testplugin.dll" Move-Item -Path "packages\Microsoft.Taef.$taefVersion\build\Binaries\$arch" -Destination "$(ob_outputDirectory)\testbin\$arch\release\taef" + Move-Item -Path "bin\$arch\cloudtest" -Destination "$(ob_outputDirectory)\testbin\$arch\cloudtest" + + $TestDistroVersion = (Select-Xml -Path packages.config -XPath '/packages/package[@id=''Microsoft.WSL.TestDistro'']/@version').Node.Value + Copy-Item "packages\Microsoft.WSL.TestDistro.$TestDistroVersion\$arch\test_distro.tar.xz" "$(ob_outputDirectory)\testbin\$arch" } - Move-Item -Path "bin\x64\cloudtest" -Destination "$(ob_outputDirectory)\testbin\x64\cloudtest" Move-Item -Path "tools\test\test-setup.ps1" -Destination "$(ob_outputDirectory)\testbin\test-setup.ps1" Move-Item -Path "tools\test\CloudTest-Setup.bat" -Destination "$(ob_outputDirectory)\testbin\CloudTest-Setup.bat" Move-Item -Path "diagnostics\wsl.wprp" -Destination "$(ob_outputDirectory)\testbin\wsl.wprp" Move-Item -Path "test\linux\unit_tests" -Destination "$(ob_outputDirectory)\testbin\unit_tests" Move-Item -Path bundle\release\* -Destination $(ob_outputDirectory)\bundle - $TestDistroVersion = (Select-Xml -Path packages.config -XPath '/packages/package[@id=''Microsoft.WSL.TestDistro'']/@version').Node.Value - Copy-Item "packages\Microsoft.WSL.TestDistro.$TestDistroVersion\test_distro.tar.xz" "$(ob_outputDirectory)\testbin\x64" displayName: Move artifacts to drop directory diff --git a/.pipelines/wsl-build-nightly-localization.yml b/.pipelines/wsl-build-nightly-localization.yml index b79bd2288..7f07be592 100644 --- a/.pipelines/wsl-build-nightly-localization.yml +++ b/.pipelines/wsl-build-nightly-localization.yml @@ -17,6 +17,9 @@ schedules: - "master" always: true +variables: + targetBranch: ${{ variables['Build.SourceBranchName'] }} + pool: vmImage: 'windows-latest' @@ -45,9 +48,10 @@ steps: - powershell: | pip install --user -r tools/devops/requirements.txt - python tools/devops/create-change.py . "$env:token" "WSL localization" "Localization change from build: $env:buildId" "user/localization/$env:buildId" + python tools/devops/create-change.py . "$env:token" "WSL localization" "Localization change from build: $env:buildId" "user/localization/$env:buildId" "$env:targetBranch" displayName: Create pull request env: token: $(GithubPRToken) - buildId: $(Build.BuildId) \ No newline at end of file + buildId: $(Build.BuildId) + targetBranch: $(targetBranch) \ No newline at end of file diff --git a/.pipelines/wsl-build-notice.yml b/.pipelines/wsl-build-notice.yml index f626c16a2..01ec662a2 100644 --- a/.pipelines/wsl-build-notice.yml +++ b/.pipelines/wsl-build-notice.yml @@ -3,6 +3,9 @@ trigger: include: - master +variables: + targetBranch: ${{ variables['Build.SourceBranchName'] }} + pool: vmImage: 'windows-latest' @@ -25,9 +28,10 @@ steps: - powershell: | pip install --user -r tools/devops/requirements.txt - python tools/devops/create-change.py . "$env:token" "WSL notice" "Notice change from build: $env:buildId" "user/notice/$env:buildId" + python tools/devops/create-change.py . "$env:token" "WSL notice" "Notice change from build: $env:buildId" "user/notice/$env:buildId" "$env:targetBranch" displayName: Create pull request env: token: $(GithubPRToken) - buildId: $(Build.BuildId) \ No newline at end of file + buildId: $(Build.BuildId) + targetBranch: $(targetBranch) \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c2380400..69693b426 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,10 +16,8 @@ endif() if("${CMAKE_GENERATOR_PLATFORM}" STREQUAL "arm64" OR "${TARGET_PLATFORM}" STREQUAL "arm64") set(TARGET_PLATFORM "arm64") - set(TEST_DISTRO_PLATFORM "arm64") elseif("${CMAKE_GENERATOR_PLATFORM}" MATCHES "x64|amd64" OR "${TARGET_PLATFORM}" MATCHES "x64|amd64") set(TARGET_PLATFORM "x64") - set(TEST_DISTRO_PLATFORM "amd64") else() message(FATAL_ERROR "Unsupported platform: ${CMAKE_GENERATOR_PLATFORM}") endif() @@ -35,6 +33,8 @@ include(FetchContent) set(FETCHCONTENT_BASE_DIR ${CMAKE_BINARY_DIR}/_deps/${TARGET_PLATFORM}) +# N.B. Changes to any of the FetchContent dependencies below (GSL, nlohmannjson) must be reflected in cgmanifest.json + FetchContent_Declare(GSL URL https://github.com/microsoft/GSL/archive/refs/tags/v4.0.0.tar.gz URL_HASH SHA256=f0e32cb10654fea91ad56bde89170d78cfbf4363ee0b01d8f097de2ba49f6ce9) @@ -188,10 +188,12 @@ endif() if (${TARGET_PLATFORM} STREQUAL "x64") add_compile_definitions(_AMD64_) + set(DISTRO_HOSTTYPE x86_64) endif() if (${TARGET_PLATFORM} STREQUAL "arm64") add_compile_definitions(_ARM64_) + set(DISTRO_HOSTTYPE aarch64) endif() add_definitions(/sdl) # Default-initialize class members @@ -214,6 +216,7 @@ add_compile_definitions(UNICODE WSL_PACKAGE_VERSION_MAJOR=${PACKAGE_VERSION_MAJOR} WSL_PACKAGE_VERSION_MINOR=${PACKAGE_VERSION_MINOR} WSL_PACKAGE_VERSION_REVISION=${PACKAGE_VERSION_REVISION} + DISTRO_HOSTTYPE="${DISTRO_HOSTTYPE}" WSL_BUILD_WSL_SETTINGS=${WSL_BUILD_WSL_SETTINGS}) if (${OFFICIAL_BUILD}) @@ -250,13 +253,19 @@ set(COMMON_LINK_LIBRARIES Shlwapi.lib synchronization.lib Bcrypt.lib - Iphlpapi.lib - icu.lib + icu.lib) + +set(MSI_LINK_LIBRARIES + Wintrust.lib + msi.lib) + +set(HCS_LINK_LIBRARIES computecore.lib computenetwork.lib + Iphlpapi.lib) + +set(SERVICE_LINK_LIBRARIES MI.lib - Wintrust.lib - msi.lib wsldeps.lib) # Linux diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c6a33f57..632ba51c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,12 +40,12 @@ Note that WSL distro's launch in the Windows Console (unless you have taken step ### Collect WSL logs for networking issues -Install tcpdump in your WSL distribution using the following commands. +Install iptables and tcpdump in your WSL distribution using the following commands. Note: This will not work if WSL has Internet connectivity issues. ``` # sudo apt-get update -# sudo apt-get -y install tcpdump +# sudo apt-get -y install iptables tcpdump ``` Install [WPR](https://learn.microsoft.com/windows-hardware/test/wpt/windows-performance-recorder) diff --git a/NOTICE.txt b/NOTICE.txt index 02353a23b..0cf170bf7 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -636,192 +636,6 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - -Microsoft.NETCore.App.Runtime.win-arm64 10.0.0 - MIT - - -Copyright (c) 2021 -Copyright (c) Six Labors -(c) Microsoft Corporation -Copyright (c) 2022 FormatJS -Copyright (c) Andrew Arnott -Copyright 2019 LLVM Project -Copyright (c) 1998 Microsoft -Copyright 2018 Daniel Lemire -Copyright (c) .NET Foundation -Copyright 1995-2022 Mark Adler -Copyright 1995-2024 Mark Adler -Copyright (c) 2011, Google Inc. -Copyright (c) 2020 Dan Shechter -(c) 1997-2005 Sean Eron Anderson -Copyright (c) 2015 Andrew Gallant -Copyright (c) 2022, Wojciech Mula -Copyright (c) 2017 Yoshifumi Kawai -Copyright (c) 2022, Geoff Langdale -Copyright (c) 2005-2020 Rich Felker -Copyright (c) 2012-2021 Yann Collet -Copyright (c) Microsoft Corporation -Copyright (c) 2007 James Newton-King -Copyright (c) 1991-2024 Unicode, Inc. -Copyright (c) 2013-2017, Alfred Klomp -Copyright (c) 2018 Nemanja Mijailovic -Copyright 2012 the V8 project authors -Copyright (c) 1999 Lucent Technologies -Copyright (c) 2008-2016, Wojciech Mula -Copyright (c) 2011-2020 Microsoft Corp -Copyright (c) 2015-2017, Wojciech Mula -Copyright (c) 2015-2018, Wojciech Mula -Copyright (c) 2005-2007, Nick Galbreath -Copyright (c) 2015 The Chromium Authors -Copyright (c) 2018 Alexander Chermyanin -Copyright (c) The Internet Society 1997 -Copyright (c) 2004-2006 Intel Corporation -Copyright (c) 2011-2015 Intel Corporation -Copyright (c) 2013-2017, Milosz Krajewski -Copyright (c) 2016-2017, Matthieu Darbois -Copyright (c) The Internet Society (2003) -Copyright (c) .NET Foundation Contributors -(c) 1995-2024 Jean-loup Gailly and Mark Adler -Copyright (c) 2020 Mara Bos -Copyright (c) .NET Foundation and Contributors -Copyright (c) 2012 - present, Victor Zverovich -Copyright (c) 2006 Jb Evain (jbevain@gmail.com) -Copyright (c) 2008-2020 Advanced Micro Devices, Inc. -Copyright (c) 2019 Microsoft Corporation, Daan Leijen -Copyright (c) 2011 Novell, Inc (http://www.novell.com) -Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) -Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors -Copyright (c) 2014 Ryan Juckett http://www.ryanjuckett.com -Copyright 1995-2024 Jean-loup Gailly and Mark Adler Qkkbal -Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. -Portions (c) International Organization for Standardization 1986 -Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang) Disclaimers -Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo Yip -Copyright (c) 1980, 1986, 1993 The Regents of the University of California -Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the University of California -Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass - -The MIT License (MIT) - -Copyright (c) .NET Foundation and Contributors - -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -Microsoft.NETCore.App.Runtime.win-x64 10.0.0 - MIT - - -Copyright (c) 2021 -Copyright (c) Six Labors -(c) Microsoft Corporation -Copyright (c) 2022 FormatJS -Copyright (c) Andrew Arnott -Copyright 2019 LLVM Project -Copyright (c) 1998 Microsoft -Copyright 2018 Daniel Lemire -Copyright (c) .NET Foundation -Copyright 1995-2022 Mark Adler -Copyright 1995-2024 Mark Adler -Copyright (c) 2011, Google Inc. -Copyright (c) 2020 Dan Shechter -(c) 1997-2005 Sean Eron Anderson -Copyright (c) 2015 Andrew Gallant -Copyright (c) 2022, Wojciech Mula -Copyright (c) 2017 Yoshifumi Kawai -Copyright (c) 2022, Geoff Langdale -Copyright (c) 2005-2020 Rich Felker -Copyright (c) 2012-2021 Yann Collet -Copyright (c) Microsoft Corporation -Copyright (c) 2007 James Newton-King -Copyright (c) 1991-2024 Unicode, Inc. -Copyright (c) 2013-2017, Alfred Klomp -Copyright (c) 2018 Nemanja Mijailovic -Copyright 2012 the V8 project authors -Copyright (c) 1999 Lucent Technologies -Copyright (c) 2008-2016, Wojciech Mula -Copyright (c) 2011-2020 Microsoft Corp -Copyright (c) 2015-2017, Wojciech Mula -Copyright (c) 2015-2018, Wojciech Mula -Copyright (c) 2005-2007, Nick Galbreath -Copyright (c) 2015 The Chromium Authors -Copyright (c) 2018 Alexander Chermyanin -Copyright (c) The Internet Society 1997 -Copyright (c) 2004-2006 Intel Corporation -Copyright (c) 2011-2015 Intel Corporation -Copyright (c) 2013-2017, Milosz Krajewski -Copyright (c) 2016-2017, Matthieu Darbois -Copyright (c) The Internet Society (2003) -Copyright (c) .NET Foundation Contributors -(c) 1995-2024 Jean-loup Gailly and Mark Adler -Copyright (c) 2020 Mara Bos -Copyright (c) .NET Foundation and Contributors -Copyright (c) 2012 - present, Victor Zverovich -Copyright (c) 2006 Jb Evain (jbevain@gmail.com) -Copyright (c) 2008-2020 Advanced Micro Devices, Inc. -Copyright (c) 2019 Microsoft Corporation, Daan Leijen -Copyright (c) 2011 Novell, Inc (http://www.novell.com) -Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) -Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors -Copyright (c) 2014 Ryan Juckett http://www.ryanjuckett.com -Copyright 1995-2024 Jean-loup Gailly and Mark Adler Qkkbal -Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. -Portions (c) International Organization for Standardization 1986 -Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang) Disclaimers -Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo Yip -Copyright (c) 1980, 1986, 1993 The Regents of the University of California -Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the University of California -Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass - -The MIT License (MIT) - -Copyright (c) .NET Foundation and Contributors - -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - --------------------------------------------------------- --------------------------------------------------------- @@ -841,39 +655,6 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - -microsoft/gsl 0f6dbc9e2915ef5c16830f3fa3565738de2a9230 - MIT - - -Copyright 2008, Google Inc. -Copyright (c) 2015 Microsoft Corporation. - -Copyright (c) 2015 Microsoft Corporation. All rights reserved. - -This code is licensed under the MIT License (MIT). - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - --------------------------------------------------------- --------------------------------------------------------- diff --git a/cgmanifest.json b/cgmanifest.json index 2911f56e8..8c8fed586 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -2,15 +2,6 @@ "version": 1, "$schema": "https://json.schemastore.org/component-detection-manifest.json", "registrations": [ - { - "component": { - "type": "git", - "git": { - "repositoryUrl": "https://github.com/microsoft/GSL", - "commitHash": "0f6dbc9e2915ef5c16830f3fa3565738de2a9230" - } - } - }, { "component": { "type": "git", @@ -30,6 +21,28 @@ "hash": "sha1:918692098b11db61aff23684ab04f375e4a68f69" } } + }, + { + "component": { + "type": "other", + "other": { + "name": "GSL", + "version": "4.0.0", + "downloadUrl": "https://github.com/microsoft/GSL/archive/refs/tags/v4.0.0.tar.gz", + "hash": "sha256:f0e32cb10654fea91ad56bde89170d78cfbf4363ee0b01d8f097de2ba49f6ce9" + } + } + }, + { + "component": { + "type": "other", + "other": { + "name": "nlohmann::json", + "version": "3.12.0", + "downloadUrl": "https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz", + "hash": "sha256:42f6e95cad6ec532fd372391373363b62a14af6d771056dbfc86160e6dfff7aa" + } + } } ] } \ No newline at end of file diff --git a/distributions/DistributionInfo.json b/distributions/DistributionInfo.json index 52e307ff5..5999fa634 100644 --- a/distributions/DistributionInfo.json +++ b/distributions/DistributionInfo.json @@ -26,6 +26,32 @@ "Url": "https://cdimages.ubuntu.com/releases/24.04.4/release/ubuntu-24.04.4-wsl-arm64.wsl", "Sha256": "6b244d89f412a68f51e58f396fab65bed3b5896a25c045a99bef9c78a07df507" } + }, + { + "Name": "Ubuntu-22.04", + "FriendlyName": "Ubuntu 22.04 LTS", + "Default": false, + "Amd64Url": { + "Url": "https://releases.ubuntu.com/jammy/ubuntu-22.04.5-wsl-amd64.wsl", + "Sha256": "4499c4fe257f2fc83145b429ce211a0a43fd590e70d6261ede616210947d9f8f" + }, + "Arm64Url": { + "Url": "https://cdimage.ubuntu.com/ubuntu/releases/jammy/release/ubuntu-22.04.5-wsl-arm64.wsl", + "Sha256": "3e2d77b92cf9dad1095eaebcad96feba2781007f5f52431cd4fe6fff63b201e5" + } + }, + { + "Name": "Ubuntu-20.04", + "FriendlyName": "Ubuntu 20.04 LTS", + "Default": false, + "Amd64Url": { + "Url": "https://releases.ubuntu.com/focal/ubuntu-20.04.6-wsl-amd64.wsl", + "Sha256": "a9073f3726aab9661076506603706eadf284b80a791dec3adfde8e1989906fa4" + }, + "Arm64Url": { + "Url": "https://cdimage.ubuntu.com/ubuntu/releases/focal/release/ubuntu-20.04.6-wsl-arm64.wsl", + "Sha256": "16174012f9a3f6fb10729e648fd8c7dfa205d039d76902e4c28de746f85bf3c8" + } } ], "openSUSE": [ @@ -82,12 +108,12 @@ "FriendlyName": "Kali Linux Rolling", "Default": true, "Amd64Url": { - "Url": "https://kali.download/wsl-images/kali-2025.4/kali-linux-2025.4-wsl-rootfs-amd64.wsl", - "Sha256": "86aba7bb3d74d313e349f9f50d3f6119ee3b1491072920d063f17ce9b3f706ab" + "Url": "https://kali.download/wsl-images/kali-2026.1/kali-linux-2026.1-wsl-rootfs-amd64.wsl", + "Sha256": "55c295400603a592d5ccf6a4046bb965f04d2509a62d1752cb2fe586c65deded" }, "Arm64Url": { - "Url": "https://kali.download/wsl-images/kali-2025.4/kali-linux-2025.4-wsl-rootfs-arm64.wsl", - "Sha256": "bd8cdfe340ec596e470b6853ac662382897bc656c0ac3d3096eb399d0780e25e" + "Url": "https://kali.download/wsl-images/kali-2026.1/kali-linux-2026.1-wsl-rootfs-arm64.wsl", + "Sha256": "6ecd31844e9c1f0f77c052480ac40aeca24036846428676fa9137b003ab1b8e5" } } ], @@ -112,12 +138,12 @@ "FriendlyName": "AlmaLinux OS 8", "Default": false, "Amd64Url": { - "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v8.10.20250415.0/AlmaLinux-8.10_x64_20250415.0.wsl", - "Sha256": "34c3bc6d3ac693968737c65db52b67f68b8c1a6f8b024450819841a967f59a3d" + "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v8.10.20260311.0/AlmaLinux-8.10_x64_20260311.0.wsl", + "Sha256": "77a662e2947a1482087e3edf22595d62121ad8011385152f9209734adac87941" }, "Arm64Url": { - "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v8.10.20250415.0/AlmaLinux-8.10_ARM64_20250415.0.wsl", - "Sha256": "bd34f64b4822f6f115058f79cdbba85a1560360efbec14c3d699695023f8ca19" + "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v8.10.20260311.0/AlmaLinux-8.10_ARM64_20260311.0.wsl", + "Sha256": "7fb2e5d25cbba9bfeef1f9e1420fa4e30b671264aa92a015adf8f2901cdfb97f" } }, { @@ -125,12 +151,12 @@ "FriendlyName": "AlmaLinux OS 9", "Default": false, "Amd64Url": { - "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v9.7.20251119.0/AlmaLinux-9.7_x64_20251119.0.wsl", - "Sha256": "0a6588f4f723fcb3edbc37dd3e3e13be8ffe0a5027e47513e3d4d2a4451794e7" + "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v9.7.20260311.0/AlmaLinux-9.7_x64_20260311.0.wsl", + "Sha256": "c7d9c8f9bbe8f52a34d920988d6cea913f0251c457aef55f5ed66cce90561f92" }, "Arm64Url": { - "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v9.7.20251119.0/AlmaLinux-9.7_ARM64_20251119.0.wsl", - "Sha256": "b3e2632efe029a81db1ae7a914ea3b7e9e6e20d8c71c158270c331e6fea39bf8" + "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v9.7.20260311.0/AlmaLinux-9.7_ARM64_20260311.0.wsl", + "Sha256": "e9b5e184aa04e6f72574e2c29cda919d4aaed0c7408dcb81905f6c9391516c7f" } }, { @@ -138,12 +164,12 @@ "FriendlyName": "AlmaLinux OS Kitten 10", "Default": false, "Amd64Url": { - "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v10-kitten.20251030.0/AlmaLinux-Kitten-10_x64_20251030.0.wsl", - "Sha256": "d765d65076b041f3a67ba60edc37d056eeab2a260aed8e077684e05b78ecd9f5" + "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v10-kitten.20260311.0/AlmaLinux-Kitten-10_x64_20260311.0.wsl", + "Sha256": "b04ab8ae277ca4bbeb70a15381124609ca7f97b8f7c275930b3fd91195c385ad" }, "Arm64Url": { - "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v10-kitten.20251030.0/AlmaLinux-Kitten-10_ARM64_20251030.0.wsl", - "Sha256": "90c30b0adbf8d414c4b0a02eaeb6a5d8e488a2187a67dbaf11f4a3e843baae53" + "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v10-kitten.20260311.0/AlmaLinux-Kitten-10_ARM64_20260311.0.wsl", + "Sha256": "d502ab27654fa326888940ebce573dbe5abf7e44f709e67b53030da49572a92f" } }, { @@ -151,12 +177,12 @@ "FriendlyName": "AlmaLinux OS 10", "Default": true, "Amd64Url": { - "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v10.1.20251124.0/AlmaLinux-10.1_x64_20251124.0.wsl", - "Sha256": "24e8fa286a4081979d97e83a227fb89f332bcf731fe4b422679a3b455ab0be37" + "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v10.1.20260311.0/AlmaLinux-10.1_x64_20260311.0.wsl", + "Sha256": "6bc470081100ec507d933ee58b467f1903acc07da719ad37532b942439fc16c4" }, "Arm64Url": { - "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v10.1.20251124.0/AlmaLinux-10.1_ARM64_20251124.0.wsl", - "Sha256": "20700a4467214074f8a1a3d4e0e1cad25af36b8127d047ab6d5b4a1355e998b8" + "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v10.1.20260311.0/AlmaLinux-10.1_ARM64_20260311.0.wsl", + "Sha256": "2aedea16c5bdbc83b61d78634cc2f6b839435ad9066df1ea1a0bba7ad2fbdc39" } } ], @@ -166,8 +192,8 @@ "FriendlyName": "Arch Linux", "Default": true, "Amd64Url": { - "Url": "https://fastly.mirror.pkgbuild.com/wsl/2026.03.01.160197/archlinux-2026.03.01.160197.wsl", - "Sha256": "bd0a3d729742e15599ba0351c800225272bffb5061f2ce7cd16fab753055ca77" + "Url": "https://fastly.mirror.pkgbuild.com/wsl/2026.04.01.162669/archlinux-2026.04.01.162669.wsl", + "Sha256": "c1c4387b89bbaf84a79084bc1c51caa54998cd8b0b241a3b4e1c0a89f239c9c5" } } ], @@ -243,36 +269,6 @@ "Arm64PackageUrl": "https://publicwsldistros.blob.core.windows.net/wsldistrostorage/KaliLinux_1.13.1.0.AppxBundle", "PackageFamilyName": "KaliLinux.54290C8133FEE_ey8k8hqnwqnmg" }, - { - "Name": "Ubuntu-20.04", - "FriendlyName": "Ubuntu 20.04 LTS", - "StoreAppId": "9MTTCL66CPXJ", - "Amd64": true, - "Arm64": true, - "Amd64PackageUrl": "https://publicwsldistros.blob.core.windows.net/wsldistrostorage/Ubuntu2004-230608_x64.appx", - "Arm64PackageUrl": "https://publicwsldistros.blob.core.windows.net/wsldistrostorage/Ubuntu2004-230608_ARM64.appx", - "PackageFamilyName": "CanonicalGroupLimited.Ubuntu20.04LTS_79rhkp1fndgsc" - }, - { - "Name": "Ubuntu-22.04", - "FriendlyName": "Ubuntu 22.04 LTS", - "StoreAppId": "9PN20MSR04DW", - "Amd64": true, - "Arm64": true, - "Amd64PackageUrl": "https://publicwsldistros.blob.core.windows.net/wsldistrostorage/Ubuntu2204LTS-230518_x64.appx", - "Arm64PackageUrl": "https://publicwsldistros.blob.core.windows.net/wsldistrostorage/Ubuntu2204LTS-230518_ARM64.appx", - "PackageFamilyName": "CanonicalGroupLimited.Ubuntu22.04LTS_79rhkp1fndgsc" - }, - { - "Name": "Ubuntu-24.04", - "FriendlyName": "Ubuntu 24.04 LTS", - "StoreAppId": "9NZ3KLHXDJP5", - "Amd64": true, - "Arm64": true, - "Amd64PackageUrl": "https://publicwsldistros.blob.core.windows.net/wsldistrostorage/Ubuntu2404-240425.AppxBundle", - "Arm64PackageUrl": "https://publicwsldistros.blob.core.windows.net/wsldistrostorage/Ubuntu2404-240425.AppxBundle", - "PackageFamilyName": "CanonicalGroupLimited.Ubuntu24.04LTS_79rhkp1fndgsc" - }, { "Name": "OracleLinux_7_9", "FriendlyName": "Oracle Linux 7.9", diff --git a/doc/docs/dev-loop.md b/doc/docs/dev-loop.md index c0d4fc907..0b9b09942 100644 --- a/doc/docs/dev-loop.md +++ b/doc/docs/dev-loop.md @@ -96,7 +96,8 @@ bin\x64\debug\test.bat /name:*UnitTest* -f See [debugging](debugging.md) for general debugging instructions. -To attach a debugger to the unit test process, use: `/waitfordebugger` when calling `test.bat`. +To automatically attach WinDbgX to the unit test process, use: `/attachdebugger` when calling `test.bat`. +To wait for a debugger to be manually attached, use: `/waitfordebugger`. Use `/breakonfailure` to automatically break on the first test failure. ## Tips and tricks diff --git a/localization/strings/cs-CZ/Resources.resw b/localization/strings/cs-CZ/Resources.resw index ce5b44f5a..dab3e777c 100644 --- a/localization/strings/cs-CZ/Resources.resw +++ b/localization/strings/cs-CZ/Resources.resw @@ -960,6 +960,10 @@ Návrat k sítím NAT. Aktualizace se nezdařila (ukončovací kód: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} nepřijímá žádné argumenty. Pokud chcete zrušit registraci distribuce, použijte místo toho {}. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Odinstalování se nezdařilo (ukončovací kód: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/da-DK/Resources.resw b/localization/strings/da-DK/Resources.resw index e67fea572..b62983264 100644 --- a/localization/strings/da-DK/Resources.resw +++ b/localization/strings/da-DK/Resources.resw @@ -960,6 +960,10 @@ Går tilbage til NAT-netværk. Opdateringen mislykkedes (afslutningskode: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} kan ikke bruge argumenter. Hvis du vil fjerne registreringen af en distribution, skal du bruge {} i stedet. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Fjernelsen mislykkedes (udgangskode: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/de-DE/Resources.resw b/localization/strings/de-DE/Resources.resw index 21e3e7859..3b92b41a9 100644 --- a/localization/strings/de-DE/Resources.resw +++ b/localization/strings/de-DE/Resources.resw @@ -966,6 +966,10 @@ Fallback auf NAT-Netzwerk. Fehler beim Aktualisieren (Exitcode: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} akzeptiert keine Argumente. Um die Registrierung einer Verteilung aufzuheben, verwenden Sie stattdessen "{}". + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Fehler bei der Deinstallation (Exitcode: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/en-GB/Resources.resw b/localization/strings/en-GB/Resources.resw index 75b481fac..585132540 100644 --- a/localization/strings/en-GB/Resources.resw +++ b/localization/strings/en-GB/Resources.resw @@ -960,6 +960,10 @@ Falling back to NAT networking. Update failed (exit code: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Uninstall failed (exit code: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 55a700b83..1a9b8cbd8 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -960,6 +960,10 @@ Falling back to NAT networking. Update failed (exit code: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Uninstall failed (exit code: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/es-ES/Resources.resw b/localization/strings/es-ES/Resources.resw index 852222d2b..946db3636 100644 --- a/localization/strings/es-ES/Resources.resw +++ b/localization/strings/es-ES/Resources.resw @@ -966,6 +966,10 @@ Revirtiendo a las redes NAT. Error de actualización (código de salida: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} no toma ningún argumento. Para anular el registro de una distribución, use {} en su lugar. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Error de desinstalación (código de salida: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/fi-FI/Resources.resw b/localization/strings/fi-FI/Resources.resw index c249bcf6e..c3fa912a4 100644 --- a/localization/strings/fi-FI/Resources.resw +++ b/localization/strings/fi-FI/Resources.resw @@ -960,6 +960,10 @@ Palataan nat-verkkopalveluun. Päivitys epäonnistui (lopetuskoodi: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} ei ota vastaan argumentteja. Jakelun rekisteröinnin poistamiseen käytä sen sijaan {}. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Asennuksen poistaminen epäonnistui (lopetuskoodi: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/fr-FR/Resources.resw b/localization/strings/fr-FR/Resources.resw index 05cdbaefa..1a853c5af 100644 --- a/localization/strings/fr-FR/Resources.resw +++ b/localization/strings/fr-FR/Resources.resw @@ -967,6 +967,10 @@ Retour à la mise en réseau NAT. Échec de la mise à jour (code de sortie : {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} n’accepte aucun argument. Pour désinscrire une distribution, utilisez {} à la place. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Échec de la désinstallation (code de sortie : {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/hu-HU/Resources.resw b/localization/strings/hu-HU/Resources.resw index 59ab0266a..d4fa796f7 100644 --- a/localization/strings/hu-HU/Resources.resw +++ b/localization/strings/hu-HU/Resources.resw @@ -960,6 +960,10 @@ Visszaállás NAT-hálózatkezelésre. A frissítés nem sikerült (kilépési kód: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + A(z) {} nem fogad argumentumokat. Egy disztribúció regisztrációjának törléséhez használja inkább a következőt: {}. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Az eltávolítás nem sikerült (kilépési kód: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/it-IT/Resources.resw b/localization/strings/it-IT/Resources.resw index a0045c19c..574a39545 100644 --- a/localization/strings/it-IT/Resources.resw +++ b/localization/strings/it-IT/Resources.resw @@ -966,6 +966,10 @@ Fallback alla rete NAT. Aggiornamento non riuscito (codice di uscita: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} non accetta argomenti. Per annullare la registrazione di una distribuzione, usare {}. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Disinstallazione non riuscita (codice di uscita: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/ja-JP/Resources.resw b/localization/strings/ja-JP/Resources.resw index bc69e12ea..65d0a4224 100644 --- a/localization/strings/ja-JP/Resources.resw +++ b/localization/strings/ja-JP/Resources.resw @@ -966,6 +966,10 @@ NAT ネットワークにフォールバックしています。 更新に失敗しました (終了コード: {})。 {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} は引数を取りません。ディストリビューションの登録を解除するには、代わりに {} を使用してください。 + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + アンインストールに失敗しました (終了コード: {})。 {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/ko-KR/Resources.resw b/localization/strings/ko-KR/Resources.resw index 0ccd6849e..f712eb327 100644 --- a/localization/strings/ko-KR/Resources.resw +++ b/localization/strings/ko-KR/Resources.resw @@ -966,6 +966,10 @@ NAT 네트워킹으로 대체합니다. 업데이트하지 못했습니다(종료 코드: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {}은(는) 인수를 받지 않습니다. 배포 등록을 취소하려면 대신 {}을(를) 사용하세요. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + 제거하지 못했습니다(종료 코드: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/nb-NO/Resources.resw b/localization/strings/nb-NO/Resources.resw index ff222e53b..332d3a6fc 100644 --- a/localization/strings/nb-NO/Resources.resw +++ b/localization/strings/nb-NO/Resources.resw @@ -960,6 +960,10 @@ Faller tilbake til NAT-nettverk. Oppdateringen mislyktes (feilkode: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} tar ingen argumenter. Hvis du vil fjerne registreringen av en distribusjon, bruker du {} i stedet. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Avinstalleringen mislyktes (feilkode: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/nl-NL/Resources.resw b/localization/strings/nl-NL/Resources.resw index d7af70dda..59468ff7b 100644 --- a/localization/strings/nl-NL/Resources.resw +++ b/localization/strings/nl-NL/Resources.resw @@ -960,6 +960,10 @@ Terugvallen op NAT-netwerken. Bijwerken is mislukt (afsluitcode: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} neemt geen argumenten aan. Gebruik in plaats daarvan {} om een distributie uit te schrijven. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Installatie ongedaan maken is mislukt (afsluitcode: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/pl-PL/Resources.resw b/localization/strings/pl-PL/Resources.resw index 4490ce23e..1db9674d9 100644 --- a/localization/strings/pl-PL/Resources.resw +++ b/localization/strings/pl-PL/Resources.resw @@ -960,6 +960,10 @@ Powrót do sieci NAT. Aktualizacja nie powiodła się (kod zakończenia: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} nie przyjmuje żadnych argumentów. Aby wyrejestrować dystrybucję, użyj {} zamiast tego. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Odinstalowanie nie powiodło się (kod zakończenia: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/pt-BR/Resources.resw b/localization/strings/pt-BR/Resources.resw index 7081b176f..cbcc93b1c 100644 --- a/localization/strings/pt-BR/Resources.resw +++ b/localization/strings/pt-BR/Resources.resw @@ -967,6 +967,10 @@ Voltando à rede NAT. Falha na atualização (código de saída: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} não tem argumentos. Para cancelar o registro de uma distribuição, use {} em vez disso. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Falha ao desinstalar (código de saída: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/pt-PT/Resources.resw b/localization/strings/pt-PT/Resources.resw index 03eacf57a..67d3e0375 100644 --- a/localization/strings/pt-PT/Resources.resw +++ b/localization/strings/pt-PT/Resources.resw @@ -960,6 +960,10 @@ A reverter para a rede NAT. A atualização falhou (código de saída: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} não aceita quaisquer argumentos. Para anular o registo de uma distribuição, utilize {} em vez disso. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Falha ao desinstalar (código de saída: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/ru-RU/Resources.resw b/localization/strings/ru-RU/Resources.resw index eae84ab0c..df1e9b89e 100644 --- a/localization/strings/ru-RU/Resources.resw +++ b/localization/strings/ru-RU/Resources.resw @@ -967,6 +967,10 @@ Сбой обновления (код выхода: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} не принимает аргументов. Чтобы отменить регистрацию дистрибутива, используйте вместо этого {}. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Не удалось удалить (код выхода: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/sv-SE/Resources.resw b/localization/strings/sv-SE/Resources.resw index ec45bca4a..b963a3869 100644 --- a/localization/strings/sv-SE/Resources.resw +++ b/localization/strings/sv-SE/Resources.resw @@ -960,6 +960,10 @@ Mer information finns på https://aka.ms/wslinstall Uppdateringen misslyckades (slutkod: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} har inga argument. Om du vill avregistrera en distribution använder du {} i stället. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Avinstallationen misslyckades (slutkod: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/tr-TR/Resources.resw b/localization/strings/tr-TR/Resources.resw index 87475b143..1b79579b3 100644 --- a/localization/strings/tr-TR/Resources.resw +++ b/localization/strings/tr-TR/Resources.resw @@ -960,6 +960,10 @@ NAT ağ bağlantısına geri dönülüyor. Güncelleştirme başarısız (çıkış kodu: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} bağımsız değişken almaz. Dağıtımın kaydını silmek için bunun yerine {} kullanın. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Kaldırma başarısız oldu (çıkış kodu: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/zh-CN/Resources.resw b/localization/strings/zh-CN/Resources.resw index 30f2e9a36..5901c85ba 100644 --- a/localization/strings/zh-CN/Resources.resw +++ b/localization/strings/zh-CN/Resources.resw @@ -966,6 +966,10 @@ Windows: {} 更新失败(退出代码: {})。 {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} 不采用任何参数。若要注销分发,请改用 {}。 + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + 卸载失败(退出代码: {})。 {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/zh-TW/Resources.resw b/localization/strings/zh-TW/Resources.resw index 933030fc1..6d8b005de 100644 --- a/localization/strings/zh-TW/Resources.resw +++ b/localization/strings/zh-TW/Resources.resw @@ -966,6 +966,10 @@ Windows 版本: {} 更新失敗 (結束代碼: {})。 {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} 不接受任何引數。若要取消註冊發行版本,請改用 {}。 + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + 解除安裝失敗 (結束代碼: {})。 {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/msipackage/CMakeLists.txt b/msipackage/CMakeLists.txt index c37a45414..a7c7c8c04 100644 --- a/msipackage/CMakeLists.txt +++ b/msipackage/CMakeLists.txt @@ -22,7 +22,7 @@ foreach(binary ${WINDOWS_BINARIES}) list(APPEND BINARIES_DEPENDENCIES "${PACKAGE_INPUT_DIR}/${binary}") endforeach() -set(LINUX_BINARIES init) +set(LINUX_BINARIES init;initrd.img) foreach(binary ${LINUX_BINARIES}) list(APPEND BINARIES_DEPENDENCIES "${BIN}/${binary}") endforeach() @@ -52,7 +52,7 @@ add_custom_command( add_custom_target(msipackage DEPENDS ${OUTPUT_PACKAGE}) set_target_properties(msipackage PROPERTIES EXCLUDE_FROM_ALL FALSE SOURCES ${PACKAGE_WIX_IN}) -add_dependencies(msipackage wsl wslg wslservice wslhost wslrelay wslserviceproxystub init wslinstall msixgluepackage) +add_dependencies(msipackage wsl wslg wslservice wslhost wslrelay wslserviceproxystub init initramfs wslinstall msixgluepackage) if (WSL_BUILD_WSL_SETTINGS) add_dependencies(msipackage wslsettings libwsl) diff --git a/msipackage/package.wix.in b/msipackage/package.wix.in index 92bcb97a1..6ebc9f3b2 100644 --- a/msipackage/package.wix.in +++ b/msipackage/package.wix.in @@ -261,6 +261,7 @@ + @@ -461,21 +462,6 @@ Execute="deferred" /> - - - - - - - - - diff --git a/packages.config b/packages.config index e52188f5c..678c68d64 100644 --- a/packages.config +++ b/packages.config @@ -8,8 +8,8 @@ - - + + @@ -18,10 +18,10 @@ - + - + diff --git a/src/linux/init/CMakeLists.txt b/src/linux/init/CMakeLists.txt index 602896934..d5dee8005 100644 --- a/src/linux/init/CMakeLists.txt +++ b/src/linux/init/CMakeLists.txt @@ -51,3 +51,14 @@ add_linux_executable(init "${SOURCES}" "${HEADERS};${COMMON_LINUX_HEADERS}" "${I add_dependencies(init localization) set_target_properties(init PROPERTIES FOLDER linux) + +set(INITRAMFS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${CMAKE_BUILD_TYPE}/initrd.img) +set(INIT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${CMAKE_BUILD_TYPE}/init) +add_custom_command( + OUTPUT ${INITRAMFS} "${CMAKE_CURRENT_BINARY_DIR}/CmakeFiles/initramfs" + DEPENDS init ${INIT} + COMMAND powershell.exe -ExecutionPolicy Bypass -NoProfile -NonInteractive -File "${CMAKE_SOURCE_DIR}/tools/create-initrd.ps1" "${INIT}" "${INITRAMFS}" + COMMAND ${CMAKE_COMMAND} -E touch "${CMAKE_CURRENT_BINARY_DIR}/CmakeFiles/initramfs" + VERBATIM) +add_custom_target(initramfs DEPENDS ${INITRAMFS}) +set_target_properties(initramfs PROPERTIES FOLDER linux) diff --git a/src/linux/init/GnsPortTracker.cpp b/src/linux/init/GnsPortTracker.cpp index d5afaf6fd..669582a14 100644 --- a/src/linux/init/GnsPortTracker.cpp +++ b/src/linux/init/GnsPortTracker.cpp @@ -2,12 +2,12 @@ #include #include -#include #include #include /* Definition of AUDIT_* constants */ #include #include #include +#include #include #include "common.h" // Needs to be included before sal.h before of __reserved macro #include "NetlinkTransactionError.h" @@ -73,6 +73,7 @@ void GnsPortTracker::Run() // for port deallocation std::thread{std::bind(&GnsPortTracker::RunPortRefresh, this)}.detach(); + std::thread{std::bind(&GnsPortTracker::RunDeferredResolve, this)}.detach(); auto future = std::make_optional(m_allocatedPortsRefresh.get_future()); std::optional refreshResult; @@ -98,7 +99,7 @@ void GnsPortTracker::Run() result = HandleRequest(allocationRequest); if (result == 0) { - m_allocatedPorts.emplace(std::make_pair(allocationRequest, std::make_optional(time(nullptr) + c_bind_timeout_seconds))); + TrackPort(allocationRequest); GNS_LOG_INFO( "Tracking bind call: family ({}) port ({}) protocol ({})", allocationRequest.Family, @@ -115,6 +116,20 @@ void GnsPortTracker::Run() { GNS_LOG_ERROR("Failed to complete bind request, {}", e.what()); } + + if (bindCall->PortZeroBind.has_value()) + { + try + { + std::lock_guard lock(m_deferredMutex); + m_deferredQueue.push_back(std::move(bindCall->PortZeroBind.value())); + m_deferredCv.notify_one(); + } + catch (const std::exception& e) + { + GNS_LOG_ERROR("Failed to queue port-0 bind for deferred resolution, {}", e.what()); + } + } } // If bindCall is empty, then the read() timed out. Look for any closed port @@ -133,12 +148,39 @@ void GnsPortTracker::Run() } } + // Process any port-0 binds that the background thread has resolved. + std::deque resolved; + { + std::lock_guard lock(m_resolvedMutex); + resolved.swap(m_resolvedQueue); + } + for (auto& allocation : resolved) + { + const auto result = HandleRequest(allocation); + if (result == 0) + { + TrackPort(std::move(allocation)); + } + else + { + GNS_LOG_ERROR( + "Failed to register resolved port-0 bind: family ({}) port ({}) protocol ({}), error {}", + allocation.Family, + allocation.Port, + allocation.Protocol, + result); + } + } + // Only look at bound ports if there's something to deallocate to avoid wasting cycles - if (refreshResult.has_value() && !m_allocatedPorts.empty()) + if (refreshResult.has_value()) { - future = m_allocatedPortsRefresh.get_future(); - refreshResult->Resume(); // This will resume the sock_diag thread - refreshResult.reset(); + if (!m_allocatedPorts.empty()) + { + future = m_allocatedPortsRefresh.get_future(); + refreshResult->Resume(); // This will resume the sock_diag thread + refreshResult.reset(); + } } } } @@ -227,6 +269,7 @@ void GnsPortTracker::OnRefreshAllocatedPorts(const std::set& Por it->first.Family, it->first.Port, it->first.Protocol); + it = m_allocatedPorts.erase(it); continue; } @@ -299,8 +342,8 @@ std::optional GnsPortTracker::ReadNextRequest() } catch (const std::exception& e) { - GNS_LOG_ERROR("Fetch to read bind() call info with ID {}lu for pid {}, {}", callInfo.id, callInfo.pid, e.what()); - return {{{}, callInfo.id}}; + GNS_LOG_ERROR("Failed to read bind() call info with ID {} for pid {}, {}", callInfo.id, callInfo.pid, e.what()); + return {{{}, {}, callInfo.id}}; } } @@ -310,14 +353,14 @@ std::optional GnsPortTracker::GetCallInfo( auto ParseSocket = [&](int Socket, size_t AddressPtr, size_t AddressLength) -> std::optional { if (AddressLength < sizeof(sockaddr)) { - return {{{}, CallId}}; // Invalid sockaddr. Let it go through. + return {{{}, {}, CallId}}; // Invalid sockaddr. Let it go through. } auto networkNamespace = std::filesystem::read_symlink(std::format("/proc/{}/ns/net", Pid)).string(); if (networkNamespace != m_networkNamespace) { GNS_LOG_INFO("Skipping bind() call for pid {} in network namespace {}", Pid, networkNamespace.c_str()); - return {{{}, CallId}}; // Different network namespace. Let it go through. + return {{{}, {}, CallId}}; // Different network namespace. Let it go through. } auto processMemory = m_seccompDispatcher->ReadProcessMemory(CallId, Pid, AddressPtr, AddressLength); @@ -331,7 +374,7 @@ std::optional GnsPortTracker::GetCallInfo( if ((address.sa_family != AF_INET && address.sa_family != AF_INET6) || (address.sa_family == AF_INET6 && AddressLength < sizeof(sockaddr_in6))) { - return {{{}, CallId}}; // This is a non IP call, or invalid sockaddr_in6. Let it go through + return {{{}, {}, CallId}}; // This is a non IP call, or invalid sockaddr_in6. Let it go through } // Read the port. The port *happens* to be in the same spot in memory for both sockaddr_in @@ -343,7 +386,28 @@ std::optional GnsPortTracker::GetCallInfo( in_port_t port = ntohs(inAddr->sin_port); if (port == 0) { - return {{{}, CallId}}; // If port is 0, just let the call go through + // Port 0 means the kernel will assign an ephemeral port. We can't know + // the port until after the bind() completes, so duplicate the socket fd + // now (while the process is still stopped by seccomp) and defer the + // getsockname() lookup to after CompleteRequest() unblocks it. + try + { + const int protocol = GetSocketProtocol(Pid, Socket); + auto dupFd = DuplicateSocketFd(Pid, Socket); + if (!dupFd) + { + return {{{}, {}, CallId}}; + } + if (!m_seccompDispatcher->ValidateCookie(CallId)) + { + return {{{}, {}, CallId}}; + } + return {{{}, DeferredPortLookup{Pid, std::move(dupFd), protocol}, CallId}}; + } + catch (const std::exception&) + { + return {{{}, {}, CallId}}; // Can't determine protocol, just let it through + } } in6_addr storedAddress = {}; @@ -370,7 +434,7 @@ std::optional GnsPortTracker::GetCallInfo( throw RuntimeErrorWithSourceLocation(std::format("Invalid call id {}", CallId)); } - return {{{PortAllocation(port, address.sa_family, protocol, storedAddress)}, CallId}}; + return {{{PortAllocation(port, address.sa_family, protocol, storedAddress)}, {}, CallId}}; }; #ifdef __x86_64__ if (Arch & __AUDIT_ARCH_64BIT) @@ -384,7 +448,7 @@ std::optional GnsPortTracker::GetCallInfo( { if (Arguments[0] != SYS_BIND) { - return {{{}, CallId}}; // Not a bind call, just let the call go through + return {{{}, {}, CallId}}; // Not a bind call, just let the call go through } // Grab the first 3 parameters auto processMemory = m_seccompDispatcher->ReadProcessMemory(CallId, Pid, Arguments[1], sizeof(uint32_t) * 3); @@ -410,7 +474,7 @@ int GnsPortTracker::GetSocketProtocol(int pid, int fd) { const auto path = std::format("/proc/{}/fd/{}", pid, fd); - // Because there's a race between the time where the buffer size is determined and + // Because there's a race between the time where the buffer size is determined // and the actual getxattr() call, retry until the buffer size is big enough std::string protocol; int result = -1; @@ -424,7 +488,7 @@ int GnsPortTracker::GetSocketProtocol(int pid, int fd) if (result < 0) { - RuntimeErrorWithSourceLocation(std::format("Failed to read protocol for socket: {}, {}", path, errno)); + throw RuntimeErrorWithSourceLocation(std::format("Failed to read protocol for socket: {}, {}", path, errno)); } // In case the size of the attribute shrunk between the two getxattr calls @@ -442,6 +506,137 @@ int GnsPortTracker::GetSocketProtocol(int pid, int fd) throw RuntimeErrorWithSourceLocation(std::format("Unexpected IP socket protocol: {}", protocol)); } +wil::unique_fd GnsPortTracker::DuplicateSocketFd(pid_t Pid, int SocketFd) +{ + // Duplicate the socket fd from the target process into our address space. + // We cannot use open("/proc/pid/fd/N") for sockets because the symlink target + // (socket:[inode]) is not a valid filesystem path. Use pidfd_getfd() instead. + wil::unique_fd pidFd(static_cast(syscall(SYS_pidfd_open, Pid, 0u))); + if (!pidFd) + { + GNS_LOG_INFO("Port-0 bind: pidfd_open failed for pid {} (errno {})", Pid, errno); + return {}; + } + + wil::unique_fd dupFd(static_cast(syscall(SYS_pidfd_getfd, pidFd.get(), SocketFd, 0u))); + if (!dupFd) + { + GNS_LOG_INFO("Port-0 bind: pidfd_getfd failed for pid {} fd {} (errno {})", Pid, SocketFd, errno); + } + + return dupFd; +} + +void GnsPortTracker::TrackPort(PortAllocation allocation) +try +{ + // Use insert_or_assign so the deallocation timeout is refreshed if the same + // port key is already present (emplace would silently keep the old entry). + m_allocatedPorts.insert_or_assign(std::move(allocation), std::make_optional(time(nullptr) + c_bind_timeout_seconds)); +} +catch (const std::exception& e) +{ + GNS_LOG_ERROR("Failed to track port allocation, {}", e.what()); +} + +void GnsPortTracker::RunDeferredResolve() +{ + UtilSetThreadName("GnsPortZero"); + + for (;;) + { + DeferredPortLookup lookup{0, {}, 0}; + { + std::unique_lock lock(m_deferredMutex); + m_deferredCv.wait(lock, [&] { return !m_deferredQueue.empty(); }); + lookup = std::move(m_deferredQueue.front()); + m_deferredQueue.pop_front(); + } + + const auto pid = lookup.Pid; + try + { + ResolvePortZeroBind(std::move(lookup)); + } + catch (const std::exception& e) + { + GNS_LOG_ERROR("Failed to resolve port-0 bind for pid {}, {}", pid, e.what()); + } + } +} + +void GnsPortTracker::ResolvePortZeroBind(DeferredPortLookup lookup) +{ + // The socket fd was already duplicated (via pidfd_getfd) while the target process + // was stopped by seccomp, so it remains valid even if the process has closed or + // reused the original fd number. + + // The bind() syscall is being completed asynchronously on the seccomp dispatcher + // thread after CompleteRequest() unblocks it. Poll getsockname() briefly until + // the kernel assigns a port. + constexpr int maxRetries = 25; + constexpr auto retryDelay = std::chrono::milliseconds(100); + + in_port_t port = 0; + in6_addr address = {}; + int resolvedFamily = 0; + + for (int attempt = 0; attempt < maxRetries; ++attempt) + { + if (attempt > 0) + { + std::this_thread::sleep_for(retryDelay); + } + + sockaddr_storage storage{}; + socklen_t addrLen = sizeof(storage); + if (getsockname(lookup.DuplicatedSocketFd.get(), reinterpret_cast(&storage), &addrLen) != 0) + { + GNS_LOG_ERROR("Port-0 bind: getsockname failed for pid {} (errno {})", lookup.Pid, errno); + return; + } + + resolvedFamily = static_cast(storage.ss_family); + + if (storage.ss_family == AF_INET) + { + const auto* sin = reinterpret_cast(&storage); + port = ntohs(sin->sin_port); + address.s6_addr32[0] = sin->sin_addr.s_addr; + } + else if (storage.ss_family == AF_INET6) + { + const auto* sin6 = reinterpret_cast(&storage); + port = ntohs(sin6->sin6_port); + memcpy(address.s6_addr32, sin6->sin6_addr.s6_addr32, sizeof(address.s6_addr32)); + } + else + { + GNS_LOG_ERROR("Port-0 bind: unexpected address family ({}) for pid {}", resolvedFamily, lookup.Pid); + return; + } + + if (port != 0) + { + break; + } + } + + if (port == 0) + { + GNS_LOG_ERROR("Port-0 bind: kernel did not assign a port for pid {} after retries", lookup.Pid); + return; + } + + PortAllocation allocation(port, resolvedFamily, lookup.Protocol, address); + GNS_LOG_INFO( + "Port-0 bind resolved: family ({}) port ({}) protocol ({}) for pid {}", resolvedFamily, port, lookup.Protocol, lookup.Pid); + { + std::lock_guard lock(m_resolvedMutex); + m_resolvedQueue.push_back(std::move(allocation)); + } +} + std::ostream& operator<<(std::ostream& out, const GnsPortTracker::PortAllocation& entry) { return out << "Port=" << entry.Port << ", Family=" << entry.Family << ", Protocol=" << entry.Protocol; diff --git a/src/linux/init/GnsPortTracker.h b/src/linux/init/GnsPortTracker.h index c6c138c51..a3f2945a1 100644 --- a/src/linux/init/GnsPortTracker.h +++ b/src/linux/init/GnsPortTracker.h @@ -1,7 +1,9 @@ // Copyright (C) Microsoft Corporation. All rights reserved. #pragma once +#include #include +#include #include #include #include @@ -92,9 +94,27 @@ class GnsPortTracker } }; + struct DeferredPortLookup + { + pid_t Pid; + wil::unique_fd DuplicatedSocketFd; // Duplicated via pidfd_getfd while process was stopped + int Protocol; + + DeferredPortLookup(pid_t Pid, wil::unique_fd DuplicatedSocketFd, int Protocol) : + Pid(Pid), DuplicatedSocketFd(std::move(DuplicatedSocketFd)), Protocol(Protocol) + { + } + + DeferredPortLookup(DeferredPortLookup&&) = default; + DeferredPortLookup& operator=(DeferredPortLookup&&) = default; + DeferredPortLookup(const DeferredPortLookup&) = delete; + DeferredPortLookup& operator=(const DeferredPortLookup&) = delete; + }; + struct BindCall { std::optional Request; + std::optional PortZeroBind; std::uint64_t CallId; }; @@ -118,14 +138,20 @@ class GnsPortTracker int RequestPort(const PortAllocation& Port, bool Allocate); - int ClosePort(const PortAllocation& Port); - int HandleRequest(const PortAllocation& Request); void CompleteRequest(uint64_t Id, int Result); static int GetSocketProtocol(int Pid, int Fd); + static wil::unique_fd DuplicateSocketFd(pid_t Pid, int SocketFd); + + void ResolvePortZeroBind(DeferredPortLookup lookup); + + void RunDeferredResolve(); + + void TrackPort(PortAllocation allocation); + std::map> m_allocatedPorts; std::shared_ptr m_hvSocketChannel; NetlinkChannel m_channel; @@ -137,6 +163,15 @@ class GnsPortTracker std::shared_ptr m_seccompDispatcher; std::string m_networkNamespace; + + std::mutex m_deferredMutex; + std::condition_variable m_deferredCv; + std::deque m_deferredQueue; + + // Resolved port-0 allocations posted by the background RunDeferredResolve thread + // for the main Run() loop to process (keeps SocketChannel access single-threaded). + std::mutex m_resolvedMutex; + std::deque m_resolvedQueue; }; std::ostream& operator<<(std::ostream& out, const GnsPortTracker::PortAllocation& portAllocation); diff --git a/src/linux/init/config.cpp b/src/linux/init/config.cpp index 5f6d29db2..b4a554b1c 100644 --- a/src/linux/init/config.cpp +++ b/src/linux/init/config.cpp @@ -2137,7 +2137,7 @@ Return Value: // const char* const Argv[] = {MOUNT_COMMAND, MOUNT_FSTAB_ARG, nullptr}; - if (UtilCreateProcessAndWait(Argv[0], Argv, nullptr, {{WSL_DRVFS_ELEVATED_ENV, Elevated ? "1" : "0"}}) < 0) + if (UtilCreateProcessAndWait(Argv[0], Argv, nullptr, {{WSL_DRVFS_ELEVATED_ENV, Elevated ? "1" : "0"}}, true) < 0) { auto message = wsl::shared::Localization::MessageFstabMountFailed(); LOG_ERROR("{}", message.c_str()); diff --git a/src/linux/init/drvfs.cpp b/src/linux/init/drvfs.cpp index 943557175..c36ae7e81 100644 --- a/src/linux/init/drvfs.cpp +++ b/src/linux/init/drvfs.cpp @@ -21,6 +21,7 @@ Module Name: #include "config.h" #include "message.h" #include +#include #include using namespace std::chrono_literals; @@ -32,6 +33,8 @@ using namespace std::chrono_literals; #define PLAN9_SYMLINK_ROOT_OPTION "symlinkroot=" #define PLAN9_UNC_PREFIX_LENGTH (2) +#define VIRTIOFS_TAG_DIR "/run/wsl/virtiofs" + #define LOG_STDERR(_errno) fprintf(stderr, "mount: %s\n", strerror(_errno)) constexpr int c_exitCodeInvalidUsage = 1; @@ -41,6 +44,62 @@ int MountFilesystem(const char* FsType, const char* Source, const char* Target, int MountWithRetry(const char* Source, const char* Target, const char* FsType, const char* Options, int* ExitCode = nullptr); +void SaveVirtiofsTagMapping(const char* Tag, const char* Source) + +/*++ + +Routine Description: + + This routine creates a symlink in VIRTIOFS_TAG_DIR that maps a virtiofs tag + to its Windows mount source path. This allows QueryVirtiofsMountSource to + resolve tags without talking to the service. + +Arguments: + + Tag - Supplies the virtiofs tag. + + Source - Supplies the Windows path the tag refers to. + +Return Value: + + None. + +--*/ + +{ + // + // Validate the tag is a GUID to prevent path traversal. + // + + const auto Guid = wsl::shared::string::ToGuid(Tag); + if (!Guid) + { + LOG_WARNING("Invalid virtiofs tag {}", Tag); + return; + } + + // + // Canonicalize path separators to backslashes before persisting. + // + + std::string CanonicalSource{Source}; + UtilCanonicalisePathSeparator(CanonicalSource, PATH_SEP_NT); + + UtilMkdirPath(VIRTIOFS_TAG_DIR, 0755); + + auto LinkPath = std::format("{}/{}", VIRTIOFS_TAG_DIR, Tag); + + // + // Remove any existing symlink for this tag before creating a new one. + // + + unlink(LinkPath.c_str()); + if (symlink(CanonicalSource.c_str(), LinkPath.c_str()) < 0) + { + LOG_WARNING("Failed to create virtiofs tag symlink {} -> {}: {}", LinkPath, CanonicalSource, errno); + } +} + std::pair ConvertDrvfsMountOptionsToPlan9(std::string_view Options, const wsl::linux::WslDistributionConfig& Config) /*++ @@ -565,7 +624,18 @@ try // auto* Tag = wsl::shared::string::FromSpan(ResponseSpan, Response.TagOffset); - return MountWithRetry(Tag, Target, VIRTIO_FS_TYPE, MountOptions.c_str(), ExitCode); + auto* ResponseSource = wsl::shared::string::FromSpan(ResponseSpan, Response.SourceOffset); + THROW_LAST_ERROR_IF(MountWithRetry(Tag, Target, VIRTIO_FS_TYPE, MountOptions.c_str(), ExitCode) < 0); + + // + // Save the tag mapping. + // + // N.B. Use the source path from the response since the service canonicalizes it. + // + + SaveVirtiofsTagMapping(Tag, ResponseSource); + + return 0; } CATCH_RETURN_ERRNO() @@ -620,8 +690,13 @@ try return -1; } - Tag = wsl::shared::string::FromSpan(ResponseSpan, Response.TagOffset); - return MountWithRetry(Tag, Target, VIRTIO_FS_TYPE, Options); + auto* NewTag = wsl::shared::string::FromSpan(ResponseSpan, Response.TagOffset); + auto* Source = wsl::shared::string::FromSpan(ResponseSpan, Response.SourceOffset); + THROW_LAST_ERROR_IF(MountWithRetry(NewTag, Target, VIRTIO_FS_TYPE, Options) < 0); + + SaveVirtiofsTagMapping(NewTag, Source); + + return 0; } CATCH_RETURN_ERRNO() @@ -631,7 +706,8 @@ std::string QueryVirtiofsMountSource(const char* Tag) Routine Description: - This routine takes a virtiofs tag and determines the Windows path it refers to. + This routine takes a virtiofs tag and determines the Windows path it refers to + by reading the symlink created during mount. Arguments: @@ -660,28 +736,12 @@ try return {}; } - wsl::shared::MessageWriter QueryShare(LxInitMessageQueryVirtioFsDevice); - QueryShare.WriteString(QueryShare->TagOffset, Tag); - // - // Connect to the host and send the query request. + // Read the symlink that maps this tag to its Windows source path. // - wsl::shared::SocketChannel Channel{UtilConnectVsock(LX_INIT_UTILITY_VM_VIRTIOFS_PORT, true), "QueryVirtioFs"}; - if (Channel.Socket() < 0) - { - return {}; - } - - gsl::span ResponseSpan; - const auto& Response = Channel.Transaction(QueryShare.Span(), &ResponseSpan); - if (Response.Result != 0) - { - LOG_ERROR("Query virtiofs share for {} failed {}", Tag, Response.Result); - return {}; - } - - return wsl::shared::string::FromSpan(ResponseSpan, Response.TagOffset); + auto LinkPath = std::format("{}/{}", VIRTIOFS_TAG_DIR, Tag); + return std::filesystem::read_symlink(LinkPath).string(); } catch (...) { diff --git a/src/linux/init/main.cpp b/src/linux/init/main.cpp index e8e831502..f31ebf732 100644 --- a/src/linux/init/main.cpp +++ b/src/linux/init/main.cpp @@ -3615,7 +3615,13 @@ Return Value: .filter = Filter, }; - wil::unique_fd Fd{syscall(__NR_seccomp, SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, &Prog)}; + wil::unique_fd Fd{syscall( + __NR_seccomp, SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER | SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV, &Prog)}; + if (!Fd && errno == EINVAL) + { + LOG_INFO("seccomp failed with EINVAL with SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV, retrying without it."); + Fd = syscall(__NR_seccomp, SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, &Prog); + } if (!Fd) { LOG_ERROR("Failed to register bpf syscall hook, {}", errno); diff --git a/src/linux/init/util.cpp b/src/linux/init/util.cpp index 3cc0ae4c2..c67d69462 100644 --- a/src/linux/init/util.cpp +++ b/src/linux/init/util.cpp @@ -613,7 +613,7 @@ Return Value: return SocketFd; } -int UtilCreateProcessAndWait(const char* const File, const char* const Argv[], int* Status, const std::map& Env) +int UtilCreateProcessAndWait(const char* const File, const char* const Argv[], int* Status, const std::map& Env, bool DetachTerminal) /*++ @@ -630,6 +630,9 @@ Routine Description: Status - Supplies an optional pointer that receives the exit status of the process. + DetachTerminal - Supplies a boolean that, when true, calls setsid() in the + child process to detach it from the controlling terminal. + Return Value: 0 on success, -1 on failure. @@ -664,7 +667,7 @@ Return Value: if (UtilSetSignalHandlers(g_SavedSignalActions, false) < 0 || UtilRestoreBlockedSignals() < 0) { - exit(-1); + _exit(-1); } // @@ -676,6 +679,19 @@ Return Value: setenv(e.first.c_str(), e.second.c_str(), 1); } + // + // Detach from the controlling terminal if requested. + // + + if (DetachTerminal) + { + if (setsid() == -1) + { + LOG_ERROR("setsid failed {}", errno); + _exit(-1); + } + } + // // Invoke the executable. // @@ -686,7 +702,7 @@ Return Value: // with std::string anyway. execv(File, const_cast(Argv)); LOG_ERROR("execv({}) failed with {}", File, errno); - exit(-1); + _exit(-1); } if (Status == nullptr) diff --git a/src/linux/init/util.h b/src/linux/init/util.h index 892500b12..1fa5414d5 100644 --- a/src/linux/init/util.h +++ b/src/linux/init/util.h @@ -191,7 +191,8 @@ Return Value: _exit(1); } -int UtilCreateProcessAndWait(const char* File, const char* const Argv[], int* Status = nullptr, const std::map& Env = {}); +int UtilCreateProcessAndWait( + const char* File, const char* const Argv[], int* Status = nullptr, const std::map& Env = {}, bool DetachTerminal = false); template void UtilCreateWorkerThread(const char* Name, TMethod&& ThreadFunction) diff --git a/src/shared/inc/lxinitshared.h b/src/shared/inc/lxinitshared.h index 14bf7b886..0e0ba26e0 100644 --- a/src/shared/inc/lxinitshared.h +++ b/src/shared/inc/lxinitshared.h @@ -302,7 +302,6 @@ typedef enum _LX_MESSAGE_TYPE LxInitMessageAddVirtioFsDevice, LxInitMessageAddVirtioFsDeviceResponse, LxInitMessageRemountVirtioFsDevice, - LxInitMessageQueryVirtioFsDevice, LxInitMessageStartDistroInit, LxInitMessageCreateLoginSession, LxInitMessageStopPlan9Server, @@ -1073,9 +1072,10 @@ typedef struct _LX_INIT_ADD_VIRTIOFS_SHARE_RESPONSE_MESSAGE MESSAGE_HEADER Header; int Result; unsigned int TagOffset; + unsigned int SourceOffset; char Buffer[]; - PRETTY_PRINT(FIELD(Header), FIELD(Result), STRING_FIELD(TagOffset)); + PRETTY_PRINT(FIELD(Header), FIELD(Result), STRING_FIELD(TagOffset), STRING_FIELD(SourceOffset)); } LX_INIT_ADD_VIRTIOFS_SHARE_RESPONSE_MESSAGE, *PLX_INIT_ADD_VIRTIOFS_SHARE_RESPONSE_MESSAGE; typedef struct _LX_INIT_ADD_VIRTIOFS_SHARE_MESSAGE @@ -1105,17 +1105,6 @@ typedef struct _LX_INIT_REMOUNT_VIRTIOFS_SHARE_MESSAGE PRETTY_PRINT(FIELD(Header), FIELD(Admin), STRING_FIELD(TagOffset)); } LX_INIT_REMOUNT_VIRTIOFS_SHARE_MESSAGE, *PLX_INIT_REMOUNT_VIRTIOFS_SHARE_MESSAGE; -typedef struct _LX_INIT_QUERY_VIRTIOFS_SHARE_MESSAGE -{ - static inline auto Type = LxInitMessageQueryVirtioFsDevice; - using TResponse = LX_INIT_ADD_VIRTIOFS_SHARE_RESPONSE_MESSAGE; - - MESSAGE_HEADER Header; - unsigned int TagOffset; - char Buffer[]; - - PRETTY_PRINT(FIELD(Header), STRING_FIELD(TagOffset)); -} LX_INIT_QUERY_VIRTIOFS_SHARE_MESSAGE, *PLX_INIT_QUERY_VIRTIOFS_SHARE_MESSAGE; // // The messages that can be sent to mini_init. // diff --git a/src/shared/inc/socketshared.h b/src/shared/inc/socketshared.h index d07916d6b..1c9129180 100644 --- a/src/shared/inc/socketshared.h +++ b/src/shared/inc/socketshared.h @@ -64,6 +64,15 @@ try #endif } + if (MessageSize > 4 * 1024 * 1024) // 4 MiB + { +#if defined(_MSC_VER) + THROW_HR_MSG(E_UNEXPECTED, "Message size too large: %llu", MessageSize); +#elif defined(__GNUC__) + THROW_UNEXCEPTED(); +#endif + } + if (Buffer.size() < MessageSize) { Buffer.resize(MessageSize); diff --git a/src/windows/common/CMakeLists.txt b/src/windows/common/CMakeLists.txt index d3cdeeb82..6dbe4c0b1 100644 --- a/src/windows/common/CMakeLists.txt +++ b/src/windows/common/CMakeLists.txt @@ -45,6 +45,7 @@ set(SOURCES WslTelemetry.cpp VirtioNetworking.cpp wslutil.cpp + install.cpp notifications.cpp) set(HEADERS diff --git a/src/windows/common/VirtioNetworking.cpp b/src/windows/common/VirtioNetworking.cpp index b150b94ef..652f64987 100644 --- a/src/windows/common/VirtioNetworking.cpp +++ b/src/windows/common/VirtioNetworking.cpp @@ -56,7 +56,7 @@ void VirtioNetworking::TraceLoggingRundown() noexcept void VirtioNetworking::FillInitialConfiguration(LX_MINI_INIT_NETWORKING_CONFIGURATION& message) { message.NetworkingMode = LxMiniInitNetworkingModeVirtioProxy; - message.DisableIpv6 = false; + message.DisableIpv6 = WI_IsFlagClear(m_flags, VirtioNetworkingFlags::Ipv6); message.EnableDhcpClient = false; message.PortTrackerType = LX_MINI_INIT_PORT_TRACKER_TYPE::LxMiniInitPortTrackerTypeMirrored; } @@ -78,6 +78,11 @@ void NETIOAPI_API_ VirtioNetworking::OnNetworkConnectivityChange(PVOID context, HRESULT VirtioNetworking::HandlePortNotification(const SOCKADDR_INET& addr, int protocol, bool allocate) const noexcept { + if (addr.si_family == AF_INET6 && WI_IsFlagClear(m_flags, VirtioNetworkingFlags::Ipv6)) + { + return S_OK; + } + int result = 0; const auto ipAddress = (addr.si_family == AF_INET) ? reinterpret_cast(&addr.Ipv4.sin_addr) : reinterpret_cast(&addr.Ipv6.sin6_addr); @@ -132,19 +137,12 @@ int VirtioNetworking::ModifyOpenPorts(_In_ PCWSTR tag, _In_ const SOCKADDR_INET& LOG_HR_MSG(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), "Unsupported bind protocol %d", protocol); return 0; } - else if (addr.si_family == AF_INET6) - { - // The virtio net adapter does not yet support IPv6 packets, so any traffic would arrive via - // IPv4. If the caller wants IPv4 they will also likely listen on an IPv4 address, which will - // be handled as a separate callback to this same code. - return 0; - } auto lock = m_lock.lock_exclusive(); const auto server = m_guestDeviceManager->GetRemoteFileSystem(VIRTIO_NET_CLASS_ID, c_defaultDeviceTag); if (server) { - std::wstring portString = std::format(L"tag={};port_number={}", tag, addr.Ipv4.sin_port); + std::wstring portString = std::format(L"tag={};port_number={}", tag, INETADDR_PORT(reinterpret_cast(&addr))); if (protocol == IPPROTO_UDP) { portString += L";udp"; @@ -185,7 +183,13 @@ try std::wstring default_route = networkSettings->GetBestGatewayAddressString(); appendOption(L"gateway_ip", default_route); - appendOption(L"gateway_mac", networkSettings->GetBestGatewayMacAddress()); + appendOption(L"gateway_mac", networkSettings->GetBestGatewayMacAddress(AF_INET)); + + if (WI_IsFlagSet(m_flags, VirtioNetworkingFlags::Ipv6)) + { + appendOption(L"client_ip_ipv6", networkSettings->PreferredIpv6Address.AddressString); + appendOption(L"gateway_mac_ipv6", networkSettings->GetBestGatewayMacAddress(AF_INET6)); + } networking::DnsInfo currentDns{}; if (WI_IsFlagSet(m_flags, VirtioNetworkingFlags::DnsTunneling)) @@ -194,7 +198,9 @@ try } else { - currentDns = networking::HostDnsInfo::GetDnsSettings(networking::DnsSettingsFlags::IncludeVpn); + wsl::core::networking::DnsSettingsFlags dnsFlags = networking::DnsSettingsFlags::IncludeVpn; + WI_SetFlagIf(dnsFlags, networking::DnsSettingsFlags::IncludeIpv6Servers, WI_IsFlagSet(m_flags, VirtioNetworkingFlags::Ipv6)); + currentDns = networking::HostDnsInfo::GetDnsSettings(dnsFlags); } const auto minMtu = GetMinimumConnectedInterfaceMtu(); @@ -221,32 +227,16 @@ try } } - // Update IP address if needed. - if (!m_networkSettings || networkSettings->PreferredIpAddress != m_networkSettings->PreferredIpAddress) + UpdateIpv4Address(networkSettings->PreferredIpAddress); + if (WI_IsFlagSet(m_flags, VirtioNetworkingFlags::Ipv6)) { - UpdateIpAddress(networkSettings->PreferredIpAddress); + UpdateIpv6Address(networkSettings->PreferredIpv6Address); } - // Send default route update if needed. - if (default_route != m_trackedDefaultRoute) - { - m_trackedDefaultRoute = default_route; - UpdateDefaultRoute(default_route, AF_INET); - } - - // Send DNS update if needed. - if (currentDns != m_trackedDnsSettings) - { - m_trackedDnsSettings = currentDns; - UpdateDnsSettings(currentDns); - } + UpdateDefaultRoute(default_route); - // Send MTU update if needed. - if (minMtu && minMtu.value() != m_networkMtu) - { - m_networkMtu = minMtu.value(); - UpdateMtu(m_networkMtu); - } + UpdateDnsSettings(currentDns); + UpdateMtu(minMtu); m_networkSettings = std::move(networkSettings); } @@ -282,27 +272,46 @@ void VirtioNetworking::SetupLoopbackDevice() m_gnsChannel.SendNetworkDeviceMessage(loopbackType, ToJsonW(createLoopbackDevice).c_str()); } -void VirtioNetworking::UpdateDefaultRoute(const std::wstring& gateway, ADDRESS_FAMILY family) +void VirtioNetworking::SendDefaultRoute(const std::wstring& gateway, hns::ModifyRequestType requestType) { - if (gateway.empty()) + if (gateway.empty() || !m_adapterId.has_value()) { return; } wsl::shared::hns::Route route; route.NextHop = gateway; - route.DestinationPrefix = (family == AF_INET) ? LX_INIT_DEFAULT_ROUTE_PREFIX : LX_INIT_DEFAULT_ROUTE_V6_PREFIX; - route.Family = family; + route.DestinationPrefix = LX_INIT_DEFAULT_ROUTE_PREFIX; + route.Family = AF_INET; hns::ModifyGuestEndpointSettingRequest request; - request.RequestType = hns::ModifyRequestType::Add; + request.RequestType = requestType; request.ResourceType = hns::GuestEndpointResourceType::Route; request.Settings = route; m_gnsChannel.SendHnsNotification(ToJsonW(request).c_str(), m_adapterId.value()); } +void VirtioNetworking::UpdateDefaultRoute(const std::wstring& gateway) +{ + if (gateway == m_trackedDefaultRoute || !m_adapterId.has_value()) + { + return; + } + + SendDefaultRoute(m_trackedDefaultRoute, hns::ModifyRequestType::Remove); + m_trackedDefaultRoute = gateway; + SendDefaultRoute(gateway, hns::ModifyRequestType::Add); +} + void VirtioNetworking::UpdateDnsSettings(const networking::DnsInfo& dns) { + if (dns == m_trackedDnsSettings || !m_adapterId.has_value()) + { + return; + } + + m_trackedDnsSettings = dns; + hns::ModifyGuestEndpointSettingRequest notification{}; notification.RequestType = hns::ModifyRequestType::Update; notification.ResourceType = hns::GuestEndpointResourceType::DNS; @@ -310,9 +319,17 @@ void VirtioNetworking::UpdateDnsSettings(const networking::DnsInfo& dns) m_gnsChannel.SendHnsNotification(ToJsonW(notification).c_str(), m_adapterId.value()); } -void VirtioNetworking::UpdateIpAddress(const networking::EndpointIpAddress& ipAddress) +void VirtioNetworking::UpdateIpv4Address(const networking::EndpointIpAddress& ipAddress) { - // N.B. The MAC address is advertised with the virtio device so doesn't need to be explicitly set. + if (ipAddress == m_trackedIpv4Address || ipAddress.AddressString.empty() || !m_adapterId.has_value()) + { + return; + } + + m_trackedIpv4Address = ipAddress; + + // N.B. SendEndpointState triggers SetAdapterConfiguration on the Linux side + // which brings the interface UP and configures the full adapter state. hns::HNSEndpoint endpointProperties; endpointProperties.ID = m_adapterId.value(); endpointProperties.IPAddress = ipAddress.AddressString; @@ -320,12 +337,53 @@ void VirtioNetworking::UpdateIpAddress(const networking::EndpointIpAddress& ipAd m_gnsChannel.SendEndpointState(endpointProperties); } -void VirtioNetworking::UpdateMtu(ULONG mtu) +void VirtioNetworking::SendIpv6Address(const networking::EndpointIpAddress& ipAddress, hns::ModifyRequestType requestType) +{ + WI_ASSERT(WI_IsFlagSet(m_flags, VirtioNetworkingFlags::Ipv6)); + + if (ipAddress.AddressString.empty() || !m_adapterId.has_value()) + { + return; + } + + // The HNSEndpoint schema doesn't support IPv6 addresses, so use ModifyGuestEndpointSettingRequest. + hns::ModifyGuestEndpointSettingRequest request; + request.RequestType = requestType; + request.ResourceType = hns::GuestEndpointResourceType::IPAddress; + request.Settings.Address = ipAddress.AddressString; + request.Settings.Family = ipAddress.Address.si_family; + request.Settings.OnLinkPrefixLength = ipAddress.PrefixLength; + request.Settings.PreferredLifetime = ULONG_MAX; + m_gnsChannel.SendHnsNotification(ToJsonW(request).c_str(), m_adapterId.value()); +} + +void VirtioNetworking::UpdateIpv6Address(const networking::EndpointIpAddress& ipAddress) +{ + WI_ASSERT(WI_IsFlagSet(m_flags, VirtioNetworkingFlags::Ipv6)); + + if (ipAddress == m_trackedIpv6Address || !m_adapterId.has_value()) + { + return; + } + + SendIpv6Address(m_trackedIpv6Address, hns::ModifyRequestType::Remove); + m_trackedIpv6Address = ipAddress; + SendIpv6Address(ipAddress, hns::ModifyRequestType::Add); +} + +void VirtioNetworking::UpdateMtu(std::optional mtu) { + if (!mtu || mtu.value() == m_networkMtu || !m_adapterId.has_value()) + { + return; + } + + m_networkMtu = mtu.value(); + hns::ModifyGuestEndpointSettingRequest notification{}; notification.ResourceType = hns::GuestEndpointResourceType::Interface; notification.RequestType = hns::ModifyRequestType::Update; notification.Settings.Connected = true; - notification.Settings.NlMtu = mtu; + notification.Settings.NlMtu = m_networkMtu; m_gnsChannel.SendHnsNotification(ToJsonW(notification).c_str(), m_adapterId.value()); } diff --git a/src/windows/common/VirtioNetworking.h b/src/windows/common/VirtioNetworking.h index 697524a81..c0fdb27b7 100644 --- a/src/windows/common/VirtioNetworking.h +++ b/src/windows/common/VirtioNetworking.h @@ -15,6 +15,7 @@ enum class VirtioNetworkingFlags None = 0x0, LocalhostRelay = 0x1, DnsTunneling = 0x2, + Ipv6 = 0x4, }; DEFINE_ENUM_FLAG_OPERATORS(VirtioNetworkingFlags); @@ -43,10 +44,13 @@ class VirtioNetworking : public INetworkingEngine int ModifyOpenPorts(_In_ PCWSTR tag, _In_ const SOCKADDR_INET& addr, _In_ int protocol, _In_ bool isOpen) const; void RefreshGuestConnection() noexcept; void SetupLoopbackDevice(); - void UpdateDefaultRoute(const std::wstring& gateway, ADDRESS_FAMILY family); + void SendDefaultRoute(const std::wstring& gateway, wsl::shared::hns::ModifyRequestType requestType); + void SendIpv6Address(const networking::EndpointIpAddress& ipAddress, wsl::shared::hns::ModifyRequestType requestType); + void UpdateDefaultRoute(const std::wstring& gateway); void UpdateDnsSettings(const networking::DnsInfo& dns); - void UpdateIpAddress(const networking::EndpointIpAddress& ipAddress); - void UpdateMtu(ULONG mtu); + void UpdateIpv4Address(const networking::EndpointIpAddress& ipAddress); + void UpdateIpv6Address(const networking::EndpointIpAddress& ipAddress); + void UpdateMtu(std::optional mtu); mutable wil::srwlock m_lock; @@ -62,6 +66,8 @@ class VirtioNetworking : public INetworkingEngine ULONG m_networkMtu = 0; std::wstring m_trackedDeviceOptions; + networking::EndpointIpAddress m_trackedIpv4Address{}; + networking::EndpointIpAddress m_trackedIpv6Address{}; std::wstring m_trackedDefaultRoute; networking::DnsInfo m_trackedDnsSettings{}; diff --git a/src/windows/common/WslClient.cpp b/src/windows/common/WslClient.cpp index 72daeecae..e1f3a0f1f 100644 --- a/src/windows/common/WslClient.cpp +++ b/src/windows/common/WslClient.cpp @@ -13,6 +13,7 @@ Module Name: --*/ #include "precomp.h" +#include "install.h" #include "WslInstall.h" #include "HandleConsoleProgressBar.h" #include "Distribution.h" @@ -156,7 +157,7 @@ int BashMain(_In_ std::wstring_view commandLine) // Call the MSI package if we're in an MSIX context if (wsl::windows::common::wslutil::IsRunningInMsix()) { - return wsl::windows::common::wslutil::CallMsiPackage(); + return wsl::windows::common::install::CallMsiPackage(); } const auto options = ParseLegacyArguments(commandLine); @@ -1265,7 +1266,7 @@ int UpdatePackage(std::wstring_view commandLine) parser.AddArgument(NoOp(), WSL_UPDATE_ARG_PROMPT_OPTION_LONG); parser.Parse(); - return wsl::windows::common::wslutil::UpdatePackage(preRelease, false); + return wsl::windows::common::install::UpdatePackage(preRelease, false); } int Uninstall() @@ -1274,7 +1275,7 @@ int Uninstall() auto clearLogs = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&logFile]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFile(logFile.c_str())); }); - const auto exitCode = wsl::windows::common::wslutil::UninstallViaMsi(logFile.c_str(), &wsl::windows::common::wslutil::MsiMessageCallback); + const auto exitCode = wsl::windows::common::install::UninstallViaMsi(logFile.c_str(), &wsl::windows::common::install::MsiMessageCallback); if (exitCode != 0) { @@ -1311,7 +1312,7 @@ int WslconfigMain(_In_ int argc, _In_reads_(argc) LPWSTR* argv) // Call the MSI package if we're in an MSIX context if (wsl::windows::common::wslutil::IsRunningInMsix()) { - return wsl::windows::common::wslutil::CallMsiPackage(); + return wsl::windows::common::install::CallMsiPackage(); } using wsl::shared::string::IsEqual; @@ -1524,7 +1525,7 @@ int WslMain(_In_ std::wstring_view commandLine) // Call the MSI package if we're in an MSIX context if (wsl::windows::common::wslutil::IsRunningInMsix()) { - return wsl::windows::common::wslutil::CallMsiPackage(); + return wsl::windows::common::install::CallMsiPackage(); } // Use exit code -1 so invokers of wsl.exe can distinguish between a Linux @@ -1750,6 +1751,15 @@ int WslMain(_In_ std::wstring_view commandLine) } else if (argument == WSL_UNINSTALL_ARG) { + commandLine = wsl::windows::common::helpers::ConsumeArgument(commandLine, argument); + argument = wsl::windows::common::helpers::ParseArgument(commandLine); + if (!argument.empty()) + { + wsl::windows::common::wslutil::PrintMessage( + Localization::MessageUninstallNoArguments(WSL_UNINSTALL_ARG, WSL_UNREGISTER_ARG), stdout); + return exitCode; + } + return Uninstall(); } else diff --git a/src/windows/common/WslCoreNetworkEndpointSettings.cpp b/src/windows/common/WslCoreNetworkEndpointSettings.cpp index 37c0cc904..89737c01d 100644 --- a/src/windows/common/WslCoreNetworkEndpointSettings.cpp +++ b/src/windows/common/WslCoreNetworkEndpointSettings.cpp @@ -24,7 +24,9 @@ std::shared_ptr wsl::core::networking::G return std::make_shared( properties.InterfaceConstraint.InterfaceGuid, address, + EndpointIpAddress{}, route, + EndpointRoute{}, properties.MacAddress, properties.InterfaceConstraint.InterfaceIndex, properties.InterfaceConstraint.InterfaceMediaType); @@ -61,44 +63,84 @@ std::shared_ptr wsl::core::networking::G } if (firstIpv4Address) { - address.Address = *reinterpret_cast(firstIpv4Address->Address.lpSockaddr); + address.Address.Ipv4 = *reinterpret_cast(firstIpv4Address->Address.lpSockaddr); address.AddressString = windows::common::string::SockAddrInetToWstring(address.Address); address.PrefixLength = firstIpv4Address->OnLinkPrefixLength; } - EndpointRoute route{}; - PIP_ADAPTER_GATEWAY_ADDRESS nextGatewayAddress = bestInterface->FirstGatewayAddress; - while (nextGatewayAddress && nextGatewayAddress->Address.lpSockaddr->sa_family != AF_INET) + // Find the first global-scope (non-link-local) IPv6 unicast address. + EndpointIpAddress ipv6Address{}; + auto nextUnicastAddress = bestInterface->FirstUnicastAddress; + while (nextUnicastAddress) { - nextGatewayAddress = nextGatewayAddress->Next; + if (nextUnicastAddress->Address.lpSockaddr->sa_family == AF_INET6) + { + const auto& sin6 = *reinterpret_cast(nextUnicastAddress->Address.lpSockaddr); + if (!IN6_IS_ADDR_LINKLOCAL(&sin6.sin6_addr) && !IN6_IS_ADDR_LOOPBACK(&sin6.sin6_addr)) + { + ipv6Address.Address.Ipv6 = sin6; + ipv6Address.AddressString = windows::common::string::SockAddrInetToWstring(ipv6Address.Address); + ipv6Address.PrefixLength = nextUnicastAddress->OnLinkPrefixLength; + break; + } + } + nextUnicastAddress = nextUnicastAddress->Next; } - if (nextGatewayAddress) + + // Helper to find the first gateway address of a given family. + auto findGatewayAddress = [](PIP_ADAPTER_GATEWAY_ADDRESS list, ADDRESS_FAMILY family) -> PIP_ADAPTER_GATEWAY_ADDRESS { + while (list && list->Address.lpSockaddr->sa_family != family) + { + list = list->Next; + } + return list; + }; + + // Build IPv4 default route. + EndpointRoute route{}; + const auto v4Gateway = findGatewayAddress(bestInterface->FirstGatewayAddress, AF_INET); + if (v4Gateway) { - route.DestinationPrefix.PrefixLength = 0; - IN4ADDR_SETANY(&route.DestinationPrefix.Prefix.Ipv4); - route.DestinationPrefixString = LX_INIT_UNSPECIFIED_ADDRESS; - route.NextHop = *reinterpret_cast(nextGatewayAddress->Address.lpSockaddr); - route.NextHopString = windows::common::string::SockAddrInetToWstring(route.NextHop); + SOCKADDR_INET v4NextHop{}; + v4NextHop.Ipv4 = *reinterpret_cast(v4Gateway->Address.lpSockaddr); + route = EndpointRoute::DefaultRoute(AF_INET, v4NextHop); } else if (address.Address.si_family == AF_INET) { - IN_ADDR default_route{}; - default_route.s_addr = htonl((ntohl(address.Address.Ipv4.sin_addr.s_addr) & ~((1 << (32 - address.PrefixLength)) - 1)) | 1); - route.DestinationPrefix.PrefixLength = 0; - IN4ADDR_SETANY(&route.DestinationPrefix.Prefix.Ipv4); - route.DestinationPrefixString = LX_INIT_UNSPECIFIED_ADDRESS; - IN4ADDR_SETSOCKADDR(&route.NextHop.Ipv4, &default_route, 0); - route.NextHopString = windows::common::string::SockAddrInetToWstring(route.NextHop); + // Synthesize a gateway from the first host address in the subnet. + SOCKADDR_INET gatewayAddr{}; + gatewayAddr.si_family = AF_INET; + const uint32_t hostAddr = ntohl(address.Address.Ipv4.sin_addr.s_addr); + const uint32_t mask = (address.PrefixLength == 0) ? 0u : ~((1u << (32u - address.PrefixLength)) - 1u); + gatewayAddr.Ipv4.sin_addr.s_addr = htonl((hostAddr & mask) | 1u); + route = EndpointRoute::DefaultRoute(AF_INET, gatewayAddr); + } + + // Build IPv6 default route. + EndpointRoute v6Route{}; + const auto v6Gateway = findGatewayAddress(bestInterface->FirstGatewayAddress, AF_INET6); + if (v6Gateway) + { + SOCKADDR_INET v6NextHop{}; + v6NextHop.Ipv6 = *reinterpret_cast(v6Gateway->Address.lpSockaddr); + v6Route = EndpointRoute::DefaultRoute(AF_INET6, v6NextHop); } return std::make_shared( - bestInterface->NetworkGuid, address, route, macAddress, bestInterface->IfIndex, bestInterface->IfType); + bestInterface->NetworkGuid, + std::move(address), + std::move(ipv6Address), + std::move(route), + std::move(v6Route), + std::move(macAddress), + bestInterface->IfIndex, + bestInterface->IfType); } -std::wstring wsl::core::networking::NetworkSettings::GetBestGatewayMacAddress() const +std::wstring wsl::core::networking::NetworkSettings::GetBestGatewayMacAddress(ADDRESS_FAMILY addressFamily) const { - auto gatewayAddress = GetBestGatewayAddress(); - if (gatewayAddress.si_family != AF_INET) + auto gatewayAddress = GetBestGatewayAddress(addressFamily); + if (gatewayAddress.si_family != addressFamily) { return {}; } diff --git a/src/windows/common/WslCoreNetworkEndpointSettings.h b/src/windows/common/WslCoreNetworkEndpointSettings.h index 227fb63e9..0dd3a92a4 100644 --- a/src/windows/common/WslCoreNetworkEndpointSettings.h +++ b/src/windows/common/WslCoreNetworkEndpointSettings.h @@ -172,6 +172,27 @@ struct EndpointRoute EndpointRoute(const EndpointRoute&) = default; EndpointRoute& operator=(const EndpointRoute&) = default; + // Build a default route (0.0.0.0/0 or ::/0) with the given next hop. + static EndpointRoute DefaultRoute(ADDRESS_FAMILY family, const SOCKADDR_INET& nextHop) + { + EndpointRoute route{}; + route.Family = family; + route.DestinationPrefix.PrefixLength = 0; + if (family == AF_INET) + { + IN4ADDR_SETANY(&route.DestinationPrefix.Prefix.Ipv4); + route.DestinationPrefixString = LX_INIT_UNSPECIFIED_ADDRESS; + } + else + { + IN6ADDR_SETANY(&route.DestinationPrefix.Prefix.Ipv6); + route.DestinationPrefixString = LX_INIT_UNSPECIFIED_V6_ADDRESS; + } + route.NextHop = nextHop; + route.NextHopString = windows::common::string::SockAddrInetToWstring(nextHop); + return route; + } + EndpointRoute(const MIB_IPFORWARD_ROW2& RouteRow) : Family(RouteRow.NextHop.si_family), DestinationPrefix(RouteRow.DestinationPrefix), @@ -258,19 +279,39 @@ struct NetworkSettings { NetworkSettings() = default; - NetworkSettings(const GUID& interfaceGuid, EndpointIpAddress preferredIpAddress, EndpointRoute gateway, std::wstring macAddress, uint32_t interfaceIndex, uint32_t mediaType) : + NetworkSettings( + const GUID& interfaceGuid, + EndpointIpAddress preferredIpAddress, + EndpointIpAddress preferredIpv6Address, + EndpointRoute gateway, + EndpointRoute v6Gateway, + std::wstring macAddress, + uint32_t interfaceIndex, + uint32_t mediaType) : InterfaceGuid(interfaceGuid), PreferredIpAddress(std::move(preferredIpAddress)), + PreferredIpv6Address(std::move(preferredIpv6Address)), MacAddress(std::move(macAddress)), InterfaceIndex(interfaceIndex), InterfaceType(mediaType) { - Routes.emplace(std::move(gateway)); + // Only insert routes that have a valid next hop. A default-constructed or empty + // EndpointRoute indicates no gateway was found for that address family. + if (!gateway.NextHopString.empty()) + { + Routes.emplace(std::move(gateway)); + } + + if (!v6Gateway.NextHopString.empty()) + { + Routes.emplace(std::move(v6Gateway)); + } } GUID InterfaceGuid{}; EndpointIpAddress PreferredIpAddress{}; - std::set IpAddresses{}; // Does not include PreferredIpAddress. + EndpointIpAddress PreferredIpv6Address{}; + std::set IpAddresses{}; // Does not include PreferredIpAddress or PreferredIpv6Address. std::set Routes{}; std::wstring MacAddress; IF_INDEX InterfaceIndex = 0; @@ -290,12 +331,13 @@ struct NetworkSettings auto operator<=>(const NetworkSettings&) const = default; - std::wstring GetBestGatewayAddressString() const + // Returns the next-hop string of the first default route matching the given address family. + std::wstring GetBestGatewayAddressString(ADDRESS_FAMILY family = AF_INET) const { - // Best is currently defined as simply the first IPv4 gateway. + const auto& unspecified = (family == AF_INET) ? LX_INIT_UNSPECIFIED_ADDRESS : LX_INIT_UNSPECIFIED_V6_ADDRESS; for (const auto& route : Routes) { - if (route.Family == AF_INET && route.DestinationPrefix.PrefixLength == 0 && route.DestinationPrefixString == LX_INIT_UNSPECIFIED_ADDRESS) + if (route.Family == family && route.DestinationPrefix.PrefixLength == 0 && route.DestinationPrefixString == unspecified) { return route.NextHopString; } @@ -304,12 +346,13 @@ struct NetworkSettings return {}; } - SOCKADDR_INET GetBestGatewayAddress() const + // Returns the next-hop address of the first default route matching the given address family. + SOCKADDR_INET GetBestGatewayAddress(ADDRESS_FAMILY family = AF_INET) const { - // Best is currently defined as simply the first IPv4 gateway. + const auto& unspecified = (family == AF_INET) ? LX_INIT_UNSPECIFIED_ADDRESS : LX_INIT_UNSPECIFIED_V6_ADDRESS; for (const auto& route : Routes) { - if (route.Family == AF_INET && route.DestinationPrefix.PrefixLength == 0 && route.DestinationPrefixString == LX_INIT_UNSPECIFIED_ADDRESS) + if (route.Family == family && route.DestinationPrefix.PrefixLength == 0 && route.DestinationPrefixString == unspecified) { return route.NextHop; } @@ -318,7 +361,7 @@ struct NetworkSettings return {}; } - std::wstring GetBestGatewayMacAddress() const; + std::wstring GetBestGatewayMacAddress(ADDRESS_FAMILY addressFamily) const; std::wstring IpAddressesString() const { @@ -369,6 +412,9 @@ std::shared_ptr GetHostEndpointSettings(); TraceLoggingValue((settings)->GetBestGatewayAddressString().c_str(), "bestGatewayAddress"), \ TraceLoggingValue((settings)->PreferredIpAddress.AddressString.c_str(), "preferredIpAddress"), \ TraceLoggingValue((settings)->PreferredIpAddress.PrefixLength, "preferredIpAddressPrefixLength"), \ + TraceLoggingValue((settings)->PreferredIpv6Address.AddressString.c_str(), "preferredIpv6Address"), \ + TraceLoggingValue((settings)->PreferredIpv6Address.PrefixLength, "preferredIpv6AddressPrefixLength"), \ + TraceLoggingValue((settings)->GetBestGatewayAddressString(AF_INET6).c_str(), "bestGatewayV6Address"), \ TraceLoggingValue((settings)->IpAddressesString().c_str(), "ipAddresses"), \ TraceLoggingValue((settings)->RoutesString().c_str(), "routes"), \ TraceLoggingValue((settings)->MacAddress.c_str(), "macAddress"), \ diff --git a/src/windows/common/WslCoreNetworkingSupport.h b/src/windows/common/WslCoreNetworkingSupport.h index f66a74234..106167075 100644 --- a/src/windows/common/WslCoreNetworkingSupport.h +++ b/src/windows/common/WslCoreNetworkingSupport.h @@ -544,7 +544,7 @@ class AdapterAddresses : public std::enable_shared_from_this m_buffer.resize(BufferSize); Result = GetAdaptersAddresses( AF_UNSPEC, - (GAA_FLAG_SKIP_FRIENDLY_NAME | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST), + (GAA_FLAG_SKIP_FRIENDLY_NAME | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS), nullptr, (PIP_ADAPTER_ADDRESSES)m_buffer.data(), &BufferSize); diff --git a/src/windows/common/filesystem.cpp b/src/windows/common/filesystem.cpp index a4418d552..2c6c585d6 100644 --- a/src/windows/common/filesystem.cpp +++ b/src/windows/common/filesystem.cpp @@ -1115,118 +1115,6 @@ void wsl::windows::common::filesystem::UpdateInit(_In_ PCWSTR BasePath, _In_ ULO CopyFileWithMetadata(source.c_str(), dest.c_str(), (LX_S_IFREG | 0755), DistroVersion); } -void wsl::windows::common::filesystem::CreateCpioInitrd(_In_ const std::filesystem::path& SourcePath, _In_ const std::filesystem::path& DestPath) -{ - // Open the source init binary - wil::unique_hfile sourceFile{ - CreateFileW(SourcePath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)}; - THROW_LAST_ERROR_IF(!sourceFile); - - // Get file size - LARGE_INTEGER fileSize{}; - THROW_IF_WIN32_BOOL_FALSE(GetFileSizeEx(sourceFile.get(), &fileSize)); - THROW_HR_IF(E_INVALIDARG, fileSize.HighPart != 0); // File too large - - const DWORD initSize = fileSize.LowPart; - const auto initDataPadding = (4 - (initSize % 4)) % 4; - wil::unique_hfile destFile{CreateFileW(DestPath.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)}; - THROW_LAST_ERROR_IF(!destFile); - - // Clean up the destination file on failure. - auto deleteFile = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] { - destFile.reset(); - LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(DestPath.c_str())); - }); - - auto writeCpioHeader = [&destFile](DWORD fileSize, PCSTR name) { - // CPIO newc header: magic(6) + 13 fields of 8 hex chars each = 110 bytes - constexpr size_t headerSize = 110; - const auto nameLen = strlen(name) + 1; - const auto headerPadding = (4 - ((headerSize + nameLen) % 4)) % 4; - const bool isTrailer = strcmp(name, "TRAILER!!!") == 0; - - // Get current time for mtime. CPIO newc format only supports 32-bit fields. - const auto mtime = static_cast(time(nullptr)); - - char header[headerSize + 1]; - sprintf_s( - header, - "070701" // magic - "%08X" // inode - "%08X" // mode - "%08X" // uid - "%08X" // gid - "%08X" // nlink - "%08X" // mtime - "%08X" // filesize - "%08X" // devmajor - "%08X" // devminor - "%08X" // rdevmajor - "%08X" // rdevminor - "%08X" // namesize - "%08X", // check - 0, - isTrailer ? 0 : 0100755, - 0, - 0, - isTrailer ? 0 : 1, - mtime, - fileSize, - 0, - 0, - 0, - 0, - static_cast(nameLen), - 0); - - DWORD bytesWritten; - THROW_IF_WIN32_BOOL_FALSE(WriteFile(destFile.get(), header, headerSize, &bytesWritten, nullptr)); - - // Write name with null terminator - THROW_IF_WIN32_BOOL_FALSE(WriteFile(destFile.get(), name, static_cast(nameLen), &bytesWritten, nullptr)); - - // Write padding to align to 4 bytes - if (headerPadding > 0) - { - char padding[4] = {0}; - THROW_IF_WIN32_BOOL_FALSE(WriteFile(destFile.get(), padding, static_cast(headerPadding), &bytesWritten, nullptr)); - } - }; - - // Write init file header - writeCpioHeader(initSize, SourcePath.filename().string().c_str()); - - // Copy file contents - wsl::windows::common::relay::InterruptableRelay(sourceFile.get(), destFile.get(), nullptr, 64 * 1024); - - // Write data padding - if (initDataPadding > 0) - { - char padding[4] = {0}; - DWORD bytesWritten; - THROW_IF_WIN32_BOOL_FALSE(WriteFile(destFile.get(), padding, static_cast(initDataPadding), &bytesWritten, nullptr)); - } - - // Write trailer entry (empty file with name "TRAILER!!!") - writeCpioHeader(0, "TRAILER!!!"); - - // Pad the archive to 512-byte boundary - constexpr DWORD archiveBlockSize = 512; - LARGE_INTEGER currentPos{}; - THROW_IF_WIN32_BOOL_FALSE(SetFilePointerEx(destFile.get(), {}, ¤tPos, FILE_CURRENT)); - - const auto currentSize = static_cast(currentPos.QuadPart); - const auto archivePadding = static_cast((archiveBlockSize - (currentSize % archiveBlockSize)) % archiveBlockSize); - if (archivePadding > 0) - { - char paddingBuffer[archiveBlockSize] = {0}; - DWORD bytesWritten; - THROW_IF_WIN32_BOOL_FALSE(WriteFile(destFile.get(), paddingBuffer, archivePadding, &bytesWritten, nullptr)); - } - - deleteFile.release(); -} - wil::unique_hfile wsl::windows::common::filesystem::WipeAndOpenDirectory(_In_ LPCWSTR pPath) { const auto result = wil::RemoveDirectoryRecursiveNoThrow(pPath); diff --git a/src/windows/common/filesystem.hpp b/src/windows/common/filesystem.hpp index 865f6ca3f..1a89f8b8f 100644 --- a/src/windows/common/filesystem.hpp +++ b/src/windows/common/filesystem.hpp @@ -228,11 +228,6 @@ std::wstring UnquotePath(_In_ LPCWSTR Path); /// void UpdateInit(_In_ PCWSTR BasePath, _In_ ULONG DistroVersion); -/// -/// Creates a CPIO newc format initrd archive from a single source file. -/// -void CreateCpioInitrd(_In_ const std::filesystem::path& SourcePath, _In_ const std::filesystem::path& DestPath); - /// /// Wipes out the directory with the given path if it exists, then creates it again and returns /// an open directory handle onto it. diff --git a/src/windows/common/install.cpp b/src/windows/common/install.cpp new file mode 100644 index 000000000..740e382d2 --- /dev/null +++ b/src/windows/common/install.cpp @@ -0,0 +1,455 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + install.cpp + +Abstract: + + This file contains MSI/Wintrust install helper functions. + Split from wslutil.cpp to avoid pulling msi.dll/wintrust.dll + into targets that don't need them. + +--*/ + +#include "precomp.h" +#include "install.h" +#include "wslutil.h" +#include "WslPluginApi.h" +#include "wslinstallerservice.h" + +#include "ConsoleProgressBar.h" +#include "ExecutionContext.h" +#include "MsiQuery.h" + +using winrt::Windows::Foundation::Uri; +using winrt::Windows::Management::Deployment::DeploymentOptions; +using wsl::shared::Localization; +using wsl::windows::common::Context; +using namespace wsl::windows::common::registry; +using namespace wsl::windows::common::wslutil; +using namespace wsl::windows::common::install; + +namespace { + +bool PromptForKeyPress() +{ + THROW_IF_WIN32_BOOL_FALSE(FlushConsoleInputBuffer(GetStdHandle(STD_INPUT_HANDLE))); + + // Note: Ctrl-c causes _getch to return 0x3. + return _getch() != 0x3; +} + +bool PromptForKeyPressWithTimeout() +{ + // Run PromptForKeyPress on a separate thread so we can apply a timeout. + // If PromptForKeyPress fails, fulfill the promise with false so the caller doesn't hang. + std::promise pressedKey; + auto thread = std::thread([&pressedKey]() { + try + { + pressedKey.set_value(PromptForKeyPress()); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + try + { + pressedKey.set_value(false); + } + CATCH_LOG() + } + }); + + auto cancelRead = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&thread]() { + if (thread.joinable()) + { + LOG_IF_WIN32_BOOL_FALSE(CancelSynchronousIo(thread.native_handle())); + thread.join(); + } + }); + + auto future = pressedKey.get_future(); + const auto waitResult = future.wait_for(std::chrono::minutes(1)); + + return waitResult == std::future_status::ready && future.get(); +} + +int UpdatePackageImpl(bool preRelease, bool repair) +{ + if (!repair) + { + PrintMessage(Localization::MessageCheckingForUpdates()); + } + + auto [version, release] = GetLatestGitHubRelease(preRelease); + + if (!repair && ParseWslPackageVersion(version) <= wsl::shared::PackageVersion) + { + PrintMessage(Localization::MessageUpdateNotNeeded()); + return 0; + } + + PrintMessage(Localization::MessageUpdatingToVersion(version.c_str())); + + const bool msiInstall = wsl::shared::string::EndsWith(release.name, L".msi"); + const auto downloadPath = DownloadFile(release.url, release.name); + if (msiInstall) + { + auto logFile = std::filesystem::temp_directory_path() / L"wsl-install-logs.txt"; + auto clearLogs = + wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&logFile]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFile(logFile.c_str())); }); + + const auto exitCode = UpgradeViaMsi(downloadPath.c_str(), L"", logFile.c_str(), &MsiMessageCallback); + + if (exitCode != 0) + { + clearLogs.release(); + THROW_HR_WITH_USER_ERROR( + HRESULT_FROM_WIN32(exitCode), + wsl::shared::Localization::MessageUpdateFailed(exitCode) + L"\r\n" + + wsl::shared::Localization::MessageSeeLogFile(logFile.c_str())); + } + } + else + { + // Set FILE_FLAG_DELETE_ON_CLOSE on the file to make sure it's deleted when the installation completes. + const wil::unique_hfile package{CreateFileW( + downloadPath.c_str(), DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_DELETE_ON_CLOSE, nullptr)}; + + THROW_LAST_ERROR_IF(!package); + + const winrt::Windows::Management::Deployment::PackageManager packageManager; + const auto result = packageManager.AddPackageAsync( + Uri{downloadPath.c_str()}, nullptr, DeploymentOptions::ForceApplicationShutdown | DeploymentOptions::ForceTargetApplicationShutdown); + + THROW_IF_FAILED(result.get().ExtendedErrorCode()); + + // Note: If the installation is successful, this process is expected to receive and Ctrl-C and exit + } + + return 0; +} + +void WaitForMsiInstall() +{ + wil::com_ptr_t installer; + + auto retry_pred = []() { + const auto errorCode = wil::ResultFromCaughtException(); + return errorCode == REGDB_E_CLASSNOTREG; + }; + + wsl::shared::retry::RetryWithTimeout( + [&installer]() { installer = wil::CoCreateInstance(__uuidof(WslInstaller), CLSCTX_LOCAL_SERVER); }, + std::chrono::seconds(1), + std::chrono::minutes(1), + retry_pred); + + fputws(wsl::shared::Localization::MessageFinishMsiInstallation().c_str(), stderr); + + auto finishLine = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { fputws(L"\n", stderr); }); + + UINT exitCode = -1; + wil::unique_cotaskmem_string message{}; + THROW_IF_FAILED(installer->Install(&exitCode, &message)); + + if (message && *message.get() != UNICODE_NULL) + { + finishLine.release(); + wprintf(L"\n%ls\n", message.get()); + } + + if (exitCode != 0) + { + THROW_HR_WITH_USER_ERROR(HRESULT_FROM_WIN32(exitCode), wsl::shared::Localization::MessageUpdateFailed(exitCode)); + } +} + +wil::unique_handle CreateJob() +{ + // Create a job object that will terminate all processes in the job on + // close but will not terminate the children of the processes in the job. + // This is used to ensure that when forwarding from an inbox binary (I) + // to a lifted binary (L), if I is terminated L is terminated as well but + // any children of L (e.g. wslhost.exe) continue to run. + wil::unique_handle job{CreateJobObject(nullptr, nullptr)}; + THROW_LAST_ERROR_IF_NULL(job.get()); + + JOBOBJECT_EXTENDED_LIMIT_INFORMATION info{}; + info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK; + THROW_IF_WIN32_BOOL_FALSE(SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation, &info, sizeof(info))); + + return job; +} + +int WINAPI InstallRecordHandler(void* context, UINT messageType, LPCWSTR message) +{ + try + { + WSL_LOG("MSIMessage", TraceLoggingValue(messageType, "type"), TraceLoggingValue(message, "message")); + auto type = (INSTALLMESSAGE)(0xFF000000 & (UINT)messageType); + + if (type == INSTALLMESSAGE_ERROR || type == INSTALLMESSAGE_FATALEXIT || type == INSTALLMESSAGE_WARNING) + { + WriteInstallLog(std::format("MSI message: {}", message)); + } + + auto* callback = reinterpret_cast*>(context); + if (callback != nullptr) + { + (*callback)(type, message); + } + } + CATCH_LOG(); + + return IDOK; +} + +void ConfigureMsiLogging(_In_opt_ LPCWSTR LogFile, _In_ const std::function& Callback) +{ + if (LogFile != nullptr) + { + LOG_IF_WIN32_ERROR(MsiEnableLog(INSTALLLOGMODE_VERBOSE | INSTALLLOGMODE_EXTRADEBUG | INSTALLLOGMODE_PROGRESS, LogFile, 0)); + } + + MsiSetExternalUI( + &InstallRecordHandler, + INSTALLLOGMODE_FATALEXIT | INSTALLLOGMODE_ERROR | INSTALLLOGMODE_WARNING | INSTALLLOGMODE_USER | INSTALLLOGMODE_INFO | + INSTALLLOGMODE_RESOLVESOURCE | INSTALLLOGMODE_OUTOFDISKSPACE | INSTALLLOGMODE_ACTIONSTART | INSTALLLOGMODE_ACTIONDATA | + INSTALLLOGMODE_COMMONDATA | INSTALLLOGMODE_INITIALIZE | INSTALLLOGMODE_TERMINATE | INSTALLLOGMODE_SHOWDIALOG, + (void*)&Callback); + + MsiSetInternalUI(INSTALLUILEVEL(INSTALLUILEVEL_NONE | INSTALLUILEVEL_UACONLY | INSTALLUILEVEL_SOURCERESONLY), nullptr); +} + +} // namespace + +int wsl::windows::common::install::CallMsiPackage() +{ + wsl::windows::common::ExecutionContext context(wsl::windows::common::CallMsi); + + auto msiPath = GetMsiPackagePath(); + if (!msiPath.has_value()) + { + wsl::windows::common::ExecutionContext context(wsl::windows::common::Install); + + try + { + WaitForMsiInstall(); + msiPath = GetMsiPackagePath(); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + + // GetMsiPackagePath() will generate a user error if the registry access fails. + // Save the error from GetMsiPackagePath() to return a proper 'install failed' message. + auto savedError = context.ReportedError(); + + // There is a race where the service might stop before returning the install result. + // if this happens, only fail if the MSI still isn't installed. + msiPath = GetMsiPackagePath(); + if (!msiPath.has_value()) + { + // Offer to directly install the MSI package if the MsixInstaller logic fails + // This can trigger a UAC so only do it + if (IsInteractiveConsole()) + { + auto errorCode = savedError.has_value() ? ErrorToString(savedError.value()).Code + : ErrorCodeToString(wil::ResultFromCaughtException()); + + EMIT_USER_WARNING(wsl::shared::Localization::MessageInstallationCorrupted(errorCode)); + + if (PromptForKeyPressWithTimeout()) + { + return UpdatePackage(false, true); + } + } + + if (savedError.has_value()) + { + THROW_HR_WITH_USER_ERROR(savedError->Code, savedError->Message.value_or(L"")); + } + + throw; + } + } + + THROW_HR_IF(E_UNEXPECTED, !msiPath.has_value()); + } + + auto target = msiPath.value() + L"\\" WSL_BINARY_NAME; + + SubProcess process(target.c_str(), GetCommandLine()); + process.SetDesktopAppPolicy(PROCESS_CREATION_DESKTOP_APP_BREAKAWAY_ENABLE_PROCESS_TREE); + auto runningProcess = process.Start(); + + // N.B. The job cannot be assigned at process creation time as the packaged process + // creation path will assign the new process to a per package job object. + // In the case of multiple processes running in a single package, assigning + // the new process to the per package job object will fail for the second request + // since both jobs already have processes which prevents a job hierarchy from + // being established. + auto job = CreateJob(); + + // Assign the process to the job, ignoring failures when the process has + // terminated. + // + // N.B. Assigning the job after process creation without CREATE_SUSPENDED is + // safe to do here since only the new child process will be in the job + // object. None of the grandchildren processes are included since the + // job is created with JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK. + if (!AssignProcessToJobObject(job.get(), runningProcess.get())) + { + auto lastError = GetLastError(); + if (lastError != ERROR_ACCESS_DENIED) + { + THROW_WIN32(lastError); + } + } + + return static_cast(SubProcess::GetExitCode(runningProcess.get())); +} + +void wsl::windows::common::install::MsiMessageCallback(INSTALLMESSAGE type, LPCWSTR message) +{ + switch (type) + { + case INSTALLMESSAGE_ERROR: + case INSTALLMESSAGE_FATALEXIT: + case INSTALLMESSAGE_WARNING: + wprintf(L"%ls\n", message); + break; + + default: + break; + } +} + +int wsl::windows::common::install::UpdatePackage(bool PreRelease, bool Repair) +{ + // Register a console control handler so "^C" is not printed when the app platform terminates the process. + THROW_IF_WIN32_BOOL_FALSE(SetConsoleCtrlHandler( + [](DWORD ctrlType) { + if (ctrlType == CTRL_C_EVENT) + { + ExitProcess(0); + } + return FALSE; + }, + TRUE)); + + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [] { SetConsoleCtrlHandler(nullptr, FALSE); }); + + try + { + return UpdatePackageImpl(PreRelease, Repair); + } + catch (...) + { + // Rethrowing via WIL is required for the error context to be properly set in case a winrt exception was thrown. + THROW_HR(wil::ResultFromCaughtException()); + } +} + +UINT wsl::windows::common::install::UpgradeViaMsi( + _In_ LPCWSTR PackageLocation, _In_opt_ LPCWSTR ExtraArgs, _In_opt_ LPCWSTR LogFile, _In_ const std::function& Callback) +{ + WriteInstallLog(std::format("Upgrading via MSI package: {}. Args: {}", PackageLocation, ExtraArgs != nullptr ? ExtraArgs : L"")); + + ConfigureMsiLogging(LogFile, Callback); + + auto result = MsiInstallProduct(PackageLocation, ExtraArgs); + WSL_LOG( + "MsiInstallResult", + TraceLoggingValue(result, "result"), + TraceLoggingValue(ExtraArgs != nullptr ? ExtraArgs : L"", "ExtraArgs")); + + WriteInstallLog(std::format("MSI upgrade result: {}", result)); + + return result; +} + +UINT wsl::windows::common::install::UninstallViaMsi(_In_opt_ LPCWSTR LogFile, _In_ const std::function& Callback) +{ + const auto key = OpenLxssMachineKey(KEY_READ); + const auto productCode = ReadString(key.get(), L"Msi", L"ProductCode", nullptr); + + WriteInstallLog(std::format("Uninstalling MSI package: {}", productCode)); + + ConfigureMsiLogging(LogFile, Callback); + + auto result = MsiConfigureProduct(productCode.c_str(), 0, INSTALLSTATE_ABSENT); + WSL_LOG("MsiUninstallResult", TraceLoggingValue(result, "result")); + + WriteInstallLog(std::format("MSI package uninstall result: {}", result)); + + return result; +} + +wil::unique_hfile wsl::windows::common::install::ValidateFileSignature(LPCWSTR Path) +{ + wil::unique_hfile fileHandle{CreateFileW(Path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr)}; + THROW_LAST_ERROR_IF(!fileHandle); + + GUID action = WINTRUST_ACTION_GENERIC_VERIFY_V2; + WINTRUST_DATA trust{}; + trust.cbStruct = sizeof(trust); + trust.dwUIChoice = WTD_UI_NONE; + trust.dwUnionChoice = WTD_CHOICE_FILE; + trust.dwStateAction = WTD_STATEACTION_VERIFY; + + WINTRUST_FILE_INFO file = {0}; + file.cbStruct = sizeof(file); + file.hFile = fileHandle.get(); + trust.pFile = &file; + + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { + trust.dwStateAction = WTD_STATEACTION_CLOSE; + WinVerifyTrust(nullptr, &action, &trust); + }); + + THROW_IF_WIN32_ERROR(WinVerifyTrust(nullptr, &action, &trust)); + + return fileHandle; +} + +void wsl::windows::common::install::WriteInstallLog(const std::string& Content) +try +{ + static std::wstring path = wil::GetWindowsDirectoryW() + L"\\temp\\wsl-install-log.txt"; + + // Wait up to 10 seconds for the log file mutex + wil::unique_handle mutex{CreateMutex(nullptr, true, L"Global\\WslInstallLog")}; + THROW_LAST_ERROR_IF(!mutex); + + THROW_LAST_ERROR_IF(WaitForSingleObject(mutex.get(), 10 * 1000) != WAIT_OBJECT_0); + + wil::unique_handle file{CreateFile( + path.c_str(), GENERIC_ALL, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_ALWAYS, 0, nullptr)}; + + THROW_LAST_ERROR_IF(!file); + + LARGE_INTEGER size{}; + THROW_IF_WIN32_BOOL_FALSE(GetFileSizeEx(file.get(), &size)); + + // Append to the file if its size is below 10MB, otherwise truncate. + if (size.QuadPart < 10 * _1MB) + { + THROW_LAST_ERROR_IF(SetFilePointer(file.get(), 0, nullptr, FILE_END) == INVALID_SET_FILE_POINTER); + } + else + { + THROW_IF_WIN32_BOOL_FALSE(SetEndOfFile(file.get())); + } + + static auto processName = wil::GetModuleFileNameW(); + auto logLine = std::format("{:%FT%TZ} {}[{}]: {}\n", std::chrono::system_clock::now(), processName, WSL_PACKAGE_VERSION, Content); + + DWORD bytesWritten{}; + THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), logLine.c_str(), static_cast(logLine.size()), &bytesWritten, nullptr)); +} +CATCH_LOG(); diff --git a/src/windows/common/install.h b/src/windows/common/install.h new file mode 100644 index 000000000..fae741e04 --- /dev/null +++ b/src/windows/common/install.h @@ -0,0 +1,34 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + install.h + +Abstract: + + This file contains MSI/Wintrust install helper function declarations. + +--*/ + +#pragma once +#include + +namespace wsl::windows::common::install { + +int CallMsiPackage(); + +void MsiMessageCallback(INSTALLMESSAGE type, LPCWSTR message); + +wil::unique_hfile ValidateFileSignature(LPCWSTR Path); + +int UpdatePackage(bool PreRelease, bool Repair); + +UINT UpgradeViaMsi(_In_ LPCWSTR PackageLocation, _In_opt_ LPCWSTR ExtraArgs, _In_opt_ LPCWSTR LogFile, _In_ const std::function& callback); + +UINT UninstallViaMsi(_In_opt_ LPCWSTR LogFile, _In_ const std::function& callback); + +void WriteInstallLog(const std::string& Content); + +} // namespace wsl::windows::common::install diff --git a/src/windows/common/relay.cpp b/src/windows/common/relay.cpp index 6d2dce21f..765120517 100644 --- a/src/windows/common/relay.cpp +++ b/src/windows/common/relay.cpp @@ -16,14 +16,20 @@ Module Name: #include "relay.hpp" #pragma hdrstop +using wsl::windows::common::relay::DockerIORelayHandle; using wsl::windows::common::relay::EventHandle; using wsl::windows::common::relay::HandleWrapper; +using wsl::windows::common::relay::HTTPChunkBasedReadHandle; using wsl::windows::common::relay::IOHandleStatus; +using wsl::windows::common::relay::LineBasedReadHandle; using wsl::windows::common::relay::MultiHandleWait; using wsl::windows::common::relay::OverlappedIOHandle; +using wsl::windows::common::relay::ReadHandle; +using wsl::windows::common::relay::RelayHandle; using wsl::windows::common::relay::ScopedMultiRelay; using wsl::windows::common::relay::ScopedRelay; using wsl::windows::common::relay::SingleAcceptHandle; +using wsl::windows::common::relay::WriteHandle; namespace { @@ -383,6 +389,384 @@ void wsl::windows::common::relay::BidirectionalRelay(_In_ HANDLE LeftHandle, _In } } +#define TTY_ALT_NUMPAD_VK_MENU (0x12) +#define TTY_ESCAPE_CHARACTER (L'\x1b') +#define TTY_INPUT_EVENT_BUFFER_SIZE (16) +#define TTY_UTF8_TRANSLATION_BUFFER_SIZE (4 * TTY_INPUT_EVENT_BUFFER_SIZE) + +BOOL IsActionableKey(_In_ PKEY_EVENT_RECORD KeyEvent) +{ + // + // This is a bit complicated to discern. + // + // 1. Our first check is that we only want structures that + // represent at least one key press. If we have 0, then we don't + // need to bother. If we have >1, we'll send the key through + // that many times into the pipe. + // 2. Our second check is where it gets confusing. + // a. Characters that are non-null get an automatic pass. Copy + // them through to the pipe. + // b. Null characters need further scrutiny. We generally do not + // pass nulls through EXCEPT if they're sourced from the + // virtual terminal engine (or another application living + // above our layer). If they're sourced by a non-keyboard + // source, they'll have no scan code (since they didn't come + // from a keyboard). But that rule has an exception too: + // "Enhanced keys" from above the standard range of scan + // codes will return 0 also with a special flag set that says + // they're an enhanced key. That means the desired behavior + // is: + // Scan Code = 0, ENHANCED_KEY = 0 + // -> This came from the VT engine or another app + // above our layer. + // Scan Code = 0, ENHANCED_KEY = 1 + // -> This came from the keyboard, but is a special + // key like 'Volume Up' that wasn't generally a + // part of historic (pre-1990s) keyboards. + // Scan Code = + // -> This came from a keyboard directly. + // + + if ((KeyEvent->wRepeatCount == 0) || ((KeyEvent->uChar.UnicodeChar == UNICODE_NULL) && + ((KeyEvent->wVirtualScanCode != 0) || (WI_IsFlagSet(KeyEvent->dwControlKeyState, ENHANCED_KEY))))) + { + return FALSE; + } + + return TRUE; +} + +BOOL GetNextCharacter(_In_ INPUT_RECORD* InputRecord, _Out_ PWCHAR NextCharacter) +{ + BOOL IsNextCharacterValid = FALSE; + if (InputRecord->EventType == KEY_EVENT) + { + const auto KeyEvent = &InputRecord->Event.KeyEvent; + if ((IsActionableKey(KeyEvent) != FALSE) && ((KeyEvent->bKeyDown != FALSE) || (KeyEvent->wVirtualKeyCode == TTY_ALT_NUMPAD_VK_MENU))) + { + *NextCharacter = KeyEvent->uChar.UnicodeChar; + IsNextCharacterValid = TRUE; + } + } + + return IsNextCharacterValid; +} + +bool wsl::windows::common::relay::StandardInputRelay( + HANDLE ConsoleHandle, HANDLE OutputHandle, const std::function& UpdateTerminalSize, HANDLE ExitEvent, const std::vector& DetachSequence) +{ + try + { + if (GetFileType(ConsoleHandle) != FILE_TYPE_CHAR) + { + wsl::windows::common::relay::InterruptableRelay(ConsoleHandle, OutputHandle, ExitEvent); + return true; + } + + // + // N.B. ReadConsoleInputEx has no associated import library. + // + + static LxssDynamicFunction readConsoleInput(L"Kernel32.dll", "ReadConsoleInputExW"); + + INPUT_RECORD InputRecordBuffer[TTY_INPUT_EVENT_BUFFER_SIZE]; + INPUT_RECORD* InputRecordPeek = &(InputRecordBuffer[1]); + KEY_EVENT_RECORD* KeyEvent; + DWORD RecordsRead; + OVERLAPPED Overlapped = {0}; + const wil::unique_event OverlappedEvent(wil::EventOptions::ManualReset); + Overlapped.hEvent = OverlappedEvent.get(); + const HANDLE WaitHandles[] = {ExitEvent, ConsoleHandle}; + const std::vector ExitHandles = {ExitEvent}; + std::deque CurrentSequence; + + for (;;) + { + // Detach if the escape sequence was detected. + // N.B. This needs to done at the beginning of the loop so the escape sequence is also sent to docker. + if (!CurrentSequence.empty() && std::ranges::equal(CurrentSequence, DetachSequence)) + { + return false; + } + + // + // Because some input events generated by the console are encoded with + // more than one input event, we have to be smart about reading the + // events. + // + // First, we peek at the next input event. + // If it's an escape (wch == L'\x1b') event, then the characters that + // follow are part of an input sequence. We can't know for sure + // how long that sequence is, but we can assume it's all sent to + // the input queue at once, and it's less that 16 events. + // Furthermore, we can assume that if there's an Escape in those + // 16 events, that the escape marks the start of a new sequence. + // So, we'll peek at another 15 events looking for escapes. + // If we see an escape, then we'll read one less than that, + // such that the escape remains the next event in the input. + // From those read events, we'll aggregate chars into a single + // string to send to the subsystem. + // If it's not an escape, send the event through one at a time. + // + + // + // Read one input event. + // + + DWORD WaitStatus = (WAIT_OBJECT_0 + 1); + do + { + THROW_IF_WIN32_BOOL_FALSE(readConsoleInput(ConsoleHandle, InputRecordBuffer, 1, &RecordsRead, CONSOLE_READ_NOWAIT)); + + if (RecordsRead == 0) + { + WaitStatus = WaitForMultipleObjects(RTL_NUMBER_OF(WaitHandles), WaitHandles, false, INFINITE); + } + } while ((WaitStatus == (WAIT_OBJECT_0 + 1)) && (RecordsRead == 0)); + + // + // Stop processing if the exit event has been signaled. + // + + if (WaitStatus != (WAIT_OBJECT_0 + 1)) + { + WI_ASSERT(WaitStatus == WAIT_OBJECT_0); + + break; + } + + WI_ASSERT(RecordsRead == 1); + + // + // Don't read additional records if the first entry is a window size + // event, or a repeated character. Handle those events on their own. + // + + DWORD RecordsPeeked = 0; + if ((InputRecordBuffer[0].EventType != WINDOW_BUFFER_SIZE_EVENT) && + ((InputRecordBuffer[0].EventType != KEY_EVENT) || (InputRecordBuffer[0].Event.KeyEvent.wRepeatCount < 2))) + { + // + // Read additional input records into the buffer if available. + // + + THROW_IF_WIN32_BOOL_FALSE(PeekConsoleInputW(ConsoleHandle, InputRecordPeek, (RTL_NUMBER_OF(InputRecordBuffer) - 1), &RecordsPeeked)); + } + + // + // Iterate over peeked records [1, RecordsPeeked]. + // + + DWORD AdditionalRecordsToRead = 0; + WCHAR NextCharacter; + for (DWORD RecordIndex = 1; RecordIndex <= RecordsPeeked; RecordIndex++) + { + if (GetNextCharacter(&InputRecordBuffer[RecordIndex], &NextCharacter) != FALSE) + { + KeyEvent = &InputRecordBuffer[RecordIndex].Event.KeyEvent; + if (NextCharacter == TTY_ESCAPE_CHARACTER) + { + // + // CurrentRecord is an escape event. We will start here + // on the next input loop. + // + + break; + } + else if (KeyEvent->wRepeatCount > 1) + { + // + // Repeated keys are handled on their own. Start with this + // key on the next input loop. + // + + break; + } + else if (IS_HIGH_SURROGATE(NextCharacter) && (RecordIndex >= (RecordsPeeked - 1))) + { + // + // If there is not enough room for the second character of + // a surrogate pair, start with this character on the next + // input loop. + // + // N.B. The test is for at least two remaining records + // because typically a surrogate pair will be entered + // via copy/paste, which will appear as an input + // record with alt-down, alt-up and character. So to + // include the next character of the surrogate pair it + // is likely that the alt-up record will need to be + // read first. + // + + break; + } + } + else if (InputRecordBuffer[RecordIndex].EventType == WINDOW_BUFFER_SIZE_EVENT) + { + // + // A window size event is handled on its own. + // + + break; + } + + // + // Process the additional input record. + // + + AdditionalRecordsToRead += 1; + } + + if (AdditionalRecordsToRead > 0) + { + THROW_IF_WIN32_BOOL_FALSE( + readConsoleInput(ConsoleHandle, InputRecordPeek, AdditionalRecordsToRead, &RecordsRead, CONSOLE_READ_NOWAIT)); + + if (RecordsRead == 0) + { + // + // This would be an unexpected case. We've already peeked to see + // that there are AdditionalRecordsToRead # of records in the + // input that need reading, yet we didn't get them when we read. + // In this case, move along and finish this input event. + // + + break; + } + + // + // We already had one input record in the buffer before reading + // additional, So account for that one too + // + + RecordsRead += 1; + } + + // + // Process each input event. Keydowns will get aggregated into + // Utf8String before getting injected into the subsystem. + // + + WCHAR Utf16String[TTY_INPUT_EVENT_BUFFER_SIZE]; + ULONG Utf16StringSize = 0; + COORD WindowSize{}; + for (DWORD RecordIndex = 0; RecordIndex < RecordsRead; RecordIndex++) + { + INPUT_RECORD* CurrentInputRecord = &(InputRecordBuffer[RecordIndex]); + switch (CurrentInputRecord->EventType) + { + case KEY_EVENT: + + KeyEvent = &CurrentInputRecord->Event.KeyEvent; + + if (KeyEvent->bKeyDown && IsActionableKey(KeyEvent) && !DetachSequence.empty()) + { + if (CurrentSequence.size() >= DetachSequence.size()) + { + CurrentSequence.pop_front(); + } + + CurrentSequence.push_back(CurrentInputRecord->Event.KeyEvent.uChar.AsciiChar); + } + + // + // Filter out key up events unless they are from an key. + // Key up with an key could contain a Unicode character + // pasted from the clipboard and converted to an + sequence. + // + + if ((KeyEvent->bKeyDown == FALSE) && (KeyEvent->wVirtualKeyCode != TTY_ALT_NUMPAD_VK_MENU)) + { + break; + } + + // + // Filter out key presses that are not actionable, such as just + // pressing , , etc. These key presses return + // the character of null but will have a valid scan code off the + // keyboard. Certain other key sequences such as Ctrl+A, + // Ctrl+, and Ctrl+@ will also return the character null + // but have no scan code. + // + sequences will show an but will have + // a scancode and character specified, so they should be actionable. + // + + if (IsActionableKey(KeyEvent) == FALSE) + { + break; + } + + Utf16String[Utf16StringSize] = KeyEvent->uChar.UnicodeChar; + Utf16StringSize += 1; + break; + + case WINDOW_BUFFER_SIZE_EVENT: + + // + // Query the window size and send an update message via the + // control channel. + // + + UpdateTerminalSize(); + break; + } + } + + CHAR Utf8String[TTY_UTF8_TRANSLATION_BUFFER_SIZE]; + DWORD Utf8StringSize = 0; + if (Utf16StringSize > 0) + { + // + // Windows uses UTF-16LE encoding, Linux uses UTF-8 by default. + // Convert each UTF-16LE character into the proper UTF-8 byte + // sequence equivalent. + // + + THROW_LAST_ERROR_IF( + (Utf8StringSize = WideCharToMultiByte( + CP_UTF8, 0, Utf16String, Utf16StringSize, Utf8String, sizeof(Utf8String), nullptr, nullptr)) == 0); + } + + // + // Send the input bytes to the terminal. + // + + DWORD BytesWritten = 0; + const auto Utf8Span = gslhelpers::struct_as_bytes(Utf8String).first(Utf8StringSize); + if ((RecordsRead == 1) && (InputRecordBuffer[0].EventType == KEY_EVENT) && (InputRecordBuffer[0].Event.KeyEvent.wRepeatCount > 1)) + { + WI_ASSERT(Utf16StringSize == 1); + + // + // Handle repeated characters. They aren't part of an input + // sequence, so there's only one event that's generating characters. + // + + WORD RepeatIndex; + for (RepeatIndex = 0; RepeatIndex < InputRecordBuffer[0].Event.KeyEvent.wRepeatCount; RepeatIndex += 1) + { + BytesWritten = wsl::windows::common::relay::InterruptableWrite(OutputHandle, Utf8Span, ExitHandles, &Overlapped); + if (BytesWritten == 0) + { + break; + } + } + } + else if (Utf8StringSize > 0) + { + BytesWritten = wsl::windows::common::relay::InterruptableWrite(OutputHandle, Utf8Span, ExitHandles, &Overlapped); + if (BytesWritten == 0) + { + break; + } + } + } + } + CATCH_LOG(); + + return true; +} + void wsl::windows::common::relay::SocketRelay(_In_ SOCKET LeftSocket, _In_ SOCKET RightSocket, _In_ size_t BufferSize) { constexpr RelayFlags flags = RelayFlags::LeftIsSocket | RelayFlags::RightIsSocket; @@ -579,16 +963,33 @@ CATCH_LOG() void MultiHandleWait::AddHandle(std::unique_ptr&& handle, Flags flags) { + EnsureIocp(); + handle->Register(m_iocp.get(), handle.get()); m_handles.emplace_back(flags, std::move(handle)); } void MultiHandleWait::Cancel() { - m_cancel = true; + // Wake up GetQueuedCompletionStatus. The key=0 completion will set m_cancel in Run(). + if (m_iocp) + { + PostQueuedCompletionStatus(m_iocp.get(), 0, 0, nullptr); + } +} + +void MultiHandleWait::EnsureIocp() +{ + if (!m_iocp) + { + m_iocp.reset(CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 1)); + THROW_LAST_ERROR_IF(!m_iocp); + } } + bool MultiHandleWait::Run(std::optional Timeout) { m_cancel = false; // Run may be called multiple times. + EnsureIocp(); std::optional deadline; @@ -602,9 +1003,9 @@ bool MultiHandleWait::Run(std::optional Timeout) while (!m_handles.empty() && !m_cancel) { // Schedule IO on each handle until all are either pending, or completed. - for (size_t i = 0; i < m_handles.size(); i++) + for (size_t i = 0; i < m_handles.size() && !m_cancel; i++) { - while (m_handles[i].second->GetState() == IOHandleStatus::Standby) + while (m_handles[i].second->GetState() == IOHandleStatus::Standby && !m_cancel) { try { @@ -615,6 +1016,7 @@ bool MultiHandleWait::Run(std::optional Timeout) if (WI_IsFlagSet(m_handles[i].first, Flags::IgnoreErrors)) { m_handles[i].second.reset(); // Reset the handle so it can be deleted. + break; } else { @@ -625,6 +1027,7 @@ bool MultiHandleWait::Run(std::optional Timeout) } // Remove completed handles from m_handles. + bool hasHandleToWaitFor = false; for (auto it = m_handles.begin(); it != m_handles.end();) { if (!it->second) @@ -642,22 +1045,21 @@ bool MultiHandleWait::Run(std::optional Timeout) } else { + // If only NeedNotComplete handles are left, we want to exit Run. + if (WI_IsFlagClear(it->first, Flags::NeedNotComplete)) + { + hasHandleToWaitFor = true; + } ++it; } } - if (m_handles.empty() || m_cancel) + if (!hasHandleToWaitFor || m_cancel) { break; } - // Wait for the next operation to complete. - std::vector waitHandles; - for (const auto& e : m_handles) - { - waitHandles.emplace_back(e.second->GetHandle()); - } - + // Wait for the next operation to complete via IOCP. DWORD waitTimeout = INFINITE; if (deadline.has_value()) { @@ -667,35 +1069,56 @@ bool MultiHandleWait::Run(std::optional Timeout) waitTimeout = static_cast(std::max(0LL, miliseconds)); } - auto result = WaitForMultipleObjects(static_cast(waitHandles.size()), waitHandles.data(), false, waitTimeout); - if (result == WAIT_TIMEOUT) + DWORD bytesTransferred{}; + ULONG_PTR completionKey{}; + OVERLAPPED* completedOverlapped{}; + BOOL success = GetQueuedCompletionStatus(m_iocp.get(), &bytesTransferred, &completionKey, &completedOverlapped, waitTimeout); + + if (!success && completedOverlapped == nullptr) + { + // Timeout or error with no completion packet. + auto error = GetLastError(); + if (error == WAIT_TIMEOUT) + { + THROW_WIN32(ERROR_TIMEOUT); + } + THROW_WIN32(error); + } + + if (completionKey == 0) { - THROW_WIN32(ERROR_TIMEOUT); + // Cancel signal posted by Cancel() or an external PostQueuedCompletionStatus. + m_cancel = true; + continue; } - else if (result >= WAIT_OBJECT_0 && result < WAIT_OBJECT_0 + m_handles.size()) + + // Find the top-level handle in m_handles that matches the completion target. + auto* completionTarget = reinterpret_cast(completionKey); + + auto it = std::ranges::find_if( + m_handles, [completionTarget](const auto& entry) { return entry.second.get() == completionTarget; }); + + if (it == m_handles.end()) { - auto index = result - WAIT_OBJECT_0; + // Handle was already removed (e.g., by CancelOnCompleted). Discard the completion. + continue; + } - try + try + { + it->second->Collect(); + } + catch (...) + { + if (WI_IsFlagSet(it->first, Flags::IgnoreErrors)) { - m_handles[index].second->Collect(); + m_handles.erase(it); } - catch (...) + else { - if (WI_IsFlagSet(m_handles[index].first, Flags::IgnoreErrors)) - { - m_handles.erase(m_handles.begin() + index); - } - else - { - throw; - } + throw; } } - else - { - THROW_LAST_ERROR_MSG("Timeout: %lu, Count: %llu", waitTimeout, waitHandles.size()); - } } return !m_cancel; @@ -711,13 +1134,39 @@ EventHandle::EventHandle(HandleWrapper&& Handle, std::function&& OnSigna { } +EventHandle::~EventHandle() +{ +} + +void EventHandle::Register(HANDLE iocp, OverlappedIOHandle* completionTarget) +{ + CompletionTarget = completionTarget; + Iocp = iocp; +} + void EventHandle::Schedule() { + // If registered with an IOCP, use RegisterWaitForSingleObject to bridge the event. + if (Iocp != nullptr && !WaitHandle) + { + THROW_IF_WIN32_BOOL_FALSE( + RegisterWaitForSingleObject(&WaitHandle, Handle.Get(), &EventHandle::WaitCallback, this, INFINITE, WT_EXECUTEONLYONCE)); + } + State = IOHandleStatus::Pending; } +VOID CALLBACK EventHandle::WaitCallback(PVOID context, BOOLEAN /*timedOut*/) +{ + auto* self = static_cast(context); + PostQueuedCompletionStatus(self->Iocp, 0, reinterpret_cast(self->CompletionTarget), nullptr); +} + void EventHandle::Collect() { + // Clean up the wait registration so it can be re-registered if needed. + WaitHandle.reset(); + State = IOHandleStatus::Completed; OnSignalled(); } @@ -727,63 +1176,751 @@ HANDLE EventHandle::GetHandle() const return Handle.Get(); } -SingleAcceptHandle::SingleAcceptHandle(HandleWrapper&& ListenSocket, HandleWrapper&& AcceptedSocket, std::function&& OnAccepted) : - ListenSocket(std::move(ListenSocket)), AcceptedSocket(std::move(AcceptedSocket)), OnAccepted(std::move(OnAccepted)) +ReadHandle::ReadHandle(HandleWrapper&& MovedHandle, std::function& Buffer)>&& OnRead) : + Handle(std::move(MovedHandle)), OnRead(OnRead), Offset(InitializeFileOffset(Handle.Get())) { Overlapped.hEvent = Event.get(); } -SingleAcceptHandle::~SingleAcceptHandle() +ReadHandle::~ReadHandle() { + WaitBridge.reset(); + if (State == IOHandleStatus::Pending) { - LOG_IF_WIN32_BOOL_FALSE(CancelIoEx(ListenSocket.Get(), &Overlapped)); + if (RegisteredWithIocp) + { + // In IOCP mode, Overlapped.hEvent is nullptr and cancellation completions go + // to the IOCP queue. Set a temporary event so GetOverlappedResult can wait + // for the cancel to complete, ensuring the OVERLAPPED isn't freed while the + // kernel still references it. + wil::unique_event cancelEvent(wil::EventOptions::ManualReset); + Overlapped.hEvent = cancelEvent.get(); + + if (CancelIoEx(Handle.Get(), &Overlapped)) + { + DWORD bytesRead{}; + GetOverlappedResult(Handle.Get(), &Overlapped, &bytesRead, true); + } - DWORD bytesProcessed{}; - DWORD flagsReturned{}; - if (!WSAGetOverlappedResult((SOCKET)ListenSocket.Get(), &Overlapped, &bytesProcessed, TRUE, &flagsReturned)) + Overlapped.hEvent = nullptr; + } + else if (CancelIoEx(Handle.Get(), &Overlapped)) { - auto error = GetLastError(); - LOG_LAST_ERROR_IF(error != ERROR_CONNECTION_ABORTED && error != ERROR_OPERATION_ABORTED); + DWORD bytesRead{}; + if (!GetOverlappedResult(Handle.Get(), &Overlapped, &bytesRead, true)) + { + auto error = GetLastError(); + LOG_LAST_ERROR_IF(error != ERROR_CONNECTION_ABORTED && error != ERROR_OPERATION_ABORTED); + } + } + else + { + LOG_LAST_ERROR_IF(GetLastError() != ERROR_NOT_FOUND); } } } -void SingleAcceptHandle::Schedule() +void ReadHandle::Register(HANDLE iocp, OverlappedIOHandle* completionTarget) +{ + CompletionTarget = completionTarget; + Iocp = iocp; + if (!RegisteredWithIocp) + { + // Try to associate the handle with the IOCP. Not all handle types support this + // (e.g., some socket types, console handles). Fall back to event-based mode on failure. + // + // N.B. FILE_SKIP_COMPLETION_PORT_ON_SUCCESS is required before associating with the IOCP. + // Without it, synchronous completions would both be processed inline by Schedule() AND + // queue an IOCP packet, causing double-processing. If it's not supported for this handle + // type, fall back to event-based mode entirely. + if (SetFileCompletionNotificationModes(Handle.Get(), FILE_SKIP_COMPLETION_PORT_ON_SUCCESS)) + { + auto result = CreateIoCompletionPort(Handle.Get(), iocp, reinterpret_cast(completionTarget), 0); + if (result != nullptr) + { + // Clear the event from OVERLAPPED — completions now go to the IOCP. + Overlapped.hEvent = nullptr; + RegisteredWithIocp = true; + } + } + // else: fall back to event-based mode (Overlapped.hEvent remains set) + } +} + +void ReadHandle::Schedule() { WI_ASSERT(State == IOHandleStatus::Standby); - // Schedule the accept. - DWORD bytesReturned{}; - if (AcceptEx((SOCKET)ListenSocket.Get(), (SOCKET)AcceptedSocket.Get(), &AcceptBuffer, 0, sizeof(SOCKADDR_STORAGE), sizeof(SOCKADDR_STORAGE), &bytesReturned, &Overlapped)) + Event.ResetEvent(); + + // Schedule the read. + DWORD bytesRead{}; + Overlapped.Offset = Offset.LowPart; + Overlapped.OffsetHigh = Offset.HighPart; + if (ReadFile(Handle.Get(), Buffer.data(), static_cast(Buffer.size()), &bytesRead, &Overlapped)) { - // Accept completed immediately. - State = IOHandleStatus::Completed; - OnAccepted(); + Offset.QuadPart += bytesRead; + + // Signal the read. + OnRead(gsl::make_span(Buffer.data(), static_cast(bytesRead))); + + // ReadFile completed immediately, process the result right away. + if (bytesRead == 0) + { + State = IOHandleStatus::Completed; + return; // Handle is completely read, don't try again. + } + + // Read was done synchronously, remain in 'standby' state. } else { - auto error = WSAGetLastError(); - THROW_HR_IF_MSG(HRESULT_FROM_WIN32(error), error != ERROR_IO_PENDING, "Handle: 0x%p", (void*)ListenSocket.Get()); + auto error = GetLastError(); + if (error == ERROR_HANDLE_EOF || error == ERROR_BROKEN_PIPE) + { + // Signal an empty read for EOF. + OnRead({}); + + State = IOHandleStatus::Completed; + return; + } + + THROW_LAST_ERROR_IF_MSG(error != ERROR_IO_PENDING, "Handle: 0x%p", (void*)Handle.Get()); + // The read is pending, update to 'Pending' State = IOHandleStatus::Pending; + + // If not registered with IOCP, bridge the event to the IOCP so + // GetQueuedCompletionStatus will wake up when this I/O completes. + if (!RegisteredWithIocp && Iocp != nullptr && !WaitBridge) + { + THROW_IF_WIN32_BOOL_FALSE(RegisterWaitForSingleObject( + &WaitBridge, Event.get(), &ReadHandle::WaitBridgeCallback, this, INFINITE, WT_EXECUTEONLYONCE)); + } } } -void SingleAcceptHandle::Collect() +VOID CALLBACK ReadHandle::WaitBridgeCallback(PVOID context, BOOLEAN /*timedOut*/) +{ + auto* self = static_cast(context); + PostQueuedCompletionStatus(self->Iocp, 0, reinterpret_cast(self->CompletionTarget), nullptr); +} + +void ReadHandle::Collect() { WI_ASSERT(State == IOHandleStatus::Pending); - DWORD bytesReceived{}; - DWORD flagsReturned{}; + // Tear down the event-to-IOCP bridge if it was set up. + WaitBridge.reset(); - THROW_IF_WIN32_BOOL_FALSE(WSAGetOverlappedResult((SOCKET)ListenSocket.Get(), &Overlapped, &bytesReceived, false, &flagsReturned)); + // Transition back to standby + State = IOHandleStatus::Standby; - State = IOHandleStatus::Completed; - OnAccepted(); + // Complete the read. + DWORD bytesRead{}; + if (!GetOverlappedResult(Handle.Get(), &Overlapped, &bytesRead, false)) + { + auto error = GetLastError(); + THROW_WIN32_IF(error, error != ERROR_HANDLE_EOF && error != ERROR_BROKEN_PIPE); + + // We received ERROR_HANDLE_EOF or ERROR_BROKEN_PIPE. Validate that this was indeed a zero byte read. + WI_ASSERT(bytesRead == 0); + } + + Offset.QuadPart += bytesRead; + + // Signal the read. + OnRead(gsl::make_span(Buffer.data(), static_cast(bytesRead))); + + // Transition to Complete if this was a zero byte read. + if (bytesRead == 0) + { + State = IOHandleStatus::Completed; + } } -HANDLE SingleAcceptHandle::GetHandle() const +HANDLE ReadHandle::GetHandle() const { return Event.get(); +} + +SingleAcceptHandle::SingleAcceptHandle(HandleWrapper&& ListenSocket, HandleWrapper&& AcceptedSocket, std::function&& OnAccepted) : + ListenSocket(std::move(ListenSocket)), AcceptedSocket(std::move(AcceptedSocket)), OnAccepted(std::move(OnAccepted)) +{ + Overlapped.hEvent = Event.get(); +} + +SingleAcceptHandle::~SingleAcceptHandle() +{ + WaitBridge.reset(); + + if (State == IOHandleStatus::Pending) + { + if (RegisteredWithIocp) + { + wil::unique_event cancelEvent(wil::EventOptions::ManualReset); + Overlapped.hEvent = cancelEvent.get(); + + if (CancelIoEx(ListenSocket.Get(), &Overlapped)) + { + DWORD bytesProcessed{}; + DWORD flagsReturned{}; + WSAGetOverlappedResult((SOCKET)ListenSocket.Get(), &Overlapped, &bytesProcessed, TRUE, &flagsReturned); + } + + Overlapped.hEvent = nullptr; + } + else + { + LOG_IF_WIN32_BOOL_FALSE(CancelIoEx(ListenSocket.Get(), &Overlapped)); + + DWORD bytesProcessed{}; + DWORD flagsReturned{}; + if (!WSAGetOverlappedResult((SOCKET)ListenSocket.Get(), &Overlapped, &bytesProcessed, TRUE, &flagsReturned)) + { + auto error = GetLastError(); + LOG_LAST_ERROR_IF(error != ERROR_CONNECTION_ABORTED && error != ERROR_OPERATION_ABORTED); + } + } + } +} + +void SingleAcceptHandle::Register(HANDLE iocp, OverlappedIOHandle* completionTarget) +{ + CompletionTarget = completionTarget; + Iocp = iocp; + if (!RegisteredWithIocp) + { + if (SetFileCompletionNotificationModes(ListenSocket.Get(), FILE_SKIP_COMPLETION_PORT_ON_SUCCESS)) + { + auto result = CreateIoCompletionPort(ListenSocket.Get(), iocp, reinterpret_cast(completionTarget), 0); + if (result != nullptr) + { + Overlapped.hEvent = nullptr; + RegisteredWithIocp = true; + } + } + } +} + +void SingleAcceptHandle::Schedule() +{ + WI_ASSERT(State == IOHandleStatus::Standby); + + // Schedule the accept. + DWORD bytesReturned{}; + if (AcceptEx((SOCKET)ListenSocket.Get(), (SOCKET)AcceptedSocket.Get(), &AcceptBuffer, 0, sizeof(SOCKADDR_STORAGE), sizeof(SOCKADDR_STORAGE), &bytesReturned, &Overlapped)) + { + // Accept completed immediately. + State = IOHandleStatus::Completed; + OnAccepted(); + } + else + { + auto error = WSAGetLastError(); + THROW_HR_IF_MSG(HRESULT_FROM_WIN32(error), error != ERROR_IO_PENDING, "Handle: 0x%p", (void*)ListenSocket.Get()); + + State = IOHandleStatus::Pending; + + if (!RegisteredWithIocp && Iocp != nullptr && !WaitBridge) + { + THROW_IF_WIN32_BOOL_FALSE(RegisterWaitForSingleObject( + &WaitBridge, Event.get(), &SingleAcceptHandle::WaitBridgeCallback, this, INFINITE, WT_EXECUTEONLYONCE)); + } + } +} + +VOID CALLBACK SingleAcceptHandle::WaitBridgeCallback(PVOID context, BOOLEAN /*timedOut*/) +{ + auto* self = static_cast(context); + PostQueuedCompletionStatus(self->Iocp, 0, reinterpret_cast(self->CompletionTarget), nullptr); +} + +void SingleAcceptHandle::Collect() +{ + WI_ASSERT(State == IOHandleStatus::Pending); + + WaitBridge.reset(); + + DWORD bytesReceived{}; + DWORD flagsReturned{}; + + THROW_IF_WIN32_BOOL_FALSE(WSAGetOverlappedResult((SOCKET)ListenSocket.Get(), &Overlapped, &bytesReceived, false, &flagsReturned)); + + State = IOHandleStatus::Completed; + OnAccepted(); +} + +HANDLE SingleAcceptHandle::GetHandle() const +{ + return Event.get(); +} + +LineBasedReadHandle::LineBasedReadHandle(HandleWrapper&& Handle, std::function& Line)>&& OnLine, bool Crlf) : + ReadHandle(std::move(Handle), [this](const gsl::span& Buffer) { OnRead(Buffer); }), OnLine(OnLine), Crlf(Crlf) +{ +} + +LineBasedReadHandle::~LineBasedReadHandle() +{ + // N.B. PendingBuffer can contain remaining data is an exception was thrown during parsing. +} + +void LineBasedReadHandle::OnRead(const gsl::span& Buffer) +{ + // If we reach of the end, signal a line with the remaining buffer. + if (Buffer.empty() && !PendingBuffer.empty()) + { + OnLine(PendingBuffer); + PendingBuffer.clear(); + return; + } + + auto begin = Buffer.begin(); + auto end = std::ranges::find(Buffer, Crlf ? '\r' : '\n'); + while (end != Buffer.end()) + { + if (Crlf) + { + end++; // Move to the following '\n' + + if (end == Buffer.end() || *end != '\n') // Incomplete CRLF sequence. Append to buffer and continue. + { + PendingBuffer.insert(PendingBuffer.end(), begin, end); + begin = end; + end = std::ranges::find(end, Buffer.end(), '\r'); + continue; + } + } + + // Discard the '\r' in CRLF mode. + PendingBuffer.insert(PendingBuffer.end(), begin, Crlf ? end - 1 : end); + + if (!PendingBuffer.empty()) + { + OnLine(PendingBuffer); + PendingBuffer.clear(); + } + + begin = end + 1; + end = std::ranges::find(begin, Buffer.end(), Crlf ? '\r' : '\n'); + } + + PendingBuffer.insert(PendingBuffer.end(), begin, end); +} + +HTTPChunkBasedReadHandle::HTTPChunkBasedReadHandle(HandleWrapper&& MovedHandle, std::function& Line)>&& OnChunk) : + ReadHandle(std::move(MovedHandle), [this](const gsl::span& Buffer) { OnRead(Buffer); }), OnChunk(std::move(OnChunk)) +{ +} + +HTTPChunkBasedReadHandle::~HTTPChunkBasedReadHandle() +{ + // N.B. PendingBuffer can contain remaining data is an exception was thrown during parsing. + LOG_HR_IF(E_UNEXPECTED, !PendingBuffer.empty() || PendingChunkSize != 0 || ExpectHeader); +} + +void HTTPChunkBasedReadHandle::OnRead(const gsl::span& Input) +{ + // See: https://httpwg.org/specs/rfc9112.html#field.transfer-encoding + + if (Input.empty()) + { + // N.B. The body can be terminated by a zero-length chunk. + THROW_HR_IF(E_INVALIDARG, PendingChunkSize != 0 || ExpectHeader); + } + + auto buffer = Input; + + auto advance = [&](size_t count) { + WI_ASSERT(buffer.size() >= count); + buffer = buffer.subspan(count); + }; + + while (!buffer.empty()) + { + if (PendingChunkSize == 0) + { + // Consume CRLF's between chunks. + if (PendingBuffer.empty() && (buffer.front() == '\r' || buffer.front() == '\n')) + { + advance(1); + continue; + } + + ExpectHeader = true; + + auto end = std::ranges::find(buffer, '\n'); + PendingBuffer.insert(PendingBuffer.end(), buffer.begin(), end); + if (end == buffer.end()) + { + // Incomplete size header, buffer until next read. + break; + } + // Advance beyond the LF + advance(end - buffer.begin() + 1); + + THROW_HR_IF_MSG( + E_INVALIDARG, + PendingBuffer.size() < 2 || PendingBuffer.back() != '\r', + "Malformed chunk header: %hs", + PendingBuffer.c_str()); + PendingBuffer.erase(PendingBuffer.end() - 1, PendingBuffer.end()); // Remove CR. + +#ifdef WSLC_HTTP_DEBUG + + WSL_LOG("HTTPChunkHeader", TraceLoggingValue(PendingBuffer.c_str(), "Size")); + +#endif + + try + { + size_t parsed{}; + PendingChunkSize = std::stoul(PendingBuffer.c_str(), &parsed, 16); + THROW_HR_IF(E_INVALIDARG, parsed != PendingBuffer.size()); + } + catch (...) + { + THROW_HR_MSG(E_INVALIDARG, "Failed to parse chunk size: %hs", PendingBuffer.c_str()); + } + + ExpectHeader = false; + PendingBuffer.clear(); + } + else + { + // Consume the chunk. + auto consumedBytes = std::min(PendingChunkSize, buffer.size()); + PendingBuffer.append(buffer.data(), consumedBytes); + advance(consumedBytes); + + WI_ASSERT(PendingChunkSize >= consumedBytes); + PendingChunkSize -= consumedBytes; + + if (PendingChunkSize == 0) + { + +#ifdef WSLC_HTTP_DEBUG + + WSL_LOG("HTTPChunk", TraceLoggingValue(PendingBuffer.c_str(), "Content")); + +#endif + OnChunk(PendingBuffer); + PendingBuffer.clear(); + } + } + } +} + +WriteHandle::WriteHandle(HandleWrapper&& MovedHandle, const std::vector& Buffer) : + Handle(std::move(MovedHandle)), Buffer(Buffer), Offset(InitializeFileOffset(Handle.Get())) +{ + Overlapped.hEvent = Event.get(); +} + +WriteHandle::~WriteHandle() +{ + WaitBridge.reset(); + + if (State == IOHandleStatus::Pending) + { + if (RegisteredWithIocp) + { + wil::unique_event cancelEvent(wil::EventOptions::ManualReset); + Overlapped.hEvent = cancelEvent.get(); + + if (CancelIoEx(Handle.Get(), &Overlapped)) + { + DWORD bytesRead{}; + GetOverlappedResult(Handle.Get(), &Overlapped, &bytesRead, true); + } + + Overlapped.hEvent = nullptr; + } + else if (CancelIoEx(Handle.Get(), &Overlapped)) + { + DWORD bytesRead{}; + if (!GetOverlappedResult(Handle.Get(), &Overlapped, &bytesRead, true)) + { + auto error = GetLastError(); + LOG_LAST_ERROR_IF(error != ERROR_CONNECTION_ABORTED && error != ERROR_OPERATION_ABORTED); + } + } + else + { + LOG_LAST_ERROR_IF(GetLastError() != ERROR_NOT_FOUND); + } + } +} + +void WriteHandle::Register(HANDLE iocp, OverlappedIOHandle* completionTarget) +{ + CompletionTarget = completionTarget; + Iocp = iocp; + if (!RegisteredWithIocp) + { + if (SetFileCompletionNotificationModes(Handle.Get(), FILE_SKIP_COMPLETION_PORT_ON_SUCCESS)) + { + auto result = CreateIoCompletionPort(Handle.Get(), iocp, reinterpret_cast(completionTarget), 0); + if (result != nullptr) + { + Overlapped.hEvent = nullptr; + RegisteredWithIocp = true; + } + } + } +} + +void WriteHandle::Schedule() +{ + WI_ASSERT(State == IOHandleStatus::Standby); + + Event.ResetEvent(); + + Overlapped.Offset = Offset.LowPart; + Overlapped.OffsetHigh = Offset.HighPart; + + // Schedule the write. + DWORD bytesWritten{}; + if (WriteFile(Handle.Get(), Buffer.data(), static_cast(Buffer.size()), &bytesWritten, &Overlapped)) + { + Offset.QuadPart += bytesWritten; + + Buffer.erase(Buffer.begin(), Buffer.begin() + bytesWritten); + if (Buffer.empty()) + { + State = IOHandleStatus::Completed; + } + } + else + { + auto error = GetLastError(); + THROW_LAST_ERROR_IF_MSG(error != ERROR_IO_PENDING, "Handle: 0x%p", (void*)Handle.Get()); + + // The write is pending, update to 'Pending' + State = IOHandleStatus::Pending; + + if (!RegisteredWithIocp && Iocp != nullptr && !WaitBridge) + { + THROW_IF_WIN32_BOOL_FALSE(RegisterWaitForSingleObject( + &WaitBridge, Event.get(), &WriteHandle::WaitBridgeCallback, this, INFINITE, WT_EXECUTEONLYONCE)); + } + } +} + +VOID CALLBACK WriteHandle::WaitBridgeCallback(PVOID context, BOOLEAN /*timedOut*/) +{ + auto* self = static_cast(context); + PostQueuedCompletionStatus(self->Iocp, 0, reinterpret_cast(self->CompletionTarget), nullptr); +} + +void WriteHandle::Collect() +{ + WI_ASSERT(State == IOHandleStatus::Pending); + + WaitBridge.reset(); + + // Transition back to standby + State = IOHandleStatus::Standby; + + // Complete the write. + DWORD bytesWritten{}; + THROW_IF_WIN32_BOOL_FALSE(GetOverlappedResult(Handle.Get(), &Overlapped, &bytesWritten, false)); + Offset.QuadPart += bytesWritten; + + Buffer.erase(Buffer.begin(), Buffer.begin() + bytesWritten); + if (Buffer.empty()) + { + State = IOHandleStatus::Completed; + } +} + +void WriteHandle::Push(const gsl::span& Content) +{ + // Don't write if a WriteFile() is pending, since that could cause the buffer to reallocate. + WI_ASSERT(State == IOHandleStatus::Standby || State == IOHandleStatus::Completed); + WI_ASSERT(!Content.empty()); + + Buffer.insert(Buffer.end(), Content.begin(), Content.end()); + + State = IOHandleStatus::Standby; +} + +HANDLE WriteHandle::GetHandle() const +{ + return Event.get(); +} + +DockerIORelayHandle::DockerIORelayHandle(HandleWrapper&& ReadHandle, HandleWrapper&& Stdout, HandleWrapper&& Stderr, Format ReadFormat) : + WriteStdout(std::move(Stdout)), WriteStderr(std::move(Stderr)) +{ + if (ReadFormat == Format::HttpChunked) + { + Read = std::make_unique( + std::move(ReadHandle), [this](const gsl::span& Line) { this->OnRead(Line); }); + } + else + { + Read = std::make_unique( + std::move(ReadHandle), [this](const gsl::span& Buffer) { this->OnRead(Buffer); }); + } +} + +void DockerIORelayHandle::Register(HANDLE iocp, OverlappedIOHandle* completionTarget) +{ + CompletionTarget = completionTarget; + Read->Register(iocp, completionTarget); + WriteStdout.Register(iocp, completionTarget); + WriteStderr.Register(iocp, completionTarget); +} + +void DockerIORelayHandle::Schedule() +{ + WI_ASSERT(State == IOHandleStatus::Standby); + WI_ASSERT(Read->GetState() != IOHandleStatus::Pending); + + // If we have an active handle and a buffer, try to flush that first. + if (ActiveHandle != nullptr && !PendingBuffer.empty()) + { + // Push the data to the selected handle. + DWORD bytesToWrite = std::min(static_cast(RemainingBytes), static_cast(PendingBuffer.size())); + + ActiveHandle->Push(gsl::make_span(PendingBuffer.data(), bytesToWrite)); + + // Consume the written bytes. + RemainingBytes -= bytesToWrite; + PendingBuffer.erase(PendingBuffer.begin(), PendingBuffer.begin() + bytesToWrite); + + // Schedule the write. + ActiveHandle->Schedule(); + + // If the write is pending, update to 'Pending' + if (ActiveHandle->GetState() == IOHandleStatus::Pending) + { + State = IOHandleStatus::Pending; + } + else if (ActiveHandle->GetState() == IOHandleStatus::Completed) + { + if (RemainingBytes == 0) + { + // Switch back to reading if we've written all bytes for this chunk. + ActiveHandle = nullptr; + + ProcessNextHeader(); + } + } + } + else + { + if (Read->GetState() == IOHandleStatus::Completed) + { + LOG_HR_IF(E_UNEXPECTED, ActiveHandle != nullptr); + + // No more data to read, we're done. + State = IOHandleStatus::Completed; + return; + } + + // Schedule a read from the input. + Read->Schedule(); + if (Read->GetState() == IOHandleStatus::Pending) + { + State = IOHandleStatus::Pending; + } + } +} + +void DockerIORelayHandle::Collect() +{ + WI_ASSERT(State == IOHandleStatus::Pending); + + if (ActiveHandle != nullptr && ActiveHandle->GetState() == IOHandleStatus::Pending) + { + // Complete the write. + ActiveHandle->Collect(); + + // If the write is completed, switch back to reading. + if (RemainingBytes == 0) + { + if (ActiveHandle->GetState() == IOHandleStatus::Completed) + { + ActiveHandle = nullptr; + } + } + + // Transition back to standby if there's still data to read. + // Otherwise switch to Completed since everything is done. + if (Read->GetState() == IOHandleStatus::Completed) + { + LOG_HR_IF(E_UNEXPECTED, RemainingBytes != 0); + + State = IOHandleStatus::Completed; + } + else + { + State = IOHandleStatus::Standby; + } + } + else + { + WI_ASSERT(Read->GetState() == IOHandleStatus::Pending); + + // Complete the read. + Read->Collect(); + + // Transition back to standby. + State = IOHandleStatus::Standby; + } +} + +HANDLE DockerIORelayHandle::GetHandle() const +{ + if (ActiveHandle != nullptr && ActiveHandle->GetState() == IOHandleStatus::Pending) + { + return ActiveHandle->GetHandle(); + } + else + { + return Read->GetHandle(); + } +} + +void DockerIORelayHandle::ProcessNextHeader() +{ + if (PendingBuffer.size() < sizeof(MultiplexedHeader)) + { + // Not enough data for a header yet. + return; + } + + const auto* header = reinterpret_cast(PendingBuffer.data()); + RemainingBytes = ntohl(header->Length); + + if (header->Fd == 1) + { + ActiveHandle = &WriteStdout; + } + else if (header->Fd == 2) + { + ActiveHandle = &WriteStderr; + } + else + { + THROW_HR_MSG(E_INVALIDARG, "Invalid Docker IO multiplexed header fd: %u", header->Fd); + } + + // Consume the header. + PendingBuffer.erase(PendingBuffer.begin(), PendingBuffer.begin() + sizeof(MultiplexedHeader)); +} + +void DockerIORelayHandle::OnRead(const gsl::span& Buffer) +{ + PendingBuffer.insert(PendingBuffer.end(), Buffer.begin(), Buffer.end()); + + if (ActiveHandle == nullptr) + { + // If no handle is active, expect a header. + ProcessNextHeader(); + } } \ No newline at end of file diff --git a/src/windows/common/wslutil.cpp b/src/windows/common/wslutil.cpp index 421caf89e..24d21733e 100644 --- a/src/windows/common/wslutil.cpp +++ b/src/windows/common/wslutil.cpp @@ -216,141 +216,6 @@ bool IsWinInetError(HRESULT error) return code >= INTERNET_ERROR_BASE && code <= INTERNET_ERROR_LAST; } -bool PromptForKeyPress() -{ - THROW_IF_WIN32_BOOL_FALSE(FlushConsoleInputBuffer(GetStdHandle(STD_INPUT_HANDLE))); - - // Note: Ctrl-c causes _getch to return 0x3. - return _getch() != 0x3; -} - -bool PromptForKeyPressWithTimeout() -{ - std::promise pressedKey; - auto thread = std::thread([&pressedKey]() { pressedKey.set_value(PromptForKeyPress()); }); - - auto cancelRead = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&thread]() { - if (thread.joinable()) - { - LOG_IF_WIN32_BOOL_FALSE(CancelSynchronousIo(thread.native_handle())); - thread.join(); - } - }); - - auto future = pressedKey.get_future(); - const auto waitResult = future.wait_for(std::chrono::minutes(1)); - - return waitResult == std::future_status::ready && future.get(); -} - -int UpdatePackageImpl(bool preRelease, bool repair) -{ - if (!repair) - { - PrintMessage(Localization::MessageCheckingForUpdates()); - } - - auto [version, release] = GetLatestGitHubRelease(preRelease); - - if (!repair && ParseWslPackageVersion(version) <= wsl::shared::PackageVersion) - { - PrintMessage(Localization::MessageUpdateNotNeeded()); - return 0; - } - - PrintMessage(Localization::MessageUpdatingToVersion(version.c_str())); - - const bool msiInstall = wsl::shared::string::EndsWith(release.name, L".msi"); - const auto downloadPath = DownloadFile(release.url, release.name); - if (msiInstall) - { - auto logFile = std::filesystem::temp_directory_path() / L"wsl-install-logs.txt"; - auto clearLogs = - wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&logFile]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFile(logFile.c_str())); }); - - const auto exitCode = UpgradeViaMsi(downloadPath.c_str(), L"", logFile.c_str(), &MsiMessageCallback); - - if (exitCode != 0) - { - clearLogs.release(); - THROW_HR_WITH_USER_ERROR( - HRESULT_FROM_WIN32(exitCode), - wsl::shared::Localization::MessageUpdateFailed(exitCode) + L"\r\n" + - wsl::shared::Localization::MessageSeeLogFile(logFile.c_str())); - } - } - else - { - // Set FILE_FLAG_DELETE_ON_CLOSE on the file to make sure it's deleted when the installation completes. - const wil::unique_hfile package{CreateFileW( - downloadPath.c_str(), DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_DELETE_ON_CLOSE, nullptr)}; - - THROW_LAST_ERROR_IF(!package); - - const winrt::Windows::Management::Deployment::PackageManager packageManager; - const auto result = packageManager.AddPackageAsync( - Uri{downloadPath.c_str()}, nullptr, DeploymentOptions::ForceApplicationShutdown | DeploymentOptions::ForceTargetApplicationShutdown); - - THROW_IF_FAILED(result.get().ExtendedErrorCode()); - - // Note: If the installation is successful, this process is expected to receive and Ctrl-C and exit - } - - return 0; -} - -void WaitForMsiInstall() -{ - wil::com_ptr_t installer; - - auto retry_pred = []() { - const auto errorCode = wil::ResultFromCaughtException(); - return errorCode == REGDB_E_CLASSNOTREG; - }; - - wsl::shared::retry::RetryWithTimeout( - [&installer]() { installer = wil::CoCreateInstance(__uuidof(WslInstaller), CLSCTX_LOCAL_SERVER); }, - std::chrono::seconds(1), - std::chrono::minutes(1), - retry_pred); - - fputws(wsl::shared::Localization::MessageFinishMsiInstallation().c_str(), stderr); - - auto finishLine = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { fputws(L"\n", stderr); }); - - UINT exitCode = -1; - wil::unique_cotaskmem_string message = nullptr; - THROW_IF_FAILED(installer->Install(&exitCode, &message)); - - if (*message.get() != UNICODE_NULL) - { - finishLine.release(); - wprintf(L"\n%ls\n", message.get()); - } - - if (exitCode != 0) - { - THROW_HR_WITH_USER_ERROR(HRESULT_FROM_WIN32(exitCode), wsl::shared::Localization::MessageUpdateFailed(exitCode)); - } -} - -wil::unique_handle CreateJob() -{ - // Create a job object that will terminate all processes in the job on - // close but will not terminate the children of the processes in the job. - // This is used to ensure that when forwarding from an inbox binary (I) - // to a lifted binary (L), if I is terminated L is terminated as well but - // any children of L (e.g. wslhost.exe) continue to run. - wil::unique_handle job{CreateJobObject(nullptr, nullptr)}; - THROW_LAST_ERROR_IF_NULL(job.get()); - - JOBOBJECT_EXTENDED_LIMIT_INFORMATION info{}; - info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK; - THROW_IF_WIN32_BOOL_FALSE(SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation, &info, sizeof(info))); - - return job; -} - constexpr uint16_t EndianSwap(uint16_t value) { return (value & 0xFF00) >> 8 | (value & 0x00FF) << 8; @@ -376,93 +241,6 @@ constexpr GUID EndianSwap(GUID value) } // namespace -int wsl::windows::common::wslutil::CallMsiPackage() -{ - wsl::windows::common::ExecutionContext context(wsl::windows::common::CallMsi); - - auto msiPath = GetMsiPackagePath(); - if (!msiPath.has_value()) - { - wsl::windows::common::ExecutionContext context(wsl::windows::common::Install); - - try - { - WaitForMsiInstall(); - msiPath = GetMsiPackagePath(); - } - catch (...) - { - LOG_CAUGHT_EXCEPTION(); - - // GetMsiPackagePath() will generate a user error if the registry access fails. - // Save the error from GetMsiPackagePath() to return a proper 'install failed' message. - auto savedError = context.ReportedError(); - - // There is a race where the service might stop before returning the install result. - // if this happens, only fail if the MSI still isn't installed. - msiPath = GetMsiPackagePath(); - if (!msiPath.has_value()) - { - // Offer to directly install the MSI package if the MsixInstaller logic fails - // This can trigger a UAC so only do it - if (IsInteractiveConsole()) - { - auto errorCode = savedError.has_value() ? ErrorToString(savedError.value()).Code - : ErrorCodeToString(wil::ResultFromCaughtException()); - - EMIT_USER_WARNING(wsl::shared::Localization::MessageInstallationCorrupted(errorCode)); - - if (PromptForKeyPressWithTimeout()) - { - return UpdatePackage(false, true); - } - } - - if (savedError.has_value()) - { - THROW_HR_WITH_USER_ERROR(savedError->Code, savedError->Message.value_or(L"")); - } - - throw; - } - } - - THROW_HR_IF(E_UNEXPECTED, !msiPath.has_value()); - } - - auto target = msiPath.value() + L"\\" WSL_BINARY_NAME; - - SubProcess process(target.c_str(), GetCommandLine()); - process.SetDesktopAppPolicy(PROCESS_CREATION_DESKTOP_APP_BREAKAWAY_ENABLE_PROCESS_TREE); - auto runningProcess = process.Start(); - - // N.B. The job cannot be assigned at process creation time as the packaged process - // creation path will assign the new process to a per package job object. - // In the case of multiple processes running in a single package, assigning - // the new process to the per package job object will fail for the second request - // since both jobs already have processes which prevents a job hierarchy from - // being established. - auto job = CreateJob(); - - // Assign the process to the job, ignoring failures when the process has - // terminated. - // - // N.B. Assigning the job after process creation without CREATE_SUSPENDED is - // safe to do here since only the new child process will be in the job - // object. None of the grandchildren processes are included since the - // job is created with JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK. - if (!AssignProcessToJobObject(job.get(), runningProcess.get())) - { - auto lastError = GetLastError(); - if (lastError != ERROR_ACCESS_DENIED) - { - THROW_WIN32(lastError); - } - } - - return static_cast(SubProcess::GetExitCode(runningProcess.get())); -} - template wil::com_ptr wsl::windows::common::wslutil::CoGetCallContext() { @@ -1208,21 +986,6 @@ std::vector wsl::windows::common::wslutil::ListRunningProcesses() return pids; } -void wsl::windows::common::wslutil::MsiMessageCallback(INSTALLMESSAGE type, LPCWSTR message) -{ - switch (type) - { - case INSTALLMESSAGE_ERROR: - case INSTALLMESSAGE_FATALEXIT: - case INSTALLMESSAGE_WARNING: - wprintf(L"%ls\n", message); - break; - - default: - break; - } -} - std::pair wsl::windows::common::wslutil::OpenAnonymousPipe(DWORD Size, bool ReadPipeOverlapped, bool WritePipeOverlapped) { // Default to 4096 byte buffer, just like CreatePipe(). @@ -1377,171 +1140,6 @@ wil::unique_hlocal_string wsl::windows::common::wslutil::SidToString(_In_ PSID U return sid; } -int WINAPI InstallRecordHandler(void* context, UINT messageType, LPCWSTR message) -{ - try - { - WSL_LOG("MSIMessage", TraceLoggingValue(messageType, "type"), TraceLoggingValue(message, "message")); - auto type = (INSTALLMESSAGE)(0xFF000000 & (UINT)messageType); - - if (type == INSTALLMESSAGE_ERROR || type == INSTALLMESSAGE_FATALEXIT || type == INSTALLMESSAGE_WARNING) - { - WriteInstallLog(std::format("MSI message: {}", message)); - } - - auto* callback = reinterpret_cast*>(context); - if (callback != nullptr) - { - (*callback)(type, message); - } - } - CATCH_LOG(); - - return IDOK; -} - -void ConfigureMsiLogging(_In_opt_ LPCWSTR LogFile, _In_ const std::function& Callback) -{ - if (LogFile != nullptr) - { - LOG_IF_WIN32_ERROR(MsiEnableLog(INSTALLLOGMODE_VERBOSE | INSTALLLOGMODE_EXTRADEBUG | INSTALLLOGMODE_PROGRESS, LogFile, 0)); - } - - MsiSetExternalUI( - &InstallRecordHandler, - INSTALLLOGMODE_FATALEXIT | INSTALLLOGMODE_ERROR | INSTALLLOGMODE_WARNING | INSTALLLOGMODE_USER | INSTALLLOGMODE_INFO | - INSTALLLOGMODE_RESOLVESOURCE | INSTALLLOGMODE_OUTOFDISKSPACE | INSTALLLOGMODE_ACTIONSTART | INSTALLLOGMODE_ACTIONDATA | - INSTALLLOGMODE_COMMONDATA | INSTALLLOGMODE_INITIALIZE | INSTALLLOGMODE_TERMINATE | INSTALLLOGMODE_SHOWDIALOG, - (void*)&Callback); - - MsiSetInternalUI(INSTALLUILEVEL(INSTALLUILEVEL_NONE | INSTALLUILEVEL_UACONLY | INSTALLUILEVEL_SOURCERESONLY), nullptr); -} - -int wsl::windows::common::wslutil::UpdatePackage(bool PreRelease, bool Repair) -{ - // Register a console control handler so "^C" is not printed when the app platform terminates the process. - THROW_IF_WIN32_BOOL_FALSE(SetConsoleCtrlHandler( - [](DWORD ctrlType) { - if (ctrlType == CTRL_C_EVENT) - { - ExitProcess(0); - } - return FALSE; - }, - TRUE)); - - auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [] { SetConsoleCtrlHandler(nullptr, FALSE); }); - - try - { - return UpdatePackageImpl(PreRelease, Repair); - } - catch (...) - { - // Rethrowing via WIL is required for the error context to be properly set in case a winrt exception was thrown. - THROW_HR(wil::ResultFromCaughtException()); - } -} - -UINT wsl::windows::common::wslutil::UpgradeViaMsi( - _In_ LPCWSTR PackageLocation, _In_opt_ LPCWSTR ExtraArgs, _In_opt_ LPCWSTR LogFile, _In_ const std::function& Callback) -{ - WriteInstallLog(std::format("Upgrading via MSI package: {}. Args: {}", PackageLocation, ExtraArgs != nullptr ? ExtraArgs : L"")); - - ConfigureMsiLogging(LogFile, Callback); - - auto result = MsiInstallProduct(PackageLocation, ExtraArgs); - WSL_LOG( - "MsiInstallResult", - TraceLoggingValue(result, "result"), - TraceLoggingValue(ExtraArgs != nullptr ? ExtraArgs : L"", "ExtraArgs")); - - WriteInstallLog(std::format("MSI upgrade result: {}", result)); - - return result; -} - -UINT wsl::windows::common::wslutil::UninstallViaMsi(_In_opt_ LPCWSTR LogFile, _In_ const std::function& Callback) -{ - const auto key = OpenLxssMachineKey(KEY_READ); - const auto productCode = ReadString(key.get(), L"Msi", L"ProductCode", nullptr); - - WriteInstallLog(std::format("Uninstalling MSI package: {}", productCode)); - - ConfigureMsiLogging(LogFile, Callback); - - auto result = MsiConfigureProduct(productCode.c_str(), 0, INSTALLSTATE_ABSENT); - WSL_LOG("MsiUninstallResult", TraceLoggingValue(result, "result")); - - WriteInstallLog(std::format("MSI package uninstall result: {}", result)); - - return result; -} - -wil::unique_hfile wsl::windows::common::wslutil::ValidateFileSignature(LPCWSTR Path) -{ - wil::unique_hfile fileHandle{CreateFileW(Path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr)}; - THROW_LAST_ERROR_IF(!fileHandle); - - GUID action = WINTRUST_ACTION_GENERIC_VERIFY_V2; - WINTRUST_DATA trust{}; - trust.cbStruct = sizeof(trust); - trust.dwUIChoice = WTD_UI_NONE; - trust.dwUnionChoice = WTD_CHOICE_FILE; - trust.dwStateAction = WTD_STATEACTION_VERIFY; - - WINTRUST_FILE_INFO file = {0}; - file.cbStruct = sizeof(file); - file.hFile = fileHandle.get(); - trust.pFile = &file; - - auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { - trust.dwStateAction = WTD_STATEACTION_CLOSE; - WinVerifyTrust(nullptr, &action, &trust); - }); - - THROW_IF_WIN32_ERROR(WinVerifyTrust(nullptr, &action, &trust)); - - return fileHandle; -} - -void wsl::windows::common::wslutil::WriteInstallLog(const std::string& Content) -try -{ - static std::wstring path = wil::GetWindowsDirectoryW() + L"\\temp\\wsl-install-log.txt"; - - // Wait up to 10 seconds for the log file mutex - wil::unique_handle mutex{CreateMutex(nullptr, true, L"Global\\WslInstallLog")}; - THROW_LAST_ERROR_IF(!mutex); - - THROW_LAST_ERROR_IF(WaitForSingleObject(mutex.get(), 10 * 1000) != WAIT_OBJECT_0); - - wil::unique_handle file{CreateFile( - path.c_str(), GENERIC_ALL, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_ALWAYS, 0, nullptr)}; - - THROW_LAST_ERROR_IF(!file); - - LARGE_INTEGER size{}; - THROW_IF_WIN32_BOOL_FALSE(GetFileSizeEx(file.get(), &size)); - - // Append to the file if its size is below 10MB, otherwise truncate. - if (size.QuadPart < 10 * _1MB) - { - THROW_LAST_ERROR_IF(SetFilePointer(file.get(), 0, nullptr, FILE_END) == INVALID_SET_FILE_POINTER); - } - else - { - THROW_IF_WIN32_BOOL_FALSE(SetEndOfFile(file.get())); - } - - static auto processName = wil::GetModuleFileNameW(); - auto logLine = std::format("{:%FT%TZ} {}[{}]: {}\n", std::chrono::system_clock::now(), processName, WSL_PACKAGE_VERSION, Content); - - DWORD bytesWritten{}; - THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), logLine.c_str(), static_cast(logLine.size()), &bytesWritten, nullptr)); -} -CATCH_LOG(); - winrt::Windows::Management::Deployment::PackageVolume wsl::windows::common::wslutil::GetSystemVolume() try { diff --git a/src/windows/common/wslutil.h b/src/windows/common/wslutil.h index a382be882..69e0e1c2a 100644 --- a/src/windows/common/wslutil.h +++ b/src/windows/common/wslutil.h @@ -70,8 +70,6 @@ void AssertValidPrintfArg() static_assert(std::is_fundamental_v || std::is_same_v || std::is_same_v || std::is_same_v); } -int CallMsiPackage(); - template wil::com_ptr CoGetCallContext(); @@ -153,8 +151,6 @@ bool IsVirtualMachinePlatformInstalled(); std::vector ListRunningProcesses(); -void MsiMessageCallback(INSTALLMESSAGE type, LPCWSTR message); - std::pair OpenAnonymousPipe(DWORD Size, bool ReadPipeOverlapped, bool WritePipeOverlapped); wil::unique_handle OpenCallingProcess(_In_ DWORD access); @@ -198,18 +194,8 @@ void SetCrtEncoding(int Mode); void SetThreadDescription(LPCWSTR Name); -wil::unique_hfile ValidateFileSignature(LPCWSTR Path); - wil::unique_hlocal_string SidToString(_In_ PSID Sid); -int UpdatePackage(bool PreRelease, bool Repair); - -UINT UpgradeViaMsi(_In_ LPCWSTR PackageLocation, _In_opt_ LPCWSTR ExtraArgs, _In_opt_ LPCWSTR LogFile, _In_ const std::function& callback); - -UINT UninstallViaMsi(_In_opt_ LPCWSTR LogFile, _In_ const std::function& callback); - -void WriteInstallLog(const std::string& Content); - winrt::Windows::Management::Deployment::PackageVolume GetSystemVolume(); } // namespace wsl::windows::common::wslutil diff --git a/src/windows/service/exe/CMakeLists.txt b/src/windows/service/exe/CMakeLists.txt index 620e1276f..cfe5b8649 100644 --- a/src/windows/service/exe/CMakeLists.txt +++ b/src/windows/service/exe/CMakeLists.txt @@ -57,7 +57,9 @@ add_compile_definitions(USE_COM_CONTEXT_DEF=1) set_target_properties(wslservice PROPERTIES LINK_FLAGS "/merge:minATL=.rdata /include:__minATLObjMap_LxssUserSession_COM") target_link_libraries(wslservice ${COMMON_LINK_LIBRARIES} - computecore + ${MSI_LINK_LIBRARIES} + ${HCS_LINK_LIBRARIES} + ${SERVICE_LINK_LIBRARIES} common configfile legacy_stdio_definitions diff --git a/src/windows/service/exe/DistributionRegistration.h b/src/windows/service/exe/DistributionRegistration.h index 61fc161d4..1da0848a9 100644 --- a/src/windows/service/exe/DistributionRegistration.h +++ b/src/windows/service/exe/DistributionRegistration.h @@ -107,7 +107,7 @@ namespace Property { inline DistributionPropertyWithDefault> DefaultEnvironment{ L"DefaultEnvironment", - {"HOSTTYPE=x86_64", + {"HOSTTYPE=" DISTRO_HOSTTYPE, "LANG=en_US.UTF-8", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games", "TERM=xterm-256color"}}; diff --git a/src/windows/service/exe/PluginManager.cpp b/src/windows/service/exe/PluginManager.cpp index b6a661e69..e4d23f226 100644 --- a/src/windows/service/exe/PluginManager.cpp +++ b/src/windows/service/exe/PluginManager.cpp @@ -13,6 +13,7 @@ Module Name: --*/ #include "precomp.h" +#include "install.h" #include "PluginManager.h" #include "WslPluginApi.h" #include "LxssUserSessionFactory.h" @@ -150,7 +151,7 @@ void PluginManager::LoadPlugin(LPCWSTR Name, LPCWSTR ModulePath) wil::unique_hfile pluginHandle; if constexpr (wsl::shared::OfficialBuild) { - pluginHandle = wsl::windows::common::wslutil::ValidateFileSignature(ModulePath); + pluginHandle = wsl::windows::common::install::ValidateFileSignature(ModulePath); WI_ASSERT(pluginHandle.is_valid()); } diff --git a/src/windows/service/exe/WslCoreVm.cpp b/src/windows/service/exe/WslCoreVm.cpp index 39552dd9c..2d26d2bee 100644 --- a/src/windows/service/exe/WslCoreVm.cpp +++ b/src/windows/service/exe/WslCoreVm.cpp @@ -537,7 +537,7 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken // Create hvsocket connection for DNS tunneling if enabled. wil::unique_socket dnsTunnelingSocket; - if (m_vmConfig.EnableDnsTunneling) + if (message->EnableDnsTunneling) { dnsTunnelingSocket = AcceptConnection(m_vmConfig.KernelBootTimeout); } @@ -576,7 +576,7 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken } else if (m_vmConfig.NetworkingMode == NetworkingMode::VirtioProxy) { - wsl::core::VirtioNetworkingFlags flags = wsl::core::VirtioNetworkingFlags::None; + wsl::core::VirtioNetworkingFlags flags = wsl::core::VirtioNetworkingFlags::Ipv6; WI_SetFlagIf(flags, wsl::core::VirtioNetworkingFlags::LocalhostRelay, m_vmConfig.EnableLocalhostRelay); m_networkingEngine = std::make_unique( std::move(gnsChannel), flags, LX_INIT_RESOLVCONF_FULL_HEADER, m_guestDeviceManager, m_userToken); @@ -1555,8 +1555,8 @@ std::wstring WslCoreVm::GenerateConfigJson() // Enable timesync workaround to sync on resume from sleep in modern standby. kernelCmdLine += L" hv_utils.timesync_implicit=1"; - // If using virtio-9p, enable SWIOTLB as a perf optimization (will cause VM to consume 64MB more memory). - if (m_vmConfig.EnableVirtio9p) + // If using virtio features, enable SWIOTLB as a perf optimization (will cause VM to consume 64MB more memory). + if (m_vmConfig.EnableVirtio9p || m_vmConfig.EnableVirtioFs || m_vmConfig.NetworkingMode == NetworkingMode::VirtioProxy) { kernelCmdLine += L" swiotlb=force"; } @@ -1931,9 +1931,7 @@ bool WslCoreVm::InitializeDrvFsLockHeld(_In_ HANDLE UserToken) bool WslCoreVm::IsDnsTunnelingSupported() const { - WI_ASSERT( - m_vmConfig.NetworkingMode == NetworkingMode::Nat || m_vmConfig.NetworkingMode == NetworkingMode::Mirrored || - m_vmConfig.NetworkingMode == NetworkingMode::VirtioProxy); + WI_ASSERT(m_vmConfig.NetworkingMode == NetworkingMode::Nat || m_vmConfig.NetworkingMode == NetworkingMode::Mirrored); return SUCCEEDED_LOG(wsl::core::networking::DnsResolver::LoadDnsResolverMethods()); } @@ -2128,7 +2126,7 @@ void WslCoreVm::WaitForPmemDeviceInVm(_In_ ULONG PmemId) } _Requires_lock_held_(m_guestDeviceLock) -std::wstring WslCoreVm::AddVirtioFsShare(_In_ bool Admin, _In_ PCWSTR Path, _In_ PCWSTR Options, _In_opt_ HANDLE UserToken) +std::pair WslCoreVm::AddVirtioFsShare(_In_ bool Admin, _In_ PCWSTR Path, _In_ PCWSTR Options, _In_opt_ HANDLE UserToken) { WI_ASSERT(m_vmConfig.EnableVirtioFs); @@ -2190,7 +2188,7 @@ std::wstring WslCoreVm::AddVirtioFsShare(_In_ bool Admin, _In_ PCWSTR Path, _In_ TraceLoggingValue(created, "created"), TraceLoggingValue(m_virtioFsShares.size(), "shareCount")); - return tag; + return {tag, sharePath}; } void WslCoreVm::OnCrash(_In_ LPCWSTR Details) @@ -2581,12 +2579,13 @@ try return; } - auto respondWithTag = [&](const std::wstring& tag, HRESULT result) { + auto respondWithTag = [&](const std::wstring& tag, const std::wstring& source, HRESULT result) { // Respond to the guest with the tag that should be used to mount the device. wsl::shared::MessageWriter response(LxInitMessageAddVirtioFsDeviceResponse); response->Result = SUCCEEDED(result) ? 0 : EINVAL; // TODO: Improved HRESULT -> errno mapping. response.WriteString(response->TagOffset, tag); + response.WriteString(response->SourceOffset, source); channel.SendMessage(response.Span()); }; @@ -2594,7 +2593,8 @@ try if (message->MessageType == LxInitMessageAddVirtioFsDevice) { std::wstring tag; - const auto result = wil::ResultFromException([this, span, &tag]() { + std::wstring source; + const auto result = wil::ResultFromException([this, span, &tag, &source]() { const auto* addShare = gslhelpers::try_get_struct(span); THROW_HR_IF(E_UNEXPECTED, !addShare); @@ -2605,15 +2605,16 @@ try // Acquire the lock and attempt to add the device. auto guestDeviceLock = m_guestDeviceLock.lock_exclusive(); - tag = AddVirtioFsShare(addShare->Admin, pathWide.c_str(), optionsWide.c_str()); + std::tie(tag, source) = AddVirtioFsShare(addShare->Admin, pathWide.c_str(), optionsWide.c_str()); }); - respondWithTag(tag, result); + respondWithTag(tag, source, result); } else if (message->MessageType == LxInitMessageRemountVirtioFsDevice) { std::wstring newTag; - const auto result = wil::ResultFromException([this, span, &newTag]() { + std::wstring source; + const auto result = wil::ResultFromException([this, span, &newTag, &source]() { const auto* remountShare = gslhelpers::try_get_struct(span); THROW_HR_IF(E_UNEXPECTED, !remountShare); @@ -2623,28 +2624,13 @@ try const auto foundShare = FindVirtioFsShare(tagWide.c_str(), !remountShare->Admin); THROW_HR_IF_MSG(E_UNEXPECTED, !foundShare.has_value(), "Unknown tag %ls", tagWide.c_str()); - newTag = AddVirtioFsShare(remountShare->Admin, foundShare->Path.c_str(), foundShare->OptionsString().c_str()); - }); - - respondWithTag(newTag, result); - } - else if (message->MessageType == LxInitMessageQueryVirtioFsDevice) - { - std::wstring newTag; - const auto result = wil::ResultFromException([this, span, &newTag]() { - const auto* query = gslhelpers::try_get_struct(span); - THROW_HR_IF(E_UNEXPECTED, !query); - - const std::string tag = wsl::shared::string::FromSpan(span, query->TagOffset); - const auto tagWide = wsl::shared::string::MultiByteToWide(tag); - auto guestDeviceLock = m_guestDeviceLock.lock_exclusive(); - const auto foundShare = FindVirtioFsShare(tagWide.c_str()); - THROW_HR_IF_MSG(E_UNEXPECTED, !foundShare.has_value(), "Unknown tag %ls", tagWide.c_str()); + std::tie(newTag, source) = + AddVirtioFsShare(remountShare->Admin, foundShare->Path.c_str(), foundShare->OptionsString().c_str()); - newTag = foundShare->Path; + WI_ASSERT(source == foundShare->Path); }); - respondWithTag(newTag, result); + respondWithTag(newTag, source, result); } else { diff --git a/src/windows/service/exe/WslCoreVm.h b/src/windows/service/exe/WslCoreVm.h index 6178c54b7..420492b05 100644 --- a/src/windows/service/exe/WslCoreVm.h +++ b/src/windows/service/exe/WslCoreVm.h @@ -183,7 +183,7 @@ class WslCoreVm void AddPlan9Share(_In_ PCWSTR AccessName, _In_ PCWSTR Path, _In_ UINT32 Port, _In_ wsl::windows::common::hcs::Plan9ShareFlags Flags, _In_ HANDLE UserToken, _In_ PCWSTR VirtIoTag); _Requires_lock_held_(m_guestDeviceLock) - std::wstring AddVirtioFsShare(_In_ bool Admin, _In_ PCWSTR Path, _In_ PCWSTR Options, _In_opt_ HANDLE UserToken = nullptr); + std::pair AddVirtioFsShare(_In_ bool Admin, _In_ PCWSTR Path, _In_ PCWSTR Options, _In_opt_ HANDLE UserToken = nullptr); _Requires_lock_held_(m_lock) ULONG AttachDiskLockHeld(_In_ PCWSTR Disk, _In_ DiskType Type, _In_ MountFlags Flags, _In_ std::optional Lun, _In_ bool IsUserDisk, _In_ HANDLE UserToken); diff --git a/src/windows/service/inc/wslservice.idl b/src/windows/service/inc/wslservice.idl index c68ad7567..78ec97e68 100644 --- a/src/windows/service/inc/wslservice.idl +++ b/src/windows/service/inc/wslservice.idl @@ -95,7 +95,7 @@ cpp_quote("#define LXSS_DISTRO_VERSION_2 2") cpp_quote("#define LXSS_DISTRO_VERSION_CURRENT LXSS_DISTRO_VERSION_2") cpp_quote("#define LXSS_DISTRO_USES_WSL_FS(DistroVersion) (DistroVersion >= LXSS_DISTRO_VERSION_2)") -cpp_quote("#define LXSS_DISTRO_DEFAULT_ENVIRONMENT \"HOSTTYPE=x86_64\0LANG=en_US.UTF-8\0PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games\0TERM=xterm-256color\0\"") +cpp_quote("#define LXSS_DISTRO_DEFAULT_ENVIRONMENT \"HOSTTYPE=\" DISTRO_HOSTTYPE \"\\0LANG=en_US.UTF-8\\0PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games\\0TERM=xterm-256color\\0\"") cpp_quote("#define LXSS_DISTRO_DEFAULT_KERNEL_COMMAND_LINE \"BOOT_IMAGE=/kernel init=/init\"") diff --git a/src/windows/wsl/CMakeLists.txt b/src/windows/wsl/CMakeLists.txt index ae52c0b88..13fbb1425 100644 --- a/src/windows/wsl/CMakeLists.txt +++ b/src/windows/wsl/CMakeLists.txt @@ -9,8 +9,11 @@ add_executable(wsl ${SOURCES} ${HEADERS}) target_link_libraries(wsl ${COMMON_LINK_LIBRARIES} + ${MSI_LINK_LIBRARIES} common - runtimeobject.lib) + runtimeobject.lib + delayimp.lib) +set_target_properties(wsl PROPERTIES LINK_FLAGS "/DELAYLOAD:msi.dll /DELAYLOAD:WINTRUST.dll") target_precompile_headers(wsl REUSE_FROM common) set_target_properties(wsl PROPERTIES FOLDER windows) \ No newline at end of file diff --git a/src/windows/wslg/CMakeLists.txt b/src/windows/wslg/CMakeLists.txt index 0a28c89d3..a30d4d1f8 100644 --- a/src/windows/wslg/CMakeLists.txt +++ b/src/windows/wslg/CMakeLists.txt @@ -12,7 +12,10 @@ add_dependencies(wslg target_link_libraries(wslg ${COMMON_LINK_LIBRARIES} - common) + ${MSI_LINK_LIBRARIES} + common + delayimp.lib) +set_target_properties(wslg PROPERTIES LINK_FLAGS "/DELAYLOAD:msi.dll /DELAYLOAD:WINTRUST.dll") target_precompile_headers(wslg REUSE_FROM common) set_target_properties(wslg PROPERTIES FOLDER windows) \ No newline at end of file diff --git a/src/windows/wslinstall/CMakeLists.txt b/src/windows/wslinstall/CMakeLists.txt index 921c22e3d..4f9fcf439 100644 --- a/src/windows/wslinstall/CMakeLists.txt +++ b/src/windows/wslinstall/CMakeLists.txt @@ -10,8 +10,8 @@ target_precompile_headers(wslinstall REUSE_FROM common) target_link_libraries(wslinstall ${COMMON_LINK_LIBRARIES} + ${MSI_LINK_LIBRARIES} common legacy_stdio_definitions - Msi.lib Crypt32.lib sfc.lib) \ No newline at end of file diff --git a/src/windows/wslinstall/DllMain.cpp b/src/windows/wslinstall/DllMain.cpp index 119e420c5..c9ab7d800 100644 --- a/src/windows/wslinstall/DllMain.cpp +++ b/src/windows/wslinstall/DllMain.cpp @@ -13,6 +13,7 @@ Module Name: --*/ #include "precomp.h" +#include "install.h" #include #include #include @@ -24,6 +25,7 @@ using unique_msi_handle = wil::unique_any InstallMsipackageImpl() } }; - auto result = wsl::windows::common::wslutil::UpgradeViaMsi( + auto result = wsl::windows::common::install::UpgradeViaMsi( GetMsiPackagePath().c_str(), L"SKIPMSIX=1", logFile.has_value() ? logFile->c_str() : nullptr, messageCallback); WSL_LOG("MSIUpgradeResult", TraceLoggingValue(result, "result"), TraceLoggingValue(errors.c_str(), "errorMessage")); @@ -140,7 +141,7 @@ std::shared_ptr LaunchInstall() return {}; } - wsl::windows::common::wslutil::WriteInstallLog(std::format("Starting upgrade via WslInstaller. Previous version: {}", existingVersion)); + wsl::windows::common::install::WriteInstallLog(std::format("Starting upgrade via WslInstaller. Previous version: {}", existingVersion)); // Return an existing install if any if (auto ptr = weak_context.lock(); ptr != nullptr) diff --git a/test/README.md b/test/README.md index 05b5c1640..0f986dda0 100644 --- a/test/README.md +++ b/test/README.md @@ -12,6 +12,16 @@ Executing tests with TAEF is done by invoking the `TE.exe` binary: 2. Navigate to the subdirectory containing the built test binaries (`bin///`) 3. Execute the binaries via invoking TE and passing the test dll/s as arguments: `TE.exe test1.dll test2.dll test3.dll` +## test.bat Options + +The following options are handled by `test.bat` / `run-tests.ps1` before invoking TE.exe: + +### **/attachdebugger** + +Automatically launches WinDbgX and attaches it to the test host process. Requires [WinDbg](https://aka.ms/windbg) to be installed (`winget install Microsoft.WinDbg`). Under the hood it passes `/waitfordebugger /inproc` to TE.exe so tests run in-process, then attaches WinDbgX directly to `TE.exe`. + +`test.bat /attachdebugger /name:*MyTest*` + ## Useful **TE.exe** Command Line Parameters for Debugging/Executing Tests Command Line parameters are passed to `TE.exe` after supplying the target `.dll`: diff --git a/test/windows/CMakeLists.txt b/test/windows/CMakeLists.txt index 0759bb980..39bb83b72 100644 --- a/test/windows/CMakeLists.txt +++ b/test/windows/CMakeLists.txt @@ -24,6 +24,9 @@ target_link_libraries(wsltests common ${TAEF_LINK_LIBRARIES} ${COMMON_LINK_LIBRARIES} + ${MSI_LINK_LIBRARIES} + ${HCS_LINK_LIBRARIES} + ${SERVICE_LINK_LIBRARIES} VirtDisk.lib Wer.lib Dbghelp.lib diff --git a/test/windows/Common.cpp b/test/windows/Common.cpp index cee8848e4..99cf3cf45 100644 --- a/test/windows/Common.cpp +++ b/test/windows/Common.cpp @@ -250,9 +250,9 @@ Return Value: { THROW_HR_MSG( E_UNEXPECTED, - "Command \"%ls\"" + "Command \"%ls\" " "returned unexpected exit code (%lu != %i). " - "Stdout: '%ls'" + "Stdout: '%ls' " "Stderr: '%ls'", Cmd, ExitCode, @@ -1450,8 +1450,6 @@ std::wstring LxssGenerateTestConfig(TestConfigDefaults Default) return value; }; - // TODO: Reset guiApplications to true by default once the virtio hang is solved. - std::wstring newConfig = L"[wsl2]\n" L"crashDumpFolder=" + @@ -1464,7 +1462,7 @@ std::wstring LxssGenerateTestConfig(TestConfigDefaults Default) EscapePath(kernelLogs) + L"\n" L"telemetry=false\n" + - boolOptionToString(L"safeMode", Default.safeMode, false) + boolOptionToString(L"guiApplications", Default.guiApplications, false) + + boolOptionToString(L"safeMode", Default.safeMode, false) + boolOptionToString(L"guiApplications", Default.guiApplications, true) + L"earlyBootLogging=false\n" + networkingModeToString(Default.networkingMode) + drvFsModeToString(Default.drvFsMode); if (Default.kernel.has_value()) diff --git a/test/windows/DrvFsTests.cpp b/test/windows/DrvFsTests.cpp index 7e012b9c8..9307b6961 100644 --- a/test/windows/DrvFsTests.cpp +++ b/test/windows/DrvFsTests.cpp @@ -1341,12 +1341,10 @@ class WSL1 : public DrvFsTests WSL2_DRVFS_TEST_CLASS(Plan9); +WSL2_DRVFS_TEST_CLASS(VirtioFs); + // Disabled while an issue with the 6.1 Linux kernel causing disk corruption is investigated. // TODO: Enable again once the issue is resolved // WSL2_DRVFS_TEST_CLASS(Virtio9p); -// Disabled because it causes too much noise. -// TODO: Enable again once virtiofs is stable -// WSL2_DRVFS_TEST_CLASS(VirtioFs); - } // namespace DrvFsTests \ No newline at end of file diff --git a/test/windows/InstallerTests.cpp b/test/windows/InstallerTests.cpp index dfa5f770a..c578ba91a 100644 --- a/test/windows/InstallerTests.cpp +++ b/test/windows/InstallerTests.cpp @@ -485,57 +485,6 @@ class InstallerTests ValidatePackageInstalledProperly(); } - TEST_METHOD(InitrdImgLifecycle) - { - // initrd.img is generated during installation from the init binary. - // It should: - // 1. Exist after a fresh install (generated by custom action) - // 2. Be re-created when the component is upgraded - // 3. Persist when reinstalling the same version - // 4. Be removed on uninstall - - auto initrdPath = m_installedPath / L"tools" / L"initrd.img"; - - // Uninstall and install an older version to get a clean state - UninstallMsi(); - VERIFY_IS_FALSE(IsMsiPackageInstalled()); - - // Case 1: Install older version (2.5.10 which ships initrd.img in the MSI) - InstallGitHubRelease(L"2.5.10"); - VERIFY_IS_TRUE(IsMsiPackageInstalled()); - VERIFY_IS_TRUE(std::filesystem::exists(initrdPath), L"initrd.img should exist after installing 2.5.10"); - ValidatePackageInstalledProperly(L"2.5.10.0"); - - // Case 2: Upgrade to current version - initrd.img should be regenerated by custom action - InstallMsi(); - VERIFY_IS_TRUE(IsMsiPackageInstalled()); - VERIFY_IS_TRUE(std::filesystem::exists(initrdPath), L"initrd.img should exist after upgrade (generated by installer)"); - ValidatePackageInstalledProperly(); - - // Case 3: Reinstall same version - initrd.img should persist - InstallMsi(); - VERIFY_IS_TRUE(IsMsiPackageInstalled()); - VERIFY_IS_TRUE(std::filesystem::exists(initrdPath), L"initrd.img should exist after reinstalling same version"); - ValidatePackageInstalledProperly(); - - // Case 4: Downgrade to older version - InstallGitHubRelease(L"2.5.10"); - VERIFY_IS_TRUE(IsMsiPackageInstalled()); - VERIFY_IS_TRUE(std::filesystem::exists(initrdPath), L"initrd.img should exist after downgrade"); - ValidatePackageInstalledProperly(L"2.5.10.0"); - - // Case 5: Uninstall - initrd.img should be removed - UninstallMsi(); - VERIFY_IS_FALSE(IsMsiPackageInstalled()); - VERIFY_IS_FALSE(std::filesystem::exists(initrdPath), L"initrd.img should be removed on uninstall"); - - // Reinstall to leave the system in a consistent state for other tests - InstallMsi(); - VERIFY_IS_TRUE(IsMsiPackageInstalled()); - VERIFY_IS_TRUE(std::filesystem::exists(initrdPath), L"initrd.img should exist after fresh install"); - ValidatePackageInstalledProperly(); - } - TEST_METHOD(UpgradeInstallsTheMsiPackage) { // Remove the MSIX package diff --git a/test/windows/NetworkTests.cpp b/test/windows/NetworkTests.cpp index 6df73175d..b8a85ab5f 100644 --- a/test/windows/NetworkTests.cpp +++ b/test/windows/NetworkTests.cpp @@ -2023,6 +2023,134 @@ class NetworkTests return std::tuple(std::move(process), std::move(read)); } + // Bind port 0 in the guest and return the process handle and the kernel-assigned port. + // Uses socat's -dd output to extract the actual port from the "listening on" line. + static std::tuple BindGuestPortZero(bool Ipv6 = false) + { + auto [stdErrRead, stdErrWrite] = CreateSubprocessPipe(false, true); + const std::wstring protocol = Ipv6 ? L"TCP6-LISTEN:0" : L"TCP4-LISTEN:0"; + const std::wstring wslCmd = L"socat -dd " + protocol + L" STDOUT"; + auto cmd = LxssGenerateWslCommandLine(wslCmd.data()); + + auto process = LxsstuStartProcess(cmd.data(), nullptr, nullptr, stdErrWrite.get()); + stdErrWrite.reset(); + + // Parse the assigned port from socat's debug output. + // socat -dd prints a line like: "... listening on AF=2 0.0.0.0:PORT" + std::string output(512, '\0'); + DWORD writeOffset = 0; + uint16_t assignedPort = 0; + bool found = false; + + while (!found) + { + // Grow the buffer if full to avoid zero-byte reads and infinite loops. + if (writeOffset == output.size()) + { + output.resize(output.size() * 2); + } + + DWORD bytesRead = 0; + if (!ReadFile(stdErrRead.get(), output.data() + writeOffset, static_cast(output.size() - writeOffset), &bytesRead, nullptr)) + { + break; + } + + if (bytesRead == 0) + { + break; + } + + writeOffset += bytesRead; + LogInfo("output %hs", output.c_str()); + std::string_view outputView(output.data(), writeOffset); + auto pos = outputView.find("listening on"); + if (pos != std::string_view::npos) + { + // Limit the search to just the "listening on" line to avoid + // matching colons in subsequent debug lines socat may emit. + auto lineEnd = outputView.find('\n', pos); + auto line = outputView.substr(pos, lineEnd != std::string_view::npos ? lineEnd - pos : std::string_view::npos); + + // Find the last ':' before the port digits. For IPv6, socat outputs + // "listening on AF=10 :::PORT", so using find() would match the + // first colon in the address instead of the port separator. + auto colonPos = line.rfind(':'); + if (colonPos != std::string_view::npos) + { + auto portStr = line.substr(colonPos + 1); + auto end = portStr.find_first_not_of("0123456789"); + if (end != std::string_view::npos) + { + portStr = portStr.substr(0, end); + } + + if (portStr.empty()) + { + continue; + } + + assignedPort = static_cast(std::stoi(std::string(portStr))); + found = true; + } + } + } + + VERIFY_IS_TRUE(found); + VERIFY_IS_TRUE(assignedPort > 0); + LogInfo("Port-0 bind resolved to port %u", assignedPort); + + return {std::move(process), assignedPort}; + } + + static void VerifyPortZeroBindIsTracked(bool verifyRelease = true) + { + // Make sure the VM doesn't time out while we wait for async port resolution + WslKeepAlive keepAlive; + + // Bind port 0 in the guest - the kernel assigns an ephemeral port. + // The port tracker intercepts the bind() via seccomp and defers lookup + // to a background thread that resolves the actual port via getsockname(). + auto [guestProcess, assignedPort] = BindGuestPortZero(); + + // The port-0 resolution is asynchronous (deferred to a background thread). + // Retry until the host port tracker registers the port, blocking the host bind. + VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout( + [&assignedPort]() { + wil::unique_socket sock(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)); + THROW_LAST_ERROR_IF(!sock); + + SOCKADDR_IN addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(assignedPort); + THROW_HR_IF(E_FAIL, bind(sock.get(), reinterpret_cast(&addr), sizeof(addr)) != SOCKET_ERROR); + }, + std::chrono::seconds(1), + std::chrono::seconds(30))); + + if (!verifyRelease) + { + return; + } + + // Kill the guest process so the port tracker releases the port. + guestProcess.reset(); + + // Retry until the host can bind the port again, confirming it was released. + VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout( + [&assignedPort]() { + wil::unique_socket sock(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)); + THROW_LAST_ERROR_IF(!sock); + + SOCKADDR_IN addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(assignedPort); + THROW_HR_IF(E_FAIL, bind(sock.get(), reinterpret_cast(&addr), sizeof(addr)) == SOCKET_ERROR); + }, + std::chrono::seconds(1), + std::chrono::minutes(2))); + } + template static void VerifyNotBound(T& Address, int AddressFamily, int Protocol) { @@ -2729,7 +2857,10 @@ class NetworkTests { if (std::regex_search(line, match, defaultRoutePattern) && match.size() >= 3) { - VERIFY_IS_FALSE(state.DefaultRoute.has_value()); + if (state.DefaultRoute.has_value()) + { + continue; + } state.DefaultRoute = {{match.str(1), match.str(2), {}, match.size() > 4 && match[4].matched ? std::stoi(match.str(4)) : 0}}; } @@ -2754,6 +2885,24 @@ class NetworkTests return GetRoutingTableState(out, defaultRoutePattern, routePattern); } + static void WaitForIpv6DefaultRoute() + { + const auto timeout = std::chrono::steady_clock::now() + std::chrono::seconds(30); + while (std::chrono::steady_clock::now() < timeout) + { + auto state = GetIpv6RoutingTableState(); + if (state.DefaultRoute.has_value()) + { + return; + } + + LogInfo("Waiting for IPv6 default route..."); + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + + VERIFY_FAIL(L"Timed out waiting for IPv6 default route"); + } + static RoutingTableState GetIpv6RoutingTableState() { auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"ip -6 route show"); @@ -3255,7 +3404,8 @@ class NetworkTests NET_LUID interfaceLuid{}; CONTINUE_IF_FAILED_WIN32(ConvertInterfaceGuidToLuid(&interfaceGuid, &interfaceLuid)); - for (const IP_ADAPTER_ADDRESSES* adapter = adapterAddresses.get(); adapter != nullptr; adapter = adapter->Next) + for (auto* adapter = reinterpret_cast(adapterAddresses.data()); adapter != nullptr; + adapter = adapter->Next) { if (interfaceLuid.Value == adapter->Luid.Value && adapter->FirstUnicastAddress != nullptr && adapter->FirstGatewayAddress != nullptr) { @@ -3267,18 +3417,63 @@ class NetworkTests return false; } - static std::unique_ptr GetAdapterAddresses(ADDRESS_FAMILY family) + static bool HostHasIpv6DnsServers() + { + ULONG bufferSize = 0; + constexpr ULONG flags = GAA_FLAG_SKIP_FRIENDLY_NAME | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS; + std::vector buffer; + ULONG result = GetAdaptersAddresses(AF_INET6, flags, nullptr, nullptr, &bufferSize); + while (result == ERROR_BUFFER_OVERFLOW) + { + buffer.resize(bufferSize); + result = GetAdaptersAddresses(AF_INET6, flags, nullptr, reinterpret_cast(buffer.data()), &bufferSize); + } + + if (result != NO_ERROR) + { + return false; + } + + DWORD bestIndex = 0; + SOCKADDR_IN6 dest{}; + dest.sin6_family = AF_INET6; + InetPtonW(AF_INET6, L"2001:4860:4860::8888", &dest.sin6_addr); + + if (GetBestInterfaceEx(reinterpret_cast(&dest), &bestIndex) != NO_ERROR) + { + return false; + } + + for (auto* adapter = reinterpret_cast(buffer.data()); adapter != nullptr; adapter = adapter->Next) + { + if (adapter->IfIndex != bestIndex) + { + continue; + } + + for (auto* dns = adapter->FirstDnsServerAddress; dns != nullptr; dns = dns->Next) + { + if (dns->Address.lpSockaddr->sa_family == AF_INET6) + { + return true; + } + } + } + + return false; + } + + static std::vector GetAdapterAddresses(ADDRESS_FAMILY family) { - ULONG result; constexpr ULONG flags = (GAA_FLAG_SKIP_FRIENDLY_NAME | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_INCLUDE_GATEWAYS); ULONG bufferSize = 0; - std::unique_ptr buffer; - - while ((result = GetAdaptersAddresses(family, flags, nullptr, buffer.get(), &bufferSize)) == ERROR_BUFFER_OVERFLOW) + std::vector buffer; + ULONG result = GetAdaptersAddresses(family, flags, nullptr, nullptr, &bufferSize); + while (result == ERROR_BUFFER_OVERFLOW) { - buffer.reset(static_cast(malloc(bufferSize))); - VERIFY_IS_NOT_NULL(buffer.get()); + buffer.resize(bufferSize); + result = GetAdaptersAddresses(family, flags, nullptr, reinterpret_cast(buffer.data()), &bufferSize); } VERIFY_WIN32_SUCCEEDED(result); @@ -3683,6 +3878,9 @@ class MirroredTests TEST_METHOD(LoopbackExplicit) { + // TODO: re-enable once OS build 29555 loopback regression is resolved. + SKIP_TEST_UNSTABLE(); + MIRRORED_NETWORKING_TEST_ONLY(); m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored})); @@ -3853,6 +4051,21 @@ class MirroredTests auto udpPort = NetworkTests::BindGuestPort(L"UDP4-LISTEN:0", true); } + TEST_METHOD(PortZeroBindIsTracked) + { + MIRRORED_NETWORKING_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored})); + WaitForMirroredStateInLinux(); + + // Skip port-release verification in mirrored mode. The host reserves a contiguous + // ephemeral port range via HcnReserveGuestNetworkServicePortRange that no Windows + // process can bind for the lifetime of the VM. Port-0 binds resolve to ports within + // this range, so even after the guest releases the port the host still cannot bind + // it — the range-level reservation remains, making release unverifiable. + NetworkTests::VerifyPortZeroBindIsTracked(false); + } + TEST_METHOD(ExplicitEphemeralBind) { MIRRORED_NETWORKING_TEST_ONLY(); @@ -4566,6 +4779,8 @@ class VirtioProxyTests return; } + NetworkTests::WaitForIpv6DefaultRoute(); + NetworkTests::GuestClient(L"tcp6-connect:bing.com:80"); } @@ -4584,8 +4799,14 @@ class VirtioProxyTests VERIFY_IS_TRUE(std::regex_match(out, pattern)); - // Verify that /etc/resolv.conf contains a 'search' line with DNS suffixes - VERIFY_IS_TRUE(out.find(L"search ") != std::wstring::npos); + // Verify that /etc/resolv.conf contains a 'search' line if the host has DNS suffixes + auto [suffixOut, suffixErr] = LxsstuLaunchPowershellAndCaptureOutput( + L"(@((Get-DnsClientGlobalSetting).SuffixSearchList) + @((Get-DnsClient).ConnectionSpecificSuffix) | Where-Object " + L"{$_}).Count"); + if (_wtoi(suffixOut.c_str()) > 0) + { + VERIFY_IS_TRUE(out.find(L"search ") != std::wstring::npos); + } } TEST_METHOD(GuestPortIsReleased) @@ -4602,23 +4823,18 @@ class VirtioProxyTests NetworkTests::BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false); } - const wil::unique_socket listenSocket(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)); - VERIFY_IS_TRUE(!!listenSocket); + wsl::shared::retry::RetryWithTimeout( + [&]() { + const wil::unique_socket listenSocket(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)); + THROW_HR_IF(E_ABORT, !listenSocket); - SOCKADDR_IN Address{}; - Address.sin_family = AF_INET; - Address.sin_port = htons(1234); - - const auto timeout = std::chrono::steady_clock::now() + std::chrono::minutes(2); - - bool bound = false; - while (!bound && std::chrono::steady_clock::now() < timeout) - { - bound = bind(listenSocket.get(), reinterpret_cast(&Address), sizeof(Address)) != SOCKET_ERROR; - std::this_thread::sleep_for(std::chrono::seconds(1)); - } - - VERIFY_IS_TRUE(bound); + SOCKADDR_IN Address{}; + Address.sin_family = AF_INET; + Address.sin_port = htons(1234); + THROW_HR_IF(E_FAIL, bind(listenSocket.get(), reinterpret_cast(&Address), sizeof(Address)) == SOCKET_ERROR); + }, + std::chrono::seconds(1), + std::chrono::minutes(2)); } TEST_METHOD(LoopbackGuestToHost) @@ -4655,40 +4871,175 @@ class VirtioProxyTests auto tcpPort = NetworkTests::BindGuestPort(L"TCP4-LISTEN:2345", true); } - TEST_METHOD(DnsResolutionBasic) + TEST_METHOD(PortZeroBindIsTracked) { VIRTIOPROXY_TEST_ONLY(); m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy})); - NetworkTests::VerifyDnsResolutionBasic(); + NetworkTests::VerifyPortZeroBindIsTracked(); } - TEST_METHOD(DnsResolutionDig) + TEST_METHOD(HttpProxySimple) + { + VIRTIOPROXY_TEST_ONLY(); + WINHTTP_PROXY_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .autoProxy = true})); + NetworkTests::VerifyHttpProxySimple(); + } + + TEST_METHOD(ConfigurationV6) { VIRTIOPROXY_TEST_ONLY(); m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy})); - NetworkTests::VerifyDnsResolutionDig(); + if (!NetworkTests::HostHasInternetConnectivity(AF_INET6)) + { + LogSkipped("Host does not have IPv6 internet connectivity. Skipping..."); + return; + } + + // Wait for the device host to send an IPv6 Router Advertisement + // before querying the interface state. + NetworkTests::WaitForIpv6DefaultRoute(); + + const auto state = NetworkTests::GetInterfaceState(L"eth0"); + + // Verify that the guest has a global IPv6 address assigned + VERIFY_IS_FALSE(state.V6Addresses.empty()); + + // Verify that the guest has an IPv6 default gateway + VERIFY_IS_TRUE(state.V6Gateway.has_value()); + + // Verify the guest IPv6 address matches the host global IPv6 address + const auto adapterAddresses = NetworkTests::GetAdapterAddresses(AF_INET6); + DWORD bestIndex = 0; + SOCKADDR_IN6 dest{}; + dest.sin6_family = AF_INET6; + InetPtonW(AF_INET6, L"2001:4860:4860::8888", &dest.sin6_addr); + VERIFY_ARE_EQUAL(NO_ERROR, GetBestInterfaceEx(reinterpret_cast(&dest), &bestIndex)); + + for (auto* adapter = reinterpret_cast(adapterAddresses.data()); adapter != nullptr; + adapter = adapter->Next) + { + if (adapter->IfIndex != bestIndex) + { + continue; + } + + for (auto* unicast = adapter->FirstUnicastAddress; unicast != nullptr; unicast = unicast->Next) + { + if (unicast->Address.lpSockaddr->sa_family != AF_INET6) + { + continue; + } + + const auto& sin6 = *reinterpret_cast(unicast->Address.lpSockaddr); + if (IN6_IS_ADDR_LINKLOCAL(&sin6.sin6_addr) || IN6_IS_ADDR_LOOPBACK(&sin6.sin6_addr)) + { + continue; + } + + SOCKADDR_INET hostAddr{}; + hostAddr.Ipv6 = sin6; + const auto hostAddrString = wsl::windows::common::string::SockAddrInetToWstring(hostAddr); + + // The host address may not be at index 0 due to SLAAC addresses from RA + bool addressFound = false; + for (const auto& v6Addr : state.V6Addresses) + { + if (v6Addr.Address == hostAddrString) + { + addressFound = true; + break; + } + } + VERIFY_IS_TRUE(addressFound); + break; + } + + break; + } } - TEST_METHOD(DnsResolutionRecordTypes) + TEST_METHOD(GuestPortIsReleasedV6) { VIRTIOPROXY_TEST_ONLY(); + WINDOWS_11_TEST_ONLY(); m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy})); - NetworkTests::VerifyDnsResolutionRecordTypes(); + // Make sure the VM doesn't time out + WslKeepAlive keepAlive; + + { + auto guestProcess = NetworkTests::BindGuestPort(L"TCP6-LISTEN:1234,bind=::", true); + NetworkTests::BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false, true); + } + + wsl::shared::retry::RetryWithTimeout( + [&]() { + const wil::unique_socket listenSocket(socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP)); + THROW_HR_IF(E_ABORT, !listenSocket); + + SOCKADDR_IN6 Address{}; + Address.sin6_family = AF_INET6; + Address.sin6_port = htons(1234); + THROW_HR_IF(E_FAIL, bind(listenSocket.get(), reinterpret_cast(&Address), sizeof(Address)) == SOCKET_ERROR); + }, + std::chrono::seconds(1), + std::chrono::minutes(2)); } - TEST_METHOD(HttpProxySimple) + TEST_METHOD(ConfigurationV6DnsServers) { VIRTIOPROXY_TEST_ONLY(); - WINHTTP_PROXY_TEST_ONLY(); - m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .autoProxy = true})); - NetworkTests::VerifyHttpProxySimple(); + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy})); + + if (!NetworkTests::HostHasInternetConnectivity(AF_INET6)) + { + LogSkipped("Host does not have IPv6 internet connectivity. Skipping..."); + return; + } + + if (!NetworkTests::HostHasIpv6DnsServers()) + { + LogSkipped("Host does not have IPv6 DNS servers configured. Skipping..."); + return; + } + + auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /etc/resolv.conf", 0); + + // Verify that /etc/resolv.conf contains at least one IPv6 nameserver + const std::wregex v6Pattern(L"(.|\\n)*nameserver [0-9a-fA-F:]+\\n(.|\\n)*"); + VERIFY_IS_TRUE(std::regex_match(out, v6Pattern)); + } + + TEST_METHOD(DnsResolutionBasic) + { + VIRTIOPROXY_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = false})); + NetworkTests::VerifyDnsResolutionBasic(); + } + + TEST_METHOD(DnsResolutionDig) + { + VIRTIOPROXY_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = false})); + NetworkTests::VerifyDnsResolutionDig(); + } + + TEST_METHOD(DnsResolutionRecordTypes) + { + VIRTIOPROXY_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = false})); + NetworkTests::VerifyDnsResolutionRecordTypes(); } }; } // namespace NetworkTests diff --git a/test/windows/PluginTests.cpp b/test/windows/PluginTests.cpp index 036efdfe8..1b5f2da75 100644 --- a/test/windows/PluginTests.cpp +++ b/test/windows/PluginTests.cpp @@ -159,8 +159,8 @@ class PluginTests VM created (settings->CustomConfigurationFlags=0) Folder mounted (* -> /test-plugin) Process created - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; ConfigurePlugin(PluginTestType::Success); @@ -182,8 +182,8 @@ class PluginTests VM created (settings->CustomConfigurationFlags=0) Folder mounted (* -> /test-plugin) Process created - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; ConfigurePlugin(PluginTestType::Success); @@ -200,8 +200,8 @@ class PluginTests VM created (settings->CustomConfigurationFlags=0) Folder mounted (* -> /test-plugin) Process created - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; ConfigurePlugin(PluginTestType::Success); @@ -259,8 +259,8 @@ class PluginTests VM created (settings->CustomConfigurationFlags=2) Folder mounted (* -> /test-plugin) Process created - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; WslConfigChange config(LxssGenerateTestConfig({.vmIdleTimeout = 1, .kernelCommandLine = L"custom"})); @@ -277,13 +277,13 @@ class PluginTests constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=10 VM created (settings->CustomConfigurationFlags=0) - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping VM created (settings->CustomConfigurationFlags=0) - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 OnDistroStarted: received same GUID - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; ConfigurePlugin(PluginTestType::SameDistroId); @@ -301,11 +301,11 @@ class PluginTests constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=14 VM created (settings->CustomConfigurationFlags=0) - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 Distribution Stopping, name=test_distro, package=, PidNs=* - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 Init's pid is different (* ! = *) - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; ConfigurePlugin(PluginTestType::InitPidIsDifferent); @@ -341,8 +341,8 @@ class PluginTests LR"(Plugin loaded. TestMode=7 VM created (settings->CustomConfigurationFlags=0) API error tests passed - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; ConfigurePlugin(PluginTestType::ApiErrors); @@ -442,8 +442,8 @@ class PluginTests constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=5 VM created (settings->CustomConfigurationFlags=0) - Distribution started, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping OnVmStopping: E_UNEXPECTED)"; @@ -459,7 +459,7 @@ class PluginTests constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=4 VM created (settings->CustomConfigurationFlags=0) - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 OnDistroStarted: E_UNEXPECTED VM Stopping)"; @@ -479,8 +479,8 @@ class PluginTests constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=6 VM created (settings->CustomConfigurationFlags=0) - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 OnDistroStopping: E_UNEXPECTED VM Stopping)"; @@ -515,7 +515,7 @@ class PluginTests constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=12 VM created (settings->CustomConfigurationFlags=0) - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 OnDistroStarted: E_FAIL VM Stopping)"; @@ -543,8 +543,8 @@ class PluginTests VM created (settings->CustomConfigurationFlags=0) Folder mounted (* -> /test-plugin) Process created - Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=12 - Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=12 + Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=13 + Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=13 VM Stopping)"; ValidateLogFile(ExpectedOutput); @@ -568,14 +568,14 @@ class PluginTests VM created (settings->CustomConfigurationFlags=0) Folder mounted (* -> /test-plugin) Process created - Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=12 + Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=13 VM Stopping - Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=12 + Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=13 VM created (settings->CustomConfigurationFlags=0) Folder mounted (* -> /test-plugin) Process created - Distribution registered, name=plugin-test-distro-vhd, package=, Flavor=debian, Version=12 - Distribution unregistered, name=plugin-test-distro-vhd, package=, Flavor=debian, Version=12 + Distribution registered, name=plugin-test-distro-vhd, package=, Flavor=debian, Version=13 + Distribution unregistered, name=plugin-test-distro-vhd, package=, Flavor=debian, Version=13 VM Stopping)"; ValidateLogFile(ExpectedOutput); @@ -593,9 +593,9 @@ class PluginTests constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=15 VM created (settings->CustomConfigurationFlags=0) - Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=12 + Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=13 OnDistributionRegistered: E_UNEXPECTED - Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=12 + Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=13 OnDistributionUnregistered: E_UNEXPECTED VM Stopping)"; @@ -611,11 +611,11 @@ class PluginTests constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=16 VM created (settings->CustomConfigurationFlags=0) - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 Process created Failed process launch returned: -2147467259 Invalid distro launch returned: -2147220717 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; StartWsl(0); @@ -632,8 +632,8 @@ class PluginTests LR"(Plugin loaded. TestMode=17 VM created (settings->CustomConfigurationFlags=0) Username: * - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; StartWsl(0); @@ -666,4 +666,4 @@ class PluginTests L"A fatal error was returned by plugin 'TestPlugin'\r\nError code: " L"Wsl/Service/CreateInstance/CreateVm/Plugin/TRUST_E_NOSIGNATURE\r\n"); } -}; \ No newline at end of file +}; diff --git a/test/windows/SimpleTests.cpp b/test/windows/SimpleTests.cpp index fff708d9e..048787df4 100644 --- a/test/windows/SimpleTests.cpp +++ b/test/windows/SimpleTests.cpp @@ -296,60 +296,5 @@ class SimpleTests VERIFY_IS_TRUE(output.find(L"/mnt/c/Program Files (x86)/Common Files") != std::wstring::npos); VERIFY_IS_TRUE(output.find(L"/mnt/c/Users/Test User/AppData/Local/Programs/Microsoft VS Code/bin") != std::wstring::npos); } - - TEST_METHOD(CreateCpioInitrd) - { - using wsl::windows::common::filesystem::CreateCpioInitrd; - using wsl::windows::common::filesystem::TempFile; - using wsl::windows::common::filesystem::TempFileFlags; - - auto validateCpio = [](size_t sourceSize) { - // Create source file with specified size - TempFile sourceFile(GENERIC_WRITE, 0, CREATE_ALWAYS, TempFileFlags::None); - std::vector sourceData(sourceSize, 'X'); - DWORD written; - THROW_IF_WIN32_BOOL_FALSE( - WriteFile(sourceFile.Handle.get(), sourceData.data(), static_cast(sourceData.size()), &written, nullptr)); - sourceFile.Handle.reset(); - - // Create CPIO archive - TempFile destFile(0, 0, CREATE_ALWAYS, TempFileFlags::None, L"img"); - CreateCpioInitrd(sourceFile.Path, destFile.Path); - - // Read and validate the CPIO archive - wil::unique_hfile cpioHandle{CreateFileW( - destFile.Path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)}; - THROW_LAST_ERROR_IF(!cpioHandle); - - LARGE_INTEGER cpioSize{}; - THROW_IF_WIN32_BOOL_FALSE(GetFileSizeEx(cpioHandle.get(), &cpioSize)); - VERIFY_ARE_EQUAL(cpioSize.QuadPart % 512, 0LL); // Archive padded to 512-byte boundary - - char header[111] = {}; - DWORD bytesRead; - THROW_IF_WIN32_BOOL_FALSE(ReadFile(cpioHandle.get(), header, 110, &bytesRead, nullptr)); - VERIFY_ARE_EQUAL(bytesRead, 110u); - - // Parse CPIO newc header: magic(6) ino mode uid gid nlink mtime filesize devmajor devminor rdevmajor rdevminor namesize check - DWORD fileSize, nameSize; - VERIFY_ARE_EQUAL(sscanf_s(header, "070701%*8x%*8x%*8x%*8x%*8x%*8x%8x%*8x%*8x%*8x%*8x%8x", &fileSize, &nameSize), 2); - - // Verify filename matches source file - auto expectedName = sourceFile.Path.filename().string(); - VERIFY_ARE_EQUAL(nameSize, static_cast(expectedName.size() + 1)); - - std::string filename(nameSize, '\0'); - THROW_IF_WIN32_BOOL_FALSE(ReadFile(cpioHandle.get(), filename.data(), nameSize, &bytesRead, nullptr)); - VERIFY_ARE_EQUAL(filename.c_str(), expectedName); - - VERIFY_ARE_EQUAL(fileSize, static_cast(sourceSize)); - }; - - // Test various sizes to exercise 4-byte alignment padding - for (size_t size : {0, 1, 2, 3, 4, 5, 100, 1024, 4096, 65536}) - { - validateCpio(size); - } - } }; } // namespace SimpleTests \ No newline at end of file diff --git a/test/windows/UnitTests.cpp b/test/windows/UnitTests.cpp index 5dfbb4ddf..8426ee165 100644 --- a/test/windows/UnitTests.cpp +++ b/test/windows/UnitTests.cpp @@ -15,6 +15,7 @@ Module Name: #include "precomp.h" #include "Common.h" +#include "install.h" #include #include #include @@ -271,6 +272,7 @@ class UnitTests }; // Validate user sessions state with gui apps disabled. + WslConfigChange config(LxssGenerateTestConfig({.guiApplications = false})); { validateUserSession(); @@ -284,7 +286,7 @@ class UnitTests // Validate user sessions state with gui apps enabled. { - WslConfigChange config(LxssGenerateTestConfig({.guiApplications = true})); + config.Update(LxssGenerateTestConfig({.guiApplications = true})); validateUserSession(); auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--user {} echo $DISPLAY", LXSST_TEST_USERNAME)); @@ -337,6 +339,9 @@ class UnitTests { WSL2_TEST_ONLY(); + // 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"); @@ -353,7 +358,7 @@ class UnitTests WSL2_TEST_ONLY(); // Override WSL's binfmt interpreter - VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"echo ':WSLInterop:M::MZ::/bin/echo:PF' > /usr/lib/binfmt.d/dummy.conf"), 0L); + 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"); @@ -2349,7 +2354,7 @@ Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND { LogInfo("Validating signature for: %ls", e.path().c_str()); - wsl::windows::common::wslutil::ValidateFileSignature(e.path().c_str()); + wsl::windows::common::install::ValidateFileSignature(e.path().c_str()); signedFiles++; } } @@ -3805,7 +3810,7 @@ localhostForwarding=true validateUidChange(L"root", 0, L"The operation completed successfully. \r\n", L"", 0); const std::wstring invalidUser = L"Nonexistent"; - validateUidChange(invalidUser, 0, L"", L"/usr/bin/id: \u2018" + invalidUser + L"\u2019: no such user\n", 1); + 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); @@ -3929,7 +3934,7 @@ localhostForwarding=true VERIFY_ARE_EQUAL(ExpectedVersion, version); }; - validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"debian", L"12"); + validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"debian", L"13"); constexpr auto testTar = L"exported-distro.tar"; constexpr auto tmpDistroName = L"tmpdistro"; @@ -4029,10 +4034,10 @@ VERSION_ID="Invalid|Format" // 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"12"); + 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"12"); + validateFlavorVersion(tmpDistroName, L"debian", L"13"); } TEST_METHOD(DistributionId) @@ -6408,8 +6413,7 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n", // 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,ctty,stderr"); + 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""); @@ -6537,5 +6541,36 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n", VERIFY_ARE_EQUAL(err, L""); } + TEST_METHOD(InteractiveMount) + { + WSL2_TEST_ONLY(); + + // 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 diff --git a/test/windows/testplugin/Plugin.cpp b/test/windows/testplugin/Plugin.cpp index 50be9e741..b7cdc3815 100644 --- a/test/windows/testplugin/Plugin.cpp +++ b/test/windows/testplugin/Plugin.cpp @@ -261,7 +261,7 @@ HRESULT OnDistroStarted(const WSLSessionInformation* Session, const WSLDistribut // Validate that the process actually ran inside the distro. auto output = ReadFromSocket(socket.get()); - const auto expected = "Debian GNU/Linux 12\n"; + const auto expected = "Debian GNU/Linux 13\n"; if (std::string(output.begin(), output.end()) != expected) { g_logfile << "Got unexpected output from bash: " << std::string(output.begin(), output.end()) diff --git a/tools/create-initrd.ps1 b/tools/create-initrd.ps1 new file mode 100644 index 000000000..f3bca1a8f --- /dev/null +++ b/tools/create-initrd.ps1 @@ -0,0 +1,67 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Creates a CPIO newc format initramfs archive containing a single file named "init" +# with mode 0100755 (rwxr-xr-x), uid 0, gid 0. + +[CmdletBinding()] +param ( + [Parameter(Mandatory)][string]$InputFile, + [Parameter(Mandatory)][string]$OutputFile +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +function Write-Pad([System.IO.Stream]$Stream) +{ + $remainder = $Stream.Position % 4 + if ($remainder -ne 0) + { + $pad = [byte[]]::new(4 - $remainder) + $Stream.Write($pad, 0, $pad.Length) + } +} + +function Write-CpioEntry([System.IO.Stream]$Stream, [byte[]]$NameBytes, [byte[]]$FileData, [int]$Mode, [uint32]$Mtime) +{ + $header = "070701" + # header magic + "00000001" + # inode + ("{0:X8}" -f $Mode) + # mode + "00000000" + # uid + "00000000" + # gid + "00000001" + # nlink + ("{0:X8}" -f $Mtime) + # mtime + ("{0:X8}" -f $FileData.Length) + # filesize + "00000000" + # devmajor + "00000000" + # devminor + "00000000" + # rdevmajor + "00000000" + # rdevminor + ("{0:X8}" -f $NameBytes.Length) + # namesize + "00000000" # check + $headerBytes = [System.Text.Encoding]::ASCII.GetBytes($header) + $Stream.Write($headerBytes, 0, $headerBytes.Length) + $Stream.Write($NameBytes, 0, $NameBytes.Length) + Write-Pad $Stream + if ($FileData.Length -gt 0) + { + $Stream.Write($FileData, 0, $FileData.Length) + Write-Pad $Stream + } +} + +$data = [System.IO.File]::ReadAllBytes($InputFile) +$name = [System.Text.Encoding]::ASCII.GetBytes("init`0") +$mtime = [uint32][System.DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + +$out = [System.IO.File]::Create($OutputFile) +try +{ + Write-CpioEntry $out $name $data 0x81ED $mtime # S_IFREG | 0755 + $trailer = [System.Text.Encoding]::ASCII.GetBytes("TRAILER!!!`0") + Write-CpioEntry $out $trailer ([byte[]]::new(0)) 0 0 +} +finally +{ + $out.Close() +} diff --git a/tools/devops/create-change.py b/tools/devops/create-change.py index a791206e1..0fd2d32f4 100644 --- a/tools/devops/create-change.py +++ b/tools/devops/create-change.py @@ -12,8 +12,9 @@ @click.argument('committer', required=True) @click.argument('message', required=True) @click.argument('branch', required=True) +@click.argument('target_branch', required=True) @click.option('--debug', default=False, is_flag=True) -def main(repo_path: str, token: str, committer: str, message: str, branch: str, debug: bool): +def main(repo_path: str, token: str, committer: str, message: str, branch: str, target_branch: str, debug: bool): try: repo = Repo(repo_path) @@ -41,7 +42,7 @@ def main(repo_path: str, token: str, committer: str, message: str, branch: str, 'title': message, 'description': 'Automated change', 'head': branch, - 'base': 'master' + 'base': target_branch } response = requests.post(f'https://api.github.com/repos/{REPO}/pulls', headers=headers, data=json.dumps(body), timeout=30) diff --git a/tools/test/Microsoft.WSL.TestDistro.nuspec b/tools/test/Microsoft.WSL.TestDistro.nuspec index 4393526c7..05d18dc04 100644 --- a/tools/test/Microsoft.WSL.TestDistro.nuspec +++ b/tools/test/Microsoft.WSL.TestDistro.nuspec @@ -8,6 +8,7 @@ WSL Test distribution - + + - \ No newline at end of file + diff --git a/tools/test/build-test-distro.ps1 b/tools/test/build-test-distro.ps1 index 3918adccc..8b12ba5bd 100644 --- a/tools/test/build-test-distro.ps1 +++ b/tools/test/build-test-distro.ps1 @@ -8,7 +8,7 @@ #> [CmdletBinding()] -Param ($Base) +Param ($Base, [string]$Platform) $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest @@ -32,10 +32,18 @@ function RunInDistro { } } +if ([string]::IsNullOrEmpty($Platform)) { + if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { + $Platform = "arm64" + } else { + $Platform = "x64" + } +} + $git_version = (git describe --tags).split('-') $version = "$($git_version[0])-$($git_version[1])" -echo "Building test_distro version: $version" +echo "Building test_distro version: $version for $Platform" Run { wsl.exe --install $Base --name test_distro --version 2 --no-launch } @@ -50,7 +58,7 @@ RunInDistro("rm -rf /etc/wsl-distribution.conf /etc/wsl.conf /usr/share/doc/* /v RunInDistro('rm -rf -- $(ls /usr/share/locale ^| grep -vE "en|locale.alias")') RunInDistro("rm /usr/lib/systemd/user/{systemd-tmpfiles-setup.service,systemd-tmpfiles-clean.timer,systemd-tmpfiles-clean.service}") RunInDistro("rm /usr/lib/systemd/system/{systemd-tmpfiles-setup-dev.service,systemd-tmpfiles-setup.service,systemd-tmpfiles-clean.timer,systemd-tmpfiles-clean.service} /lib/systemd/system/{kmod-static-nodes.service,kmod.service,sysinit.target.wants/kmod-static-nodes.service}") -Run { wsl.exe --export test_distro "test_distro.tar" } -Run { wsl.exe xz -e9 "test_distro.tar" } +New-Item -ItemType Directory -Path $Platform -Force +Run { wsl.exe --export --format tar.xz test_distro "$Platform\test_distro.tar.xz" } -& "$PSScriptRoot/../../_deps/nuget.exe" pack Microsoft.WSL.TestDistro.nuspec -Properties version=$version \ No newline at end of file +& "$PSScriptRoot/../../_deps/nuget.exe" pack Microsoft.WSL.TestDistro.nuspec -Properties version=$version diff --git a/tools/test/run-tests.ps1 b/tools/test/run-tests.ps1 index e07d69646..bfa097b32 100644 --- a/tools/test/run-tests.ps1 +++ b/tools/test/run-tests.ps1 @@ -45,9 +45,37 @@ if ($Fast) $SetupScript = $null } -te.exe $TestDllPath /p:SetupScript=$SetupScript /p:Version=$Version /p:DistroPath=$DistroPath /p:Package=$Package /p:UnitTestsPath=$UnitTestsPath /p:PullRequest=$PullRequest /p:AllowUnsigned=1 @TeArgs +# Handle /attachdebugger: verify WinDbgX is available, then add /waitfordebugger so we can find and attach to the test host. +$AttachDebugger = $false +if ($TeArgs -and ($TeArgs -icontains '/attachdebugger')) +{ + $TeArgs = @($TeArgs | Where-Object { $_ -ine '/attachdebugger' }) + if (Get-Command "WinDbgX.exe" -ErrorAction SilentlyContinue) + { + $AttachDebugger = $true + $TeArgs += '/waitfordebugger' + # Run in-process so WinDbgX can attach directly to TE.exe without + # polling for a TE.ProcessHost.exe child process. + if (-not ($TeArgs -icontains '/inproc')) + { + $TeArgs += '/inproc' + } + } + else + { + Write-Warning "/attachdebugger was requested, but WinDbgX.exe was not found. Continuing without debugger." + } +} + +$teArgList = @($TestDllPath, "/p:SetupScript=$SetupScript", "/p:Version=$Version", "/p:DistroPath=$DistroPath", + "/p:Package=$Package", "/p:UnitTestsPath=$UnitTestsPath", "/p:PullRequest=$PullRequest", "/p:AllowUnsigned=1") + $TeArgs +$teProcess = Start-Process -FilePath "te.exe" -ArgumentList $teArgList -PassThru -NoNewWindow -if (!$?) +if ($AttachDebugger) { - exit 1 -} \ No newline at end of file + # /inproc is always added above, so attach directly to TE.exe. + Write-Host "Launching WinDbgX attached to TE.exe (PID: $($teProcess.Id))..." + Start-Process "WinDbgX.exe" -ArgumentList "-p $($teProcess.Id)" +} + +exit ($teProcess | Wait-Process -PassThru).ExitCode diff --git a/tools/test/setup-vm-for-tests.ps1 b/tools/test/setup-vm-for-tests.ps1 index 335d53e75..cab784548 100644 --- a/tools/test/setup-vm-for-tests.ps1 +++ b/tools/test/setup-vm-for-tests.ps1 @@ -95,7 +95,7 @@ $Platform = switch ($Arch) { if ([string]::IsNullOrEmpty($TestDistroPath)) { $TestDistroVersion = (Select-Xml -Path "$PSScriptRoot\..\..\packages.config" -XPath '/packages/package[@id=''Microsoft.WSL.TestDistro'']/@version').Node.Value - $TestDistroPath = "$PSScriptRoot\..\..\packages\Microsoft.WSL.TestDistro.$TestDistroVersion\test_distro.tar.xz" + $TestDistroPath = "$PSScriptRoot\..\..\packages\Microsoft.WSL.TestDistro.$TestDistroVersion\$Platform\test_distro.tar.xz" } if ([string]::IsNullOrEmpty($ArtifactFolder)) { diff --git a/tools/test/test.bat.in b/tools/test/test.bat.in index 767d122e2..733c884ca 100644 --- a/tools/test/test.bat.in +++ b/tools/test/test.bat.in @@ -6,7 +6,7 @@ set "Bin=${CMAKE_RUNTIME_OUTPUT_DIRECTORY}\${CMAKE_BUILD_TYPE}" set "Tools=${CMAKE_CURRENT_LIST_DIR}\tools" path %PATH%;${TAEF_SOURCE_DIR}\build\Binaries\${TARGET_PLATFORM} -powershell.exe -ExecutionPolicy Bypass "%tools%\test\run-tests.ps1" -SetupScript "%tools%\test\test-setup.ps1" -DistroPath "${TEST_DISTRO_SOURCE_DIR}test_distro.tar.xz" -TestDllPath "%bin%\wsltests.dll" -UnitTestsPath "${CMAKE_CURRENT_LIST_DIR}\test\linux\unit_tests" -Package "%bin%\installer.msix" %* || goto fail +powershell.exe -ExecutionPolicy Bypass "%tools%\test\run-tests.ps1" -SetupScript "%tools%\test\test-setup.ps1" -DistroPath "${TEST_DISTRO_SOURCE_DIR}${TARGET_PLATFORM}\test_distro.tar.xz" -TestDllPath "%bin%\wsltests.dll" -UnitTestsPath "${CMAKE_CURRENT_LIST_DIR}\test\linux\unit_tests" -Package "%bin%\installer.msix" %* || goto fail exit /b 0